Merge "Hotword: Update the package name in TEST_MAPPING"
diff --git a/apct-tests/perftests/core/Android.bp b/apct-tests/perftests/core/Android.bp
index 23464f8..ab20fdb 100644
--- a/apct-tests/perftests/core/Android.bp
+++ b/apct-tests/perftests/core/Android.bp
@@ -43,6 +43,7 @@
"apct-perftests-resources-manager-apps",
"apct-perftests-utils",
"collector-device-lib",
+ "compatibility-device-util-axt",
"core-tests-support",
"guava",
],
diff --git a/apct-tests/perftests/core/AndroidManifest.xml b/apct-tests/perftests/core/AndroidManifest.xml
index 4e24909..cd0f1cd 100644
--- a/apct-tests/perftests/core/AndroidManifest.xml
+++ b/apct-tests/perftests/core/AndroidManifest.xml
@@ -13,7 +13,9 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
-
+ <uses-permission android:name="android.permission.DEVICE_POWER" />
+ <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner" />
<profileable android:shell="true" />
diff --git a/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java b/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java
new file mode 100644
index 0000000..0802072
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.provider.Settings;
+import android.view.Display;
+
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class DisplayPerfTest {
+ private static final float DELTA = 0.001f;
+
+ @Rule
+ public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+
+ private DisplayManager mDisplayManager;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mDisplayManager = mContext.getSystemService(DisplayManager.class);
+ }
+
+ @Test
+ public void testBrightnessChanges() throws Exception {
+ final BenchmarkState state = mBenchmarkRule.getState();
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+ SystemClock.sleep(20);
+ float brightness = 0.3f;
+ while (state.keepRunning()) {
+ setAndWaitToChangeBrightness(brightness);
+ brightness = toggleBrightness(brightness);
+ }
+ }
+
+ private float toggleBrightness(float oldBrightness) {
+ float[] brightnesses = new float[]{0.3f, 0.5f};
+ if (oldBrightness == brightnesses[0]) {
+ return brightnesses[1];
+ }
+ return brightnesses[0];
+ }
+
+ private void setAndWaitToChangeBrightness(float brightness) throws Exception {
+ mDisplayManager.setBrightness(0, brightness);
+ PollingCheck.check("Brightness is not set to the expected value", 500,
+ () -> isInRange(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY), brightness,
+ DELTA));
+ }
+
+ private boolean isInRange(float value, float target, float delta) {
+ return target - delta <= value && target + delta >= value;
+ }
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index 431f06c..7bdaa2b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4749,7 +4749,7 @@
method @Deprecated public int startOp(@NonNull String, int, @NonNull String);
method public int startOp(@NonNull String, int, @Nullable String, @Nullable String, @Nullable String);
method @Deprecated public int startOpNoThrow(@NonNull String, int, @NonNull String);
- method public int startOpNoThrow(@NonNull String, int, @NonNull String, @NonNull String, @Nullable String);
+ method public int startOpNoThrow(@NonNull String, int, @NonNull String, @Nullable String, @Nullable String);
method public int startProxyOp(@NonNull String, int, @NonNull String, @Nullable String, @Nullable String);
method public int startProxyOpNoThrow(@NonNull String, int, @NonNull String, @Nullable String, @Nullable String);
method public void startWatchingActive(@NonNull String[], @NonNull java.util.concurrent.Executor, @NonNull android.app.AppOpsManager.OnOpActiveChangedListener);
@@ -19427,9 +19427,30 @@
public final class GnssCapabilities implements android.os.Parcelable {
method public int describeContents();
method public boolean hasAntennaInfo();
+ method public boolean hasGeofencing();
method @Deprecated public boolean hasGnssAntennaInfo();
+ method public boolean hasLowPowerMode();
+ method public boolean hasMeasurementCorrections();
+ method public boolean hasMeasurementCorrectionsExcessPathLength();
+ method public boolean hasMeasurementCorrectionsForDriving();
+ method public boolean hasMeasurementCorrectionsLosSats();
+ method public boolean hasMeasurementCorrectionsReflectingPlane();
+ method public boolean hasMeasurementCorrelationVectors();
method public boolean hasMeasurements();
+ method public boolean hasMsa();
+ method public boolean hasMsb();
method public boolean hasNavigationMessages();
+ method public boolean hasOnDemandTime();
+ method public boolean hasPowerMultibandAcquisition();
+ method public boolean hasPowerMultibandTracking();
+ method public boolean hasPowerOtherModes();
+ method public boolean hasPowerSinglebandAcquisition();
+ method public boolean hasPowerSinglebandTracking();
+ method public boolean hasPowerTotal();
+ method public boolean hasSatelliteBlocklist();
+ method public boolean hasSatellitePvt();
+ method public boolean hasScheduling();
+ method public boolean hasSingleShot();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssCapabilities> CREATOR;
}
@@ -19439,8 +19460,29 @@
ctor public GnssCapabilities.Builder(@NonNull android.location.GnssCapabilities);
method @NonNull public android.location.GnssCapabilities build();
method @NonNull public android.location.GnssCapabilities.Builder setHasAntennaInfo(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasGeofencing(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasLowPowerMode(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrections(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsExcessPathLength(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsForDriving(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsLosSats(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsReflectingPlane(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrelationVectors(boolean);
method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurements(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMsa(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasMsb(boolean);
method @NonNull public android.location.GnssCapabilities.Builder setHasNavigationMessages(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasOnDemandTime(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasPowerMultibandAcquisition(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasPowerMultibandTracking(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasPowerOtherModes(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasPowerSinglebandAcquisition(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasPowerSinglebandTracking(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasPowerTotal(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasSatelliteBlocklist(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasSatellitePvt(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasScheduling(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShot(boolean);
}
public final class GnssClock implements android.os.Parcelable {
@@ -41556,6 +41598,7 @@
field public static final String KEY_DATA_LIMIT_NOTIFICATION_BOOL = "data_limit_notification_bool";
field public static final String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
field public static final String KEY_DATA_RAPID_NOTIFICATION_BOOL = "data_rapid_notification_bool";
+ field public static final String KEY_DATA_SWITCH_VALIDATION_MIN_INTERVAL_MILLIS_LONG = "data_switch_validation_min_gap_long";
field public static final String KEY_DATA_SWITCH_VALIDATION_TIMEOUT_LONG = "data_switch_validation_timeout_long";
field public static final String KEY_DATA_WARNING_NOTIFICATION_BOOL = "data_warning_notification_bool";
field public static final String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
@@ -43775,7 +43818,7 @@
method public void registerTelephonyCallback(int, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public void requestCellInfoUpdate(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyManager.CellInfoCallback);
method @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.ACCESS_FINE_LOCATION}) public android.telephony.NetworkScan requestNetworkScan(android.telephony.NetworkScanRequest, java.util.concurrent.Executor, android.telephony.TelephonyScanManager.NetworkScanCallback);
- method @Nullable @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.ACCESS_FINE_LOCATION}) public android.telephony.NetworkScan requestNetworkScan(int, @NonNull android.telephony.NetworkScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyScanManager.NetworkScanCallback);
+ method @Nullable @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE}) public android.telephony.NetworkScan requestNetworkScan(int, @NonNull android.telephony.NetworkScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyScanManager.NetworkScanCallback);
method public void sendDialerSpecialCode(String);
method public String sendEnvelopeWithStatus(String);
method @RequiresPermission(android.Manifest.permission.CALL_PHONE) public void sendUssdRequest(String, android.telephony.TelephonyManager.UssdResponseCallback, android.os.Handler);
@@ -48390,9 +48433,9 @@
public static interface GestureDetector.OnGestureListener {
method public boolean onDown(@NonNull android.view.MotionEvent);
- method public boolean onFling(@NonNull android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
+ method public boolean onFling(@Nullable android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
method public void onLongPress(@NonNull android.view.MotionEvent);
- method public boolean onScroll(@NonNull android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
+ method public boolean onScroll(@Nullable android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
method public void onShowPress(@NonNull android.view.MotionEvent);
method public boolean onSingleTapUp(@NonNull android.view.MotionEvent);
}
@@ -48403,9 +48446,9 @@
method public boolean onDoubleTap(@NonNull android.view.MotionEvent);
method public boolean onDoubleTapEvent(@NonNull android.view.MotionEvent);
method public boolean onDown(@NonNull android.view.MotionEvent);
- method public boolean onFling(@NonNull android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
+ method public boolean onFling(@Nullable android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
method public void onLongPress(@NonNull android.view.MotionEvent);
- method public boolean onScroll(@NonNull android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
+ method public boolean onScroll(@Nullable android.view.MotionEvent, @NonNull android.view.MotionEvent, float, float);
method public void onShowPress(@NonNull android.view.MotionEvent);
method public boolean onSingleTapConfirmed(@NonNull android.view.MotionEvent);
method public boolean onSingleTapUp(@NonNull android.view.MotionEvent);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0b428d9..761d9fa 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5471,32 +5471,9 @@
}
public final class GnssCapabilities implements android.os.Parcelable {
- method public boolean hasGeofencing();
- method public boolean hasLowPowerMode();
- method public boolean hasMeasurementCorrections();
- method public boolean hasMeasurementCorrectionsExcessPathLength();
- method public boolean hasMeasurementCorrectionsForDriving();
- method public boolean hasMeasurementCorrectionsLosSats();
method @Deprecated public boolean hasMeasurementCorrectionsReflectingPane();
- method public boolean hasMeasurementCorrectionsReflectingPlane();
- method public boolean hasMeasurementCorrelationVectors();
method @Deprecated public boolean hasNavMessages();
method @Deprecated public boolean hasSatelliteBlacklist();
- method public boolean hasSatelliteBlocklist();
- method public boolean hasSatellitePvt();
- }
-
- public static final class GnssCapabilities.Builder {
- method @NonNull public android.location.GnssCapabilities.Builder setHasGeofencing(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasLowPowerMode(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrections(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsExcessPathLength(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsForDriving(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsLosSats(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrectionsReflectingPlane(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasMeasurementCorrelationVectors(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasSatelliteBlocklist(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasSatellitePvt(boolean);
}
public final class GnssExcessPathInfo implements android.os.Parcelable {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 28404d5..0cb00d9 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8189,7 +8189,7 @@
* @see #startOp(String, int, String, String, String)
*/
public int startOpNoThrow(@NonNull String op, int uid, @NonNull String packageName,
- @NonNull String attributionTag, @Nullable String message) {
+ @Nullable String attributionTag, @Nullable String message) {
return startOpNoThrow(strOpToOp(op), uid, packageName, false, attributionTag, message);
}
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index a51b9d3..27f9f54 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1406,6 +1406,17 @@
}
/**
+ * Return the number of entries in the cache. This is used for testing and has package-only
+ * visibility.
+ * @hide
+ */
+ public int size() {
+ synchronized (mLock) {
+ return mCache.size();
+ }
+ }
+
+ /**
* Returns a list of caches alive at the current time.
*/
@GuardedBy("sGlobalLock")
@@ -1612,8 +1623,12 @@
* @hide
*/
public static void onTrimMemory() {
- for (PropertyInvalidatedCache pic : getActiveCaches()) {
- pic.clear();
+ ArrayList<PropertyInvalidatedCache> activeCaches;
+ synchronized (sGlobalLock) {
+ activeCaches = getActiveCaches();
+ }
+ for (int i = 0; i < activeCaches.size(); i++) {
+ activeCaches.get(i).clear();
}
}
}
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 5b0bd96..0af96c2 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -175,6 +175,23 @@
"file_patterns": [
"(/|^)KeyguardManager.java"
]
+ },
+ {
+ "name": "FrameworksCoreTests",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "include-filter": "android.app.PropertyInvalidatedCacheTest"
+ }
+ ],
+ "file_patterns": [
+ "(/|^)PropertyInvalidatedCache.java"
+ ]
}
],
"presubmit-large": [
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java
index a2277e9..3325d1e 100644
--- a/core/java/android/companion/AssociationRequest.java
+++ b/core/java/android/companion/AssociationRequest.java
@@ -35,7 +35,6 @@
import android.provider.OneTimeUseBuilder;
import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.DataClass;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -55,13 +54,6 @@
* You can also set {@link Builder#setSingleDevice single device} to request a popup with single
* device to be shown instead of a list to choose from
*/
-@DataClass(
- genConstructor = false,
- genToString = true,
- genEqualsHashCode = true,
- genHiddenGetters = true,
- genParcelable = true,
- genConstDefs = false)
public final class AssociationRequest implements Parcelable {
/**
* Device profile: watch.
@@ -139,24 +131,28 @@
/**
* If set, only devices matching either of the given filters will be shown to the user
*/
- @DataClass.PluralOf("deviceFilter")
- private final @NonNull List<DeviceFilter<?>> mDeviceFilters;
+ @NonNull
+ private final List<DeviceFilter<?>> mDeviceFilters;
/**
* Profile of the device.
*/
- private final @Nullable @DeviceProfile String mDeviceProfile;
+ @Nullable
+ @DeviceProfile
+ private final String mDeviceProfile;
/**
* The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
* "self-managed" association.
*/
- private @Nullable CharSequence mDisplayName;
+ @Nullable
+ private CharSequence mDisplayName;
/**
* The device that was associated. Will be null for "self-managed" association.
*/
- private @Nullable AssociatedDevice mAssociatedDevice;
+ @Nullable
+ private AssociatedDevice mAssociatedDevice;
/**
* Whether the association is to be managed by the companion application.
@@ -175,21 +171,24 @@
* Populated by the system.
* @hide
*/
- private @Nullable String mPackageName;
+ @Nullable
+ private String mPackageName;
/**
* The UserId of the user the association will belong to.
* Populated by the system.
* @hide
*/
- private @UserIdInt int mUserId;
+ @UserIdInt
+ private int mUserId;
/**
* The user-readable description of the device profile's privileges.
* Populated by the system.
* @hide
*/
- private @Nullable String mDeviceProfilePrivilegesDescription;
+ @Nullable
+ private String mDeviceProfilePrivilegesDescription;
/**
* The time at which his request was created
@@ -243,7 +242,9 @@
/**
* @return profile of the companion device.
*/
- public @Nullable @DeviceProfile String getDeviceProfile() {
+ @Nullable
+ @DeviceProfile
+ public String getDeviceProfile() {
return mDeviceProfile;
}
@@ -251,7 +252,8 @@
* The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
* "self-managed" association.
*/
- public @Nullable CharSequence getDisplayName() {
+ @Nullable
+ public CharSequence getDisplayName() {
return mDisplayName;
}
@@ -328,9 +330,9 @@
*/
public static final class Builder extends OneTimeUseBuilder<AssociationRequest> {
private boolean mSingleDevice = false;
- private @Nullable ArrayList<DeviceFilter<?>> mDeviceFilters = null;
- private @Nullable String mDeviceProfile;
- private @Nullable CharSequence mDisplayName;
+ private ArrayList<DeviceFilter<?>> mDeviceFilters = null;
+ private String mDeviceProfile;
+ private CharSequence mDisplayName;
private boolean mSelfManaged = false;
private boolean mForceConfirmation = false;
@@ -432,29 +434,13 @@
}
}
-
-
-
- // Code below generated by codegen v1.0.23.
- //
- // DO NOT MODIFY!
- // CHECKSTYLE:OFF Generated code
- //
- // To regenerate run:
- // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/companion/AssociationRequest.java
- //
- // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
- // Settings > Editor > Code Style > Formatter Control
- //@formatter:off
-
-
/**
* The device that was associated. Will be null for "self-managed" association.
*
* @hide
*/
- @DataClass.Generated.Member
- public @Nullable AssociatedDevice getAssociatedDevice() {
+ @Nullable
+ public AssociatedDevice getAssociatedDevice() {
return mAssociatedDevice;
}
@@ -464,8 +450,8 @@
*
* @hide
*/
- @DataClass.Generated.Member
- public @Nullable String getPackageName() {
+ @Nullable
+ public String getPackageName() {
return mPackageName;
}
@@ -475,8 +461,8 @@
*
* @hide
*/
- @DataClass.Generated.Member
- public @UserIdInt int getUserId() {
+ @UserIdInt
+ public int getUserId() {
return mUserId;
}
@@ -486,8 +472,8 @@
*
* @hide
*/
- @DataClass.Generated.Member
- public @Nullable String getDeviceProfilePrivilegesDescription() {
+ @Nullable
+ public String getDeviceProfilePrivilegesDescription() {
return mDeviceProfilePrivilegesDescription;
}
@@ -496,7 +482,6 @@
*
* @hide
*/
- @DataClass.Generated.Member
public long getCreationTime() {
return mCreationTime;
}
@@ -507,47 +492,34 @@
*
* @hide
*/
- @DataClass.Generated.Member
public boolean isSkipPrompt() {
return mSkipPrompt;
}
@Override
- @DataClass.Generated.Member
public String toString() {
- // You can override field toString logic by defining methods like:
- // String fieldNameToString() { ... }
-
- return "AssociationRequest { " +
- "singleDevice = " + mSingleDevice + ", " +
- "deviceFilters = " + mDeviceFilters + ", " +
- "deviceProfile = " + mDeviceProfile + ", " +
- "displayName = " + mDisplayName + ", " +
- "associatedDevice = " + mAssociatedDevice + ", " +
- "selfManaged = " + mSelfManaged + ", " +
- "forceConfirmation = " + mForceConfirmation + ", " +
- "packageName = " + mPackageName + ", " +
- "userId = " + mUserId + ", " +
- "deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription + ", " +
- "creationTime = " + mCreationTime + ", " +
- "skipPrompt = " + mSkipPrompt +
- " }";
+ return "AssociationRequest { "
+ + "singleDevice = " + mSingleDevice
+ + ", deviceFilters = " + mDeviceFilters
+ + ", deviceProfile = " + mDeviceProfile
+ + ", displayName = " + mDisplayName
+ + ", associatedDevice = " + mAssociatedDevice
+ + ", selfManaged = " + mSelfManaged
+ + ", forceConfirmation = " + mForceConfirmation
+ + ", packageName = " + mPackageName
+ + ", userId = " + mUserId
+ + ", deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription
+ + ", creationTime = " + mCreationTime
+ + ", skipPrompt = " + mSkipPrompt
+ + " }";
}
@Override
- @DataClass.Generated.Member
public boolean equals(@Nullable Object o) {
- // You can override field equality logic by defining either of the methods like:
- // boolean fieldNameEquals(AssociationRequest other) { ... }
- // boolean fieldNameEquals(FieldType otherValue) { ... }
-
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- @SuppressWarnings("unchecked")
AssociationRequest that = (AssociationRequest) o;
- //noinspection PointlessBooleanExpression
- return true
- && mSingleDevice == that.mSingleDevice
+ return mSingleDevice == that.mSingleDevice
&& Objects.equals(mDeviceFilters, that.mDeviceFilters)
&& Objects.equals(mDeviceProfile, that.mDeviceProfile)
&& Objects.equals(mDisplayName, that.mDisplayName)
@@ -556,17 +528,14 @@
&& mForceConfirmation == that.mForceConfirmation
&& Objects.equals(mPackageName, that.mPackageName)
&& mUserId == that.mUserId
- && Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription)
+ && Objects.equals(mDeviceProfilePrivilegesDescription,
+ that.mDeviceProfilePrivilegesDescription)
&& mCreationTime == that.mCreationTime
&& mSkipPrompt == that.mSkipPrompt;
}
@Override
- @DataClass.Generated.Member
public int hashCode() {
- // You can override field hashCode logic by defining methods like:
- // int fieldNameHashCode() { ... }
-
int _hash = 1;
_hash = 31 * _hash + Boolean.hashCode(mSingleDevice);
_hash = 31 * _hash + Objects.hashCode(mDeviceFilters);
@@ -584,21 +553,18 @@
}
@Override
- @DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
- // You can override field parcelling by defining methods like:
- // void parcelFieldName(Parcel dest, int flags) { ... }
-
int flg = 0;
if (mSingleDevice) flg |= 0x1;
- if (mSelfManaged) flg |= 0x20;
- if (mForceConfirmation) flg |= 0x40;
- if (mSkipPrompt) flg |= 0x800;
- if (mDeviceProfile != null) flg |= 0x4;
- if (mDisplayName != null) flg |= 0x8;
- if (mAssociatedDevice != null) flg |= 0x10;
+ if (mSelfManaged) flg |= 0x2;
+ if (mForceConfirmation) flg |= 0x4;
+ if (mSkipPrompt) flg |= 0x8;
+ if (mDeviceProfile != null) flg |= 0x10;
+ if (mDisplayName != null) flg |= 0x20;
+ if (mAssociatedDevice != null) flg |= 0x40;
if (mPackageName != null) flg |= 0x80;
- if (mDeviceProfilePrivilegesDescription != null) flg |= 0x200;
+ if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100;
+
dest.writeInt(flg);
dest.writeParcelableList(mDeviceFilters, flags);
if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
@@ -606,37 +572,36 @@
if (mAssociatedDevice != null) dest.writeTypedObject(mAssociatedDevice, flags);
if (mPackageName != null) dest.writeString(mPackageName);
dest.writeInt(mUserId);
- if (mDeviceProfilePrivilegesDescription != null) dest.writeString(mDeviceProfilePrivilegesDescription);
+ if (mDeviceProfilePrivilegesDescription != null) {
+ dest.writeString8(mDeviceProfilePrivilegesDescription);
+ }
dest.writeLong(mCreationTime);
}
@Override
- @DataClass.Generated.Member
- public int describeContents() { return 0; }
+ public int describeContents() {
+ return 0;
+ }
/** @hide */
- @SuppressWarnings({"unchecked", "RedundantCast"})
- @DataClass.Generated.Member
+ @SuppressWarnings("unchecked")
/* package-private */ AssociationRequest(@NonNull Parcel in) {
- // You can override field unparcelling by defining methods like:
- // static FieldType unparcelFieldName(Parcel in) { ... }
-
int flg = in.readInt();
boolean singleDevice = (flg & 0x1) != 0;
- boolean selfManaged = (flg & 0x20) != 0;
- boolean forceConfirmation = (flg & 0x40) != 0;
- boolean skipPrompt = (flg & 0x800) != 0;
+ boolean selfManaged = (flg & 0x2) != 0;
+ boolean forceConfirmation = (flg & 0x4) != 0;
+ boolean skipPrompt = (flg & 0x8) != 0;
List<DeviceFilter<?>> deviceFilters = new ArrayList<>();
in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader(),
(Class<android.companion.DeviceFilter<?>>) (Class<?>)
android.companion.DeviceFilter.class);
- String deviceProfile = (flg & 0x4) == 0 ? null : in.readString();
- CharSequence displayName = (flg & 0x8) == 0 ? null : (CharSequence) in.readCharSequence();
- AssociatedDevice associatedDevice = (flg & 0x10) == 0
- ? null : (AssociatedDevice) in.readTypedObject(AssociatedDevice.CREATOR);
+ String deviceProfile = (flg & 0x10) == 0 ? null : in.readString();
+ CharSequence displayName = (flg & 0x20) == 0 ? null : in.readCharSequence();
+ AssociatedDevice associatedDevice = (flg & 0x40) == 0 ? null
+ : in.readTypedObject(AssociatedDevice.CREATOR);
String packageName = (flg & 0x80) == 0 ? null : in.readString();
int userId = in.readInt();
- String deviceProfilePrivilegesDescription = (flg & 0x200) == 0 ? null : in.readString();
+ String deviceProfilePrivilegesDescription = (flg & 0x100) == 0 ? null : in.readString8();
long creationTime = in.readLong();
this.mSingleDevice = singleDevice;
@@ -644,8 +609,6 @@
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mDeviceFilters);
this.mDeviceProfile = deviceProfile;
- com.android.internal.util.AnnotationValidations.validate(
- DeviceProfile.class, null, mDeviceProfile);
this.mDisplayName = displayName;
this.mAssociatedDevice = associatedDevice;
this.mSelfManaged = selfManaged;
@@ -657,13 +620,11 @@
this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
this.mCreationTime = creationTime;
this.mSkipPrompt = skipPrompt;
-
- // onConstructed(); // You can define this method to get a callback
}
- @DataClass.Generated.Member
- public static final @NonNull Parcelable.Creator<AssociationRequest> CREATOR
- = new Parcelable.Creator<AssociationRequest>() {
+ @NonNull
+ public static final Parcelable.Creator<AssociationRequest> CREATOR =
+ new Parcelable.Creator<AssociationRequest>() {
@Override
public AssociationRequest[] newArray(int size) {
return new AssociationRequest[size];
@@ -674,17 +635,4 @@
return new AssociationRequest(in);
}
};
-
- @DataClass.Generated(
- time = 1663088980513L,
- codegenVersion = "1.0.23",
- sourceFile = "frameworks/base/core/java/android/companion/AssociationRequest.java",
- inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\npublic static final @android.annotation.RequiresPermission java.lang.String DEVICE_PROFILE_COMPUTER\nprivate final boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate @android.annotation.Nullable android.companion.AssociatedDevice mAssociatedDevice\nprivate final boolean mSelfManaged\nprivate final boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mPackageName\nprivate @android.annotation.UserIdInt int mUserId\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate final long mCreationTime\nprivate boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic boolean isSelfManaged()\npublic boolean isForceConfirmation()\npublic boolean isSingleDevice()\npublic void setPackageName(java.lang.String)\npublic void setUserId(int)\npublic void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic void setSkipPrompt(boolean)\npublic void setDisplayName(java.lang.CharSequence)\npublic void setAssociatedDevice(android.companion.AssociatedDevice)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate boolean mSelfManaged\nprivate boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genConstDefs=false)")
- @Deprecated
- private void __metadata() {}
-
-
- //@formatter:on
- // End of generated code
-
}
diff --git a/core/java/android/companion/WifiDeviceFilter.java b/core/java/android/companion/WifiDeviceFilter.java
index 8b48f4c..7d2a25a 100644
--- a/core/java/android/companion/WifiDeviceFilter.java
+++ b/core/java/android/companion/WifiDeviceFilter.java
@@ -27,7 +27,6 @@
import android.os.Parcel;
import android.os.Parcelable;
-import com.android.internal.util.DataClass;
import com.android.internal.util.Parcelling;
import java.util.Objects;
@@ -38,32 +37,27 @@
*
* @see ScanFilter
*/
-@DataClass(
- genParcelable = true,
- genAidl = false,
- genBuilder = true,
- genEqualsHashCode = true,
- genHiddenGetters = true)
public final class WifiDeviceFilter implements DeviceFilter<ScanResult> {
/**
* If set, only devices with {@link BluetoothDevice#getName name} matching the given regular
* expression will be shown
*/
- @DataClass.ParcelWith(Parcelling.BuiltIn.ForPattern.class)
- @DataClass.MaySetToNull
- private @Nullable Pattern mNamePattern = null;
+ @Nullable
+ private final Pattern mNamePattern;
/**
* If set, only devices with BSSID matching the given one will be shown
*/
- private @Nullable MacAddress mBssid = null;
+ @Nullable
+ private final MacAddress mBssid;
/**
* If set, only bits at positions set in this mask, will be compared to the given
* {@link Builder#setBssid BSSID} filter.
*/
- private @NonNull MacAddress mBssidMask = MacAddress.BROADCAST_ADDRESS;
+ @NonNull
+ private final MacAddress mBssidMask;
/** @hide */
@Override
@@ -85,22 +79,6 @@
return MEDIUM_TYPE_WIFI;
}
-
-
- // Code below generated by codegen v1.0.15.
- //
- // DO NOT MODIFY!
- // CHECKSTYLE:OFF Generated code
- //
- // To regenerate run:
- // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/companion/WifiDeviceFilter.java
- //
- // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
- // Settings > Editor > Code Style > Formatter Control
- //@formatter:off
-
-
- @DataClass.Generated.Member
/* package-private */ WifiDeviceFilter(
@Nullable Pattern namePattern,
@Nullable MacAddress bssid,
@@ -110,8 +88,6 @@
this.mBssidMask = bssidMask;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mBssidMask);
-
- // onConstructed(); // You can define this method to get a callback
}
/**
@@ -120,8 +96,8 @@
*
* @hide
*/
- @DataClass.Generated.Member
- public @Nullable Pattern getNamePattern() {
+ @Nullable
+ public Pattern getNamePattern() {
return mNamePattern;
}
@@ -130,8 +106,8 @@
*
* @hide
*/
- @DataClass.Generated.Member
- public @Nullable MacAddress getBssid() {
+ @Nullable
+ public MacAddress getBssid() {
return mBssid;
}
@@ -141,35 +117,23 @@
*
* @hide
*/
- @DataClass.Generated.Member
- public @NonNull MacAddress getBssidMask() {
+ @NonNull
+ public MacAddress getBssidMask() {
return mBssidMask;
}
@Override
- @DataClass.Generated.Member
public boolean equals(@Nullable Object o) {
- // You can override field equality logic by defining either of the methods like:
- // boolean fieldNameEquals(WifiDeviceFilter other) { ... }
- // boolean fieldNameEquals(FieldType otherValue) { ... }
-
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- @SuppressWarnings("unchecked")
WifiDeviceFilter that = (WifiDeviceFilter) o;
- //noinspection PointlessBooleanExpression
- return true
- && Objects.equals(mNamePattern, that.mNamePattern)
+ return Objects.equals(mNamePattern, that.mNamePattern)
&& Objects.equals(mBssid, that.mBssid)
&& Objects.equals(mBssidMask, that.mBssidMask);
}
@Override
- @DataClass.Generated.Member
public int hashCode() {
- // You can override field hashCode logic by defining methods like:
- // int fieldNameHashCode() { ... }
-
int _hash = 1;
_hash = 31 * _hash + Objects.hashCode(mNamePattern);
_hash = 31 * _hash + Objects.hashCode(mBssid);
@@ -177,10 +141,8 @@
return _hash;
}
- @DataClass.Generated.Member
static Parcelling<Pattern> sParcellingForNamePattern =
- Parcelling.Cache.get(
- Parcelling.BuiltIn.ForPattern.class);
+ Parcelling.Cache.get(Parcelling.BuiltIn.ForPattern.class);
static {
if (sParcellingForNamePattern == null) {
sParcellingForNamePattern = Parcelling.Cache.put(
@@ -189,11 +151,7 @@
}
@Override
- @DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
- // You can override field parcelling by defining methods like:
- // void parcelFieldName(Parcel dest, int flags) { ... }
-
byte flg = 0;
if (mNamePattern != null) flg |= 0x1;
if (mBssid != null) flg |= 0x2;
@@ -204,33 +162,27 @@
}
@Override
- @DataClass.Generated.Member
- public int describeContents() { return 0; }
+ public int describeContents() {
+ return 0;
+ }
/** @hide */
- @SuppressWarnings({"unchecked", "RedundantCast"})
- @DataClass.Generated.Member
/* package-private */ WifiDeviceFilter(@NonNull Parcel in) {
- // You can override field unparcelling by defining methods like:
- // static FieldType unparcelFieldName(Parcel in) { ... }
-
byte flg = in.readByte();
Pattern namePattern = sParcellingForNamePattern.unparcel(in);
- MacAddress bssid = (flg & 0x2) == 0 ? null : (MacAddress) in.readTypedObject(MacAddress.CREATOR);
- MacAddress bssidMask = (MacAddress) in.readTypedObject(MacAddress.CREATOR);
+ MacAddress bssid = (flg & 0x2) == 0 ? null : in.readTypedObject(MacAddress.CREATOR);
+ MacAddress bssidMask = in.readTypedObject(MacAddress.CREATOR);
this.mNamePattern = namePattern;
this.mBssid = bssid;
this.mBssidMask = bssidMask;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mBssidMask);
-
- // onConstructed(); // You can define this method to get a callback
}
- @DataClass.Generated.Member
- public static final @NonNull Parcelable.Creator<WifiDeviceFilter> CREATOR
- = new Parcelable.Creator<WifiDeviceFilter>() {
+ @NonNull
+ public static final Parcelable.Creator<WifiDeviceFilter> CREATOR =
+ new Parcelable.Creator<WifiDeviceFilter>() {
@Override
public WifiDeviceFilter[] newArray(int size) {
return new WifiDeviceFilter[size];
@@ -246,12 +198,11 @@
* A builder for {@link WifiDeviceFilter}
*/
@SuppressWarnings("WeakerAccess")
- @DataClass.Generated.Member
public static final class Builder {
- private @Nullable Pattern mNamePattern;
- private @Nullable MacAddress mBssid;
- private @NonNull MacAddress mBssidMask;
+ @Nullable private Pattern mNamePattern;
+ @Nullable private MacAddress mBssid;
+ @NonNull private MacAddress mBssidMask;
private long mBuilderFieldsSet = 0L;
@@ -262,8 +213,8 @@
* If set, only devices with {@link BluetoothDevice#getName name} matching the given regular
* expression will be shown
*/
- @DataClass.Generated.Member
- public @NonNull Builder setNamePattern(@Nullable Pattern value) {
+ @NonNull
+ public Builder setNamePattern(@Nullable Pattern value) {
checkNotUsed();
mBuilderFieldsSet |= 0x1;
mNamePattern = value;
@@ -273,8 +224,8 @@
/**
* If set, only devices with BSSID matching the given one will be shown
*/
- @DataClass.Generated.Member
- public @NonNull Builder setBssid(@NonNull MacAddress value) {
+ @NonNull
+ public Builder setBssid(@NonNull MacAddress value) {
checkNotUsed();
mBuilderFieldsSet |= 0x2;
mBssid = value;
@@ -285,8 +236,8 @@
* If set, only bits at positions set in this mask, will be compared to the given
* {@link Builder#setBssid BSSID} filter.
*/
- @DataClass.Generated.Member
- public @NonNull Builder setBssidMask(@NonNull MacAddress value) {
+ @NonNull
+ public Builder setBssidMask(@NonNull MacAddress value) {
checkNotUsed();
mBuilderFieldsSet |= 0x4;
mBssidMask = value;
@@ -294,7 +245,8 @@
}
/** Builds the instance. This builder should not be touched after calling this! */
- public @NonNull WifiDeviceFilter build() {
+ @NonNull
+ public WifiDeviceFilter build() {
checkNotUsed();
mBuilderFieldsSet |= 0x8; // Mark builder used
@@ -307,11 +259,10 @@
if ((mBuilderFieldsSet & 0x4) == 0) {
mBssidMask = MacAddress.BROADCAST_ADDRESS;
}
- WifiDeviceFilter o = new WifiDeviceFilter(
+ return new WifiDeviceFilter(
mNamePattern,
mBssid,
mBssidMask);
- return o;
}
private void checkNotUsed() {
@@ -321,17 +272,4 @@
}
}
}
-
- @DataClass.Generated(
- time = 1582688421965L,
- codegenVersion = "1.0.15",
- sourceFile = "frameworks/base/core/java/android/companion/WifiDeviceFilter.java",
- inputSignatures = "private @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForPattern.class) @com.android.internal.util.DataClass.MaySetToNull @android.annotation.Nullable java.util.regex.Pattern mNamePattern\nprivate @android.annotation.Nullable android.net.MacAddress mBssid\nprivate @android.annotation.NonNull android.net.MacAddress mBssidMask\npublic @java.lang.Override boolean matches(android.net.wifi.ScanResult)\npublic @java.lang.Override java.lang.String getDeviceDisplayName(android.net.wifi.ScanResult)\npublic @java.lang.Override int getMediumType()\nclass WifiDeviceFilter extends java.lang.Object implements [android.companion.DeviceFilter<android.net.wifi.ScanResult>]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=false, genBuilder=true, genEqualsHashCode=true, genHiddenGetters=true)")
- @Deprecated
- private void __metadata() {}
-
-
- //@formatter:on
- // End of generated code
-
}
diff --git a/core/java/android/hardware/OverlayProperties.java b/core/java/android/hardware/OverlayProperties.java
new file mode 100644
index 0000000..2a0956b
--- /dev/null
+++ b/core/java/android/hardware/OverlayProperties.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware;
+
+import java.util.List;
+
+/**
+ * // TODO(b/242588489): Continue work, the class needs a jni-specific constructor and DisplayInfo
+ * // side constructs the object.
+ *
+ * @hide
+ */
+public final class OverlayProperties {
+ private final SupportedBufferCombinations[] mCombinations = null;
+ private final boolean mSupportFp16ForHdr = false;
+
+ static class SupportedBufferCombinations {
+ @HardwareBuffer.Format List<Integer> mHardwareBufferFormats;
+ @DataSpace.NamedDataSpace List<Integer> mDataSpaces;
+ SupportedBufferCombinations(@HardwareBuffer.Format List<Integer> hardwareBufferFormats,
+ @DataSpace.NamedDataSpace List<Integer> dataSpaces) {
+ mHardwareBufferFormats = hardwareBufferFormats;
+ mDataSpaces = dataSpaces;
+ }
+ }
+
+ /***
+ * @return if the device can support fp16.
+ */
+ public boolean supportFp16ForHdr() {
+ return mSupportFp16ForHdr;
+ }
+}
diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java
index 5b1973a..8e4a108 100644
--- a/core/java/android/hardware/camera2/CameraCaptureSession.java
+++ b/core/java/android/hardware/camera2/CameraCaptureSession.java
@@ -306,9 +306,9 @@
* requests are processed in first-in, first-out order and reprocess requests are processed in
* first-in, first-out order, respectively. However, the processing order of a regular request
* and a reprocess request in progress is not specified. In other words, a regular request
- * will always be processed before regular requets that are submitted later. A reprocess request
- * will always be processed before reprocess requests that are submitted later. However, a
- * regular request may not be processed before reprocess requests that are submitted later.<p>
+ * will always be processed before regular requests that are submitted later. A reprocess
+ * request will always be processed before reprocess requests that are submitted later. However,
+ * a regular request may not be processed before reprocess requests that are submitted later.<p>
*
* <p>Requests submitted through this method have higher priority than
* those submitted through {@link #setRepeatingRequest} or
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 9294dea..79223f5 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -32,6 +32,7 @@
import android.content.res.Resources;
import android.graphics.ColorSpace;
import android.graphics.Point;
+import android.hardware.OverlayProperties;
import android.hardware.display.DisplayManager.DisplayListener;
import android.hardware.graphics.common.DisplayDecorationSupport;
import android.media.projection.IMediaProjection;
@@ -112,6 +113,7 @@
private final SparseArray<DisplayInfo> mDisplayInfoCache = new SparseArray<>();
private final ColorSpace mWideColorSpace;
+ private final OverlayProperties mOverlayProperties = new OverlayProperties();
private int[] mDisplayIdCache;
private int mWifiDisplayScanNestCount;
@@ -726,6 +728,11 @@
return mWideColorSpace;
}
+ /** @hide */
+ public OverlayProperties getOverlaySupport() {
+ return mOverlayProperties;
+ }
+
/**
* Sets the global brightness configuration for a given user.
*
diff --git a/core/java/android/inputmethodservice/InkWindow.java b/core/java/android/inputmethodservice/InkWindow.java
index 469b52bd91..70bd504 100644
--- a/core/java/android/inputmethodservice/InkWindow.java
+++ b/core/java/android/inputmethodservice/InkWindow.java
@@ -53,6 +53,9 @@
final LayoutParams attrs = getAttributes();
attrs.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
attrs.setFitInsetsTypes(0);
+ // disable window animations.
+ // TODO(b/253477462): replace with API when available
+ attrs.windowAnimations = -1;
// TODO(b/210039666): use INPUT_FEATURE_NO_INPUT_CHANNEL once b/216179339 is fixed.
setAttributes(attrs);
// Ink window is not touchable with finger.
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 0aafaf4..6091bf9 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -50,10 +50,10 @@
private final Context mContext;
@GuardedBy("mBrokenListeners")
- private final ArrayList<AllVibratorsStateListener> mBrokenListeners = new ArrayList<>();
+ private final ArrayList<MultiVibratorStateListener> mBrokenListeners = new ArrayList<>();
@GuardedBy("mRegisteredListeners")
- private final ArrayMap<OnVibratorStateChangedListener, AllVibratorsStateListener>
+ private final ArrayMap<OnVibratorStateChangedListener, MultiVibratorStateListener>
mRegisteredListeners = new ArrayMap<>();
private final Object mLock = new Object();
@@ -147,7 +147,7 @@
Log.w(TAG, "Failed to add vibrate state listener; no vibrator manager.");
return;
}
- AllVibratorsStateListener delegate = null;
+ MultiVibratorStateListener delegate = null;
try {
synchronized (mRegisteredListeners) {
// If listener is already registered, reject and return.
@@ -155,7 +155,7 @@
Log.w(TAG, "Listener already registered.");
return;
}
- delegate = new AllVibratorsStateListener(executor, listener);
+ delegate = new MultiVibratorStateListener(executor, listener);
delegate.register(mVibratorManager);
mRegisteredListeners.put(listener, delegate);
delegate = null;
@@ -181,7 +181,7 @@
}
synchronized (mRegisteredListeners) {
if (mRegisteredListeners.containsKey(listener)) {
- AllVibratorsStateListener delegate = mRegisteredListeners.get(listener);
+ MultiVibratorStateListener delegate = mRegisteredListeners.get(listener);
delegate.unregister(mVibratorManager);
mRegisteredListeners.remove(listener);
}
@@ -238,7 +238,7 @@
* Tries to unregister individual {@link android.os.Vibrator.OnVibratorStateChangedListener}
* that were left registered to vibrators after failures to register them to all vibrators.
*
- * <p>This might happen if {@link AllVibratorsStateListener} fails to register to any vibrator
+ * <p>This might happen if {@link MultiVibratorStateListener} fails to register to any vibrator
* and also fails to unregister any previously registered single listeners to other vibrators.
*
* <p>This method never throws {@link RuntimeException} if it fails to unregister again, it will
@@ -259,10 +259,10 @@
/** Listener for a single vibrator state change. */
private static class SingleVibratorStateListener implements OnVibratorStateChangedListener {
- private final AllVibratorsStateListener mAllVibratorsListener;
+ private final MultiVibratorStateListener mAllVibratorsListener;
private final int mVibratorIdx;
- SingleVibratorStateListener(AllVibratorsStateListener listener, int vibratorIdx) {
+ SingleVibratorStateListener(MultiVibratorStateListener listener, int vibratorIdx) {
mAllVibratorsListener = listener;
mVibratorIdx = vibratorIdx;
}
@@ -552,8 +552,16 @@
}
}
- /** Listener for all vibrators state change. */
- private static class AllVibratorsStateListener {
+ /**
+ * Listener for all vibrators state change.
+ *
+ * <p>This registers a listener to all vibrators to merge the callbacks into a single state
+ * that is set to true if any individual vibrator is also true, and false otherwise.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class MultiVibratorStateListener {
private final Object mLock = new Object();
private final Executor mExecutor;
private final OnVibratorStateChangedListener mDelegate;
@@ -567,19 +575,21 @@
@GuardedBy("mLock")
private int mVibratingMask;
- AllVibratorsStateListener(@NonNull Executor executor,
+ public MultiVibratorStateListener(@NonNull Executor executor,
@NonNull OnVibratorStateChangedListener listener) {
mExecutor = executor;
mDelegate = listener;
}
- boolean hasRegisteredListeners() {
+ /** Returns true if at least one listener was registered to an individual vibrator. */
+ public boolean hasRegisteredListeners() {
synchronized (mLock) {
return mVibratorListeners.size() > 0;
}
}
- void register(VibratorManager vibratorManager) {
+ /** Registers a listener to all individual vibrators in {@link VibratorManager}. */
+ public void register(VibratorManager vibratorManager) {
int[] vibratorIds = vibratorManager.getVibratorIds();
synchronized (mLock) {
for (int i = 0; i < vibratorIds.length; i++) {
@@ -603,7 +613,8 @@
}
}
- void unregister(VibratorManager vibratorManager) {
+ /** Unregisters the listeners from all individual vibrators in {@link VibratorManager}. */
+ public void unregister(VibratorManager vibratorManager) {
synchronized (mLock) {
for (int i = mVibratorListeners.size(); --i >= 0; ) {
int vibratorId = mVibratorListeners.keyAt(i);
@@ -614,30 +625,44 @@
}
}
- void onVibrating(int vibratorIdx, boolean vibrating) {
+ /** Callback triggered by {@link SingleVibratorStateListener} for each vibrator. */
+ public void onVibrating(int vibratorIdx, boolean vibrating) {
mExecutor.execute(() -> {
- boolean anyVibrating;
+ boolean shouldNotifyStateChange;
+ boolean isAnyVibrating;
synchronized (mLock) {
+ // Bitmask indicating that all vibrators have been initialized.
int allInitializedMask = (1 << mVibratorListeners.size()) - 1;
- int vibratorMask = 1 << vibratorIdx;
- if ((mInitializedMask & vibratorMask) == 0) {
- // First state report for this vibrator, set vibrating initial value.
- mInitializedMask |= vibratorMask;
- mVibratingMask |= vibrating ? vibratorMask : 0;
- } else {
- // Flip vibrating value, if changed.
- boolean prevVibrating = (mVibratingMask & vibratorMask) != 0;
- if (prevVibrating != vibrating) {
- mVibratingMask ^= vibratorMask;
- }
+
+ // Save current global state before processing this vibrator state change.
+ boolean previousIsAnyVibrating = (mVibratingMask != 0);
+ boolean previousAreAllInitialized = (mInitializedMask == allInitializedMask);
+
+ // Mark this vibrator as initialized.
+ int vibratorMask = (1 << vibratorIdx);
+ mInitializedMask |= vibratorMask;
+
+ // Flip the vibrating bit flag for this vibrator, only if the state is changing.
+ boolean previousVibrating = (mVibratingMask & vibratorMask) != 0;
+ if (previousVibrating != vibrating) {
+ mVibratingMask ^= vibratorMask;
}
- if (mInitializedMask != allInitializedMask) {
- // Wait for all vibrators initial state to be reported before delegating.
- return;
- }
- anyVibrating = mVibratingMask != 0;
+
+ // Check new global state after processing this vibrator state change.
+ isAnyVibrating = (mVibratingMask != 0);
+ boolean areAllInitialized = (mInitializedMask == allInitializedMask);
+
+ // Prevent multiple triggers with the same state.
+ // Trigger once when all vibrators have reported their state, and then only when
+ // the merged vibrating state changes.
+ boolean isStateChanging = (previousIsAnyVibrating != isAnyVibrating);
+ shouldNotifyStateChange =
+ areAllInitialized && (!previousAreAllInitialized || isStateChanging);
}
- mDelegate.onVibratorStateChanged(anyVibrating);
+ // Notify delegate listener outside the lock, only if merged state is changing.
+ if (shouldNotifyStateChange) {
+ mDelegate.onVibratorStateChanged(isAnyVibrating);
+ }
});
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d16bbbc..84a6194 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10897,13 +10897,6 @@
"accessibility_floating_menu_migration_tooltip_prompt";
/**
- * Setting that specifies whether the software cursor accessibility service is enabled.
- * @hide
- */
- public static final String ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED =
- "accessibility_software_cursor_enabled";
-
- /**
* Whether the Adaptive connectivity option is enabled.
*
* @hide
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 21c615c..7199e57 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -36,6 +36,7 @@
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
+import android.hardware.OverlayProperties;
import android.hardware.display.BrightnessInfo;
import android.hardware.display.DeviceProductInfo;
import android.hardware.display.DisplayManager;
@@ -1290,6 +1291,18 @@
}
}
+ /** @hide */
+ @Nullable
+ public OverlayProperties getOverlaySupport() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ if (mDisplayInfo.type != TYPE_VIRTUAL) {
+ return mGlobal.getOverlaySupport();
+ }
+ return new OverlayProperties();
+ }
+ }
+
/**
* Gets the supported color modes of this device.
* @hide
diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java
index 57ba7e9..fc47c19 100644
--- a/core/java/android/view/GestureDetector.java
+++ b/core/java/android/view/GestureDetector.java
@@ -96,7 +96,8 @@
* current move {@link MotionEvent}. The distance in x and y is also supplied for
* convenience.
*
- * @param e1 The first down motion event that started the scrolling.
+ * @param e1 The first down motion event that started the scrolling. A {@code null} event
+ * indicates an incomplete event stream or error state.
* @param e2 The move motion event that triggered the current onScroll.
* @param distanceX The distance along the X axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
@@ -106,7 +107,7 @@
* and {@code e2}.
* @return true if the event is consumed, else false
*/
- boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
+ boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
float distanceY);
/**
@@ -122,7 +123,8 @@
* and the matching up {@link MotionEvent}. The calculated velocity is supplied along
* the x and y axis in pixels per second.
*
- * @param e1 The first down motion event that started the fling.
+ * @param e1 The first down motion event that started the fling. A {@code null} event
+ * indicates an incomplete event stream or error state.
* @param e2 The move motion event that triggered the current onFling.
* @param velocityX The velocity of this fling measured in pixels per second
* along the x axis.
@@ -130,7 +132,7 @@
* along the y axis.
* @return true if the event is consumed, else false
*/
- boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
+ boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
float velocityY);
}
@@ -201,12 +203,12 @@
public void onLongPress(@NonNull MotionEvent e) {
}
- public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2,
float distanceX, float distanceY) {
return false;
}
- public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
+ public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index d4875d4..1afd048 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -17,8 +17,6 @@
package android.view;
import static android.view.ImeFocusControllerProto.HAS_IME_FOCUS;
-import static android.view.ImeFocusControllerProto.NEXT_SERVED_VIEW;
-import static android.view.ImeFocusControllerProto.SERVED_VIEW;
import android.annotation.AnyThread;
import android.annotation.NonNull;
@@ -31,8 +29,6 @@
import com.android.internal.inputmethod.StartInputFlags;
import com.android.internal.inputmethod.StartInputReason;
-import java.util.Objects;
-
/**
* Responsible for IME focus handling inside {@link ViewRootImpl}.
* @hide
@@ -43,21 +39,6 @@
private final ViewRootImpl mViewRootImpl;
private boolean mHasImeFocus = false;
-
- /**
- * This is the view that should currently be served by an input method,
- * regardless of the state of setting that up.
- * @see InputMethodManagerDelegate#getLockObject()
- */
- private View mServedView;
-
- /**
- * This is the next view that will be served by the input method, when
- * we get around to updating things.
- * @see InputMethodManagerDelegate#getLockObject()
- */
- private View mNextServedView;
-
private InputMethodManagerDelegate mDelegate;
@UiThread
@@ -134,122 +115,30 @@
windowAttribute.softInputMode));
}
- boolean forceFocus = false;
- final InputMethodManagerDelegate immDelegate = getImmDelegate();
- synchronized (immDelegate.getLockObject()) {
- // Update mNextServedView when focusedView changed.
- onViewFocusChanged(viewForWindowFocus, true);
-
- // Starting new input when the next focused view is same as served view but the
- // currently active connection (if any) is not associated with it.
- final boolean nextFocusIsServedView = mServedView == viewForWindowFocus;
-
- if (nextFocusIsServedView && !immDelegate.hasActiveConnection(viewForWindowFocus)) {
- forceFocus = true;
- }
- }
-
- immDelegate.startInputOnWindowFocusGain(viewForWindowFocus,
- windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
+ getImmDelegate().onPostWindowFocus(viewForWindowFocus, windowAttribute);
}
/**
* @see InputMethodManager#checkFocus()
*/
public boolean checkFocus(boolean forceNewFocus, boolean startInput) {
- final InputMethodManagerDelegate immDelegate = getImmDelegate();
- synchronized (immDelegate.getLockObject()) {
- if (!immDelegate.isCurrentRootView(mViewRootImpl)) {
- return false;
- }
- if (mServedView == mNextServedView && !forceNewFocus) {
- return false;
- }
- if (DEBUG) {
- Log.v(TAG, "checkFocus: view=" + mServedView
- + " next=" + mNextServedView
- + " force=" + forceNewFocus
- + " package="
- + (mServedView != null ? mServedView.getContext().getPackageName()
- : "<none>"));
- }
- // Close the connection when no next served view coming.
- if (mNextServedView == null) {
- immDelegate.finishInput();
- immDelegate.closeCurrentIme();
- return false;
- }
- mServedView = mNextServedView;
- immDelegate.finishComposingText();
- }
-
- if (startInput) {
- immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */,
- 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
- }
- return true;
+ return getImmDelegate().checkFocus(forceNewFocus, startInput, mViewRootImpl);
}
@UiThread
void onViewFocusChanged(View view, boolean hasFocus) {
- if (view == null || view.isTemporarilyDetached()) {
- return;
- }
- final InputMethodManagerDelegate immDelegate = getImmDelegate();
- synchronized (immDelegate.getLockObject()) {
- if (!immDelegate.isCurrentRootView(view.getViewRootImpl())) {
- return;
- }
- if (!view.hasImeFocus() || !view.hasWindowFocus()) {
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "onViewFocusChanged, view=" + InputMethodDebug.dumpViewInfo(view)
- + ", mServedView=" + InputMethodDebug.dumpViewInfo(mServedView));
- }
-
- // We don't need to track the next served view when the view lost focus here because:
- // 1) The current view focus may be cleared temporary when in touch mode, closing input
- // at this moment isn't the right way.
- // 2) We only care about the served view change when it focused, since changing input
- // connection when the focus target changed is reasonable.
- // 3) Setting the next served view as null when no more served view should be handled in
- // other special events (e.g. view detached from window or the window dismissed).
- if (hasFocus) {
- mNextServedView = view;
- }
- }
- mViewRootImpl.dispatchCheckFocus();
+ getImmDelegate().onViewFocusChanged(view, hasFocus);
}
@UiThread
void onViewDetachedFromWindow(View view) {
- final InputMethodManagerDelegate immDelegate = getImmDelegate();
- synchronized (immDelegate.getLockObject()) {
- if (!immDelegate.isCurrentRootView(view.getViewRootImpl())) {
- return;
- }
- if (mNextServedView == view) {
- mNextServedView = null;
- }
- if (mServedView == view) {
- mViewRootImpl.dispatchCheckFocus();
- }
- }
+ getImmDelegate().onViewDetachedFromWindow(view, mViewRootImpl);
+
}
@UiThread
void onWindowDismissed() {
- final InputMethodManagerDelegate immDelegate = getImmDelegate();
- synchronized (immDelegate.getLockObject()) {
- if (!immDelegate.isCurrentRootView(mViewRootImpl)) {
- return;
- }
- if (mServedView != null) {
- immDelegate.finishInput();
- }
- immDelegate.setCurrentRootView(null);
- }
+ getImmDelegate().onWindowDismissed(mViewRootImpl);
mHasImeFocus = false;
}
@@ -302,75 +191,21 @@
public interface InputMethodManagerDelegate {
/**
* Starts the input connection.
- * Note that this method must not hold the {@link InputMethodManager} lock with
- * {@link InputMethodManagerDelegate#getLockObject()} while {@link InputMethodManager}
- * calling into app-code in different threads.
*/
boolean startInput(@StartInputReason int startInputReason, View focusedView,
@StartInputFlags int startInputFlags,
@WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags);
- /**
- * Starts the input connection when gaining the window focus.
- * Note that this method must not hold the {@link InputMethodManager} lock with
- * {@link InputMethodManagerDelegate#getLockObject()} while {@link InputMethodManager}
- * calling into app-code in different threads.
- */
- void startInputOnWindowFocusGain(View rootView,
- @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags,
- boolean forceNewFocus);
- void finishInput();
+
+ void onPostWindowFocus(View viewForWindowFocus,
+ @NonNull WindowManager.LayoutParams windowAttribute);
+ void onViewFocusChanged(@NonNull View view, boolean hasFocus);
+ boolean checkFocus(boolean forceNewFocus, boolean startInput, ViewRootImpl viewRootImpl);
+ void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl);
+ void onWindowDismissed(ViewRootImpl viewRootImpl);
+
void finishInputAndReportToIme();
- void closeCurrentIme();
- void finishComposingText();
void setCurrentRootView(ViewRootImpl rootView);
boolean isCurrentRootView(ViewRootImpl rootView);
- boolean hasActiveConnection(View view);
-
- /**
- * Returns the {@code InputMethodManager#mH} lock object.
- * Used for {@link ImeFocusController} to guard the served view being accessed by
- * {@link InputMethodManager} in different threads.
- */
- Object getLockObject();
- }
-
- /**
- * Returns The current IME served view for {@link InputMethodManager}.
- * Used to start input connection or check the caller's validity when calling
- * {@link InputMethodManager} APIs.
- * Note that this method requires to be called inside {@code InputMethodManager#mH} lock for
- * data consistency.
- */
- public View getServedViewLocked() {
- return mServedView;
- }
-
- /**
- * Returns The next incoming IME served view for {@link InputMethodManager}.
- * Note that this method requires to be called inside {@code InputMethodManager#mH} lock for
- * data consistency.
- */
- public View getNextServedViewLocked() {
- return mNextServedView;
- }
-
- /**
- * Clears the served & the next served view when the controller triggers
- * {@link InputMethodManagerDelegate#finishInput()} or
- * {@link InputMethodManagerDelegate#finishInputAndReportToIme()}.
- * Note that this method requires to be called inside {@code InputMethodManager#mH} lock for
- * data consistency.
- *
- * @return The {@code mServedView} that has cleared, or {@code null} means nothing to clear.
- */
- public View clearServedViewsLocked() {
- View clearedView = null;
- mNextServedView = null;
- if (mServedView != null) {
- clearedView = mServedView;
- mServedView = null;
- }
- return clearedView;
}
/**
@@ -384,8 +219,6 @@
void dumpDebug(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(HAS_IME_FOCUS, mHasImeFocus);
- proto.write(SERVED_VIEW, Objects.toString(mServedView));
- proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView));
proto.end(token);
}
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index d0405f0..08a7583 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -94,6 +94,7 @@
import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowInsets;
+import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
@@ -465,6 +466,22 @@
// -----------------------------------------------------------
/**
+ * This is the view that should currently be served by an input method,
+ * regardless of the state of setting that up.
+ */
+ @Nullable
+ @GuardedBy("mH")
+ private View mServedView;
+
+ /**
+ * This is the next view that will be served by the input method, when
+ * we get around to updating things.
+ */
+ @Nullable
+ @GuardedBy("mH")
+ private View mNextServedView;
+
+ /**
* This is the root view of the overall window that currently has input
* method focus.
*/
@@ -699,8 +716,7 @@
if (mCurRootView == null) {
return null;
}
- final View servedView = mCurRootView.getImeFocusController().getServedViewLocked();
- return servedView != null ? servedView.getContext() : null;
+ return mServedView != null ? mServedView.getContext() : null;
}
}
@@ -735,11 +751,7 @@
startInputFlags, softInputMode, windowFlags);
}
- /**
- * Used by {@link ImeFocusController} to finish input connection.
- */
- @Override
- public void finishInput() {
+ private void finishInput() {
ImeTracing.getInstance().triggerClientDump(
"InputMethodManager.DelegateImpl#finishInput", InputMethodManager.this,
null /* icProto */);
@@ -768,19 +780,28 @@
}
}
- /**
- * Used by {@link ImeFocusController} to hide current input method editor.
- */
@Override
- public void closeCurrentIme() {
- closeCurrentInput();
+ public void onPostWindowFocus(View viewForWindowFocus,
+ @NonNull WindowManager.LayoutParams windowAttribute) {
+ boolean forceFocus = false;
+ synchronized (mH) {
+ // Update mNextServedView when focusedView changed.
+ onViewFocusChanged(viewForWindowFocus, true);
+
+ // Starting new input when the next focused view is same as served view but the
+ // currently active connection (if any) is not associated with it.
+ final boolean nextFocusIsServedView = mServedView == viewForWindowFocus;
+
+ if (nextFocusIsServedView
+ && !hasActiveInputConnectionInternal(viewForWindowFocus)) {
+ forceFocus = true;
+ }
+ }
+ startInputOnWindowFocusGain(viewForWindowFocus,
+ windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
}
- /**
- * For {@link ImeFocusController} to start input when gaining the window focus.
- */
- @Override
- public void startInputOnWindowFocusGain(View focusedView,
+ private void startInputOnWindowFocusGain(View focusedView,
@SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
int startInputFlags = getStartInputFlags(focusedView, 0);
startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS;
@@ -816,8 +837,7 @@
synchronized (mH) {
// For some reason we didn't do a startInput + windowFocusGain, so
// we'll just do a window focus gain and call it a day.
- View servedView = controller.getServedViewLocked();
- boolean nextFocusHasConnection = servedView != null && servedView == focusedView
+ boolean nextFocusHasConnection = mServedView != null && mServedView == focusedView
&& hasActiveInputConnectionInternal(focusedView);
if (DEBUG) {
Log.v(TAG, "Reporting focus gain, without startInput"
@@ -839,16 +859,102 @@
}
}
- /**
- * Used by {@link ImeFocusController} to finish current composing text.
- */
@Override
- public void finishComposingText() {
+ public void onViewFocusChanged(@Nullable View view, boolean hasFocus) {
+ if (view == null || view.isTemporarilyDetached()) {
+ return;
+ }
+ final ViewRootImpl viewRootImpl = view.getViewRootImpl();
synchronized (mH) {
+ if (mCurRootView != viewRootImpl) {
+ return;
+ }
+ if (!view.hasImeFocus() || !view.hasWindowFocus()) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onViewFocusChanged, view=" + InputMethodDebug.dumpViewInfo(view));
+ }
+
+ // We don't need to track the next served view when the view lost focus here
+ // because:
+ // 1) The current view focus may be cleared temporary when in touch mode, closing
+ // input at this moment isn't the right way.
+ // 2) We only care about the served view change when it focused, since changing
+ // input connection when the focus target changed is reasonable.
+ // 3) Setting the next served view as null when no more served view should be
+ // handled in other special events (e.g. view detached from window or the window
+ // dismissed).
+ if (hasFocus) {
+ mNextServedView = view;
+ }
+ }
+ viewRootImpl.dispatchCheckFocus();
+ }
+
+ @Override
+ public boolean checkFocus(boolean forceNewFocus, boolean startInput,
+ ViewRootImpl viewRootImpl) {
+ synchronized (mH) {
+ if (mCurRootView != viewRootImpl) {
+ return false;
+ }
+ if (mServedView == mNextServedView && !forceNewFocus) {
+ return false;
+ }
+ if (DEBUG) {
+ Log.v(TAG, "checkFocus: view=" + mServedView
+ + " next=" + mNextServedView
+ + " force=" + forceNewFocus
+ + " package="
+ + (mServedView != null ? mServedView.getContext().getPackageName()
+ : "<none>"));
+ }
+ // Close the connection when no next served view coming.
+ if (mNextServedView == null) {
+ finishInput();
+ closeCurrentInput();
+ return false;
+ }
+ mServedView = mNextServedView;
if (mServedInputConnection != null) {
mServedInputConnection.finishComposingTextFromImm();
}
}
+
+ if (startInput) {
+ startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */,
+ 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
+ }
+ return true;
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl) {
+ synchronized (mH) {
+ if (mCurRootView != view.getViewRootImpl()) {
+ return;
+ }
+ if (mNextServedView == view) {
+ mNextServedView = null;
+ }
+ if (mServedView == view) {
+ viewRootImpl.dispatchCheckFocus();
+ }
+ }
+ }
+
+ @Override
+ public void onWindowDismissed(ViewRootImpl viewRootImpl) {
+ synchronized (mH) {
+ if (mCurRootView != viewRootImpl) {
+ return;
+ }
+ if (mServedView != null) {
+ finishInput();
+ }
+ setCurrentRootView(null);
+ }
}
/**
@@ -873,26 +979,6 @@
return mCurRootView == rootView;
}
}
-
- /**
- * Checks whether the active input connection (if any) is for the given view.
- *
- * @see #hasActiveInputConnectionInternal(View)}
- */
- @Override
- public boolean hasActiveConnection(View view) {
- return hasActiveInputConnectionInternal(view);
- }
-
- /**
- * Returns the {@link InputMethodManager#mH} lock object.
- * Used for {@link ImeFocusController} to guard the served view being accessed by
- * {@link InputMethodManager} in different threads.
- */
- @Override
- public Object getLockObject() {
- return mH;
- }
}
/** @hide */
@@ -946,14 +1032,12 @@
@GuardedBy("mH")
private View getServedViewLocked() {
- return mCurRootView != null ? mCurRootView.getImeFocusController().getServedViewLocked()
- : null;
+ return mCurRootView != null ? mServedView : null;
}
@GuardedBy("mH")
private View getNextServedViewLocked() {
- return mCurRootView != null ? mCurRootView.getImeFocusController().getNextServedViewLocked()
- : null;
+ return mCurRootView != null ? mNextServedView : null;
}
private ImeFocusController getFocusController() {
@@ -1802,8 +1886,12 @@
@GuardedBy("mH")
void finishInputLocked() {
mVirtualDisplayToScreenMatrix = null;
- final ImeFocusController controller = getFocusController();
- final View clearedView = controller != null ? controller.clearServedViewsLocked() : null;
+ View clearedView = null;
+ mNextServedView = null;
+ if (mServedView != null) {
+ clearedView = mServedView;
+ mServedView = null;
+ }
if (clearedView != null) {
if (DEBUG) {
Log.v(TAG, "FINISH INPUT: mServedView="
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index aa3aefd..ce5365a 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9436,17 +9436,23 @@
}
// The operation should be applied to all characters touched by the line joining the points.
- int startOffset = mLayout.getOffsetForHorizontal(line, startPoint.x);
- int endOffset = mLayout.getOffsetForHorizontal(line, endPoint.x);
- if (startOffset == endOffset) {
+ float lineVerticalCenter = (mLayout.getLineTop(line)
+ + mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) / 2f;
+ // Create a rectangle which is +/-0.1f around the line's vertical center, so that the
+ // rectangle doesn't touch the line above or below. (The line height is at least 1f.)
+ RectF area = new RectF(
+ Math.min(startPoint.x, endPoint.x),
+ lineVerticalCenter + 0.1f,
+ Math.max(startPoint.x, endPoint.x),
+ lineVerticalCenter - 0.1f);
+ Range<Integer> range = mLayout.getRangeForRect(
+ area, new GraphemeClusterSegmentFinder(mText, mTextPaint),
+ Layout.INCLUSION_STRATEGY_ANY_OVERLAP);
+ if (range == null) {
return handleGestureFailure(gesture);
- } else if (startOffset > endOffset) {
- int tmp = startOffset;
- startOffset = endOffset;
- endOffset = tmp;
}
- // TODO(b/247557062): The boundary offsets might be off by one. We should check which side
- // of the offset the point is on, and adjust if necessary.
+ int startOffset = range.getLower();
+ int endOffset = range.getUpper();
// TODO(b/247557062): This doesn't handle bidirectional text correctly.
Pattern whitespacePattern = getWhitespacePattern();
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 1bc8e6d..8815ab3 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -135,8 +135,11 @@
/** This change happened underneath something else. */
public static final int FLAG_IS_OCCLUDED = 1 << 15;
+ /** The container is a system window, excluding wallpaper and input-method. */
+ public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16;
+
/** The first unused bit. This can be used by remotes to attach custom flags to this change. */
- public static final int FLAG_FIRST_CUSTOM = 1 << 16;
+ public static final int FLAG_FIRST_CUSTOM = 1 << 17;
/** @hide */
@IntDef(prefix = { "FLAG_" }, value = {
@@ -157,6 +160,7 @@
FLAG_CROSS_PROFILE_WORK_THUMBNAIL,
FLAG_IS_BEHIND_STARTING_WINDOW,
FLAG_IS_OCCLUDED,
+ FLAG_IS_SYSTEM_WINDOW,
FLAG_FIRST_CUSTOM
})
public @interface ChangeFlags {}
@@ -369,6 +373,9 @@
if ((flags & FLAG_IS_OCCLUDED) != 0) {
sb.append(sb.length() == 0 ? "" : "|").append("IS_OCCLUDED");
}
+ if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) {
+ sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW");
+ }
if ((flags & FLAG_FIRST_CUSTOM) != 0) {
sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
}
@@ -701,14 +708,37 @@
@Override
public String toString() {
- String out = "{" + mContainer + "(" + mParent + ") leash=" + mLeash
- + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb="
- + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r="
- + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation
- + " endFixedRotation=" + mEndFixedRotation;
- if (mSnapshot != null) out += " snapshot=" + mSnapshot;
- if (mLastParent != null) out += " lastParent=" + mLastParent;
- return out + "}";
+ final StringBuilder sb = new StringBuilder();
+ sb.append('{'); sb.append(mContainer);
+ sb.append(" m="); sb.append(modeToString(mMode));
+ sb.append(" f="); sb.append(flagsToString(mFlags));
+ if (mParent != null) {
+ sb.append(" p="); sb.append(mParent);
+ }
+ if (mLeash != null) {
+ sb.append(" leash="); sb.append(mLeash);
+ }
+ sb.append(" sb="); sb.append(mStartAbsBounds);
+ sb.append(" eb="); sb.append(mEndAbsBounds);
+ if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) {
+ sb.append(" eo="); sb.append(mEndRelOffset);
+ }
+ if (mStartRotation != mEndRotation) {
+ sb.append(" r="); sb.append(mStartRotation);
+ sb.append("->"); sb.append(mEndRotation);
+ sb.append(':'); sb.append(mRotationAnimation);
+ }
+ if (mEndFixedRotation != ROTATION_UNDEFINED) {
+ sb.append(" endFixedRotation="); sb.append(mEndFixedRotation);
+ }
+ if (mSnapshot != null) {
+ sb.append(" snapshot="); sb.append(mSnapshot);
+ }
+ if (mLastParent != null) {
+ sb.append(" lastParent="); sb.append(mLastParent);
+ }
+ sb.append('}');
+ return sb.toString();
}
}
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 4f74ca7..2ae2c09 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -43,6 +43,7 @@
import android.widget.TextView;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.ChooserTargetInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
@@ -86,6 +87,7 @@
private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0;
+ private final Map<SelectableTargetInfo, LoadDirectShareIconTask> mIconLoaders = new HashMap<>();
private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
@@ -239,7 +241,6 @@
mListViewDataChanged = false;
}
-
private void createPlaceHolders() {
mNumShortcutResults = 0;
mServiceTargets.clear();
@@ -268,12 +269,16 @@
holder.bindIcon(info);
if (info instanceof SelectableTargetInfo) {
// direct share targets should append the application name for a better readout
- DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+ SelectableTargetInfo sti = (SelectableTargetInfo) info;
+ DisplayResolveInfo rInfo = sti.getDisplayResolveInfo();
CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
CharSequence extendedInfo = info.getExtendedInfo();
String contentDescription = String.join(" ", info.getDisplayLabel(),
extendedInfo != null ? extendedInfo : "", appName);
holder.updateContentDescription(contentDescription);
+ if (!sti.hasDisplayIcon()) {
+ loadDirectShareIcon(sti);
+ }
} else if (info instanceof DisplayResolveInfo) {
DisplayResolveInfo dri = (DisplayResolveInfo) info;
if (!dri.hasDisplayIcon()) {
@@ -318,6 +323,20 @@
}
}
+ private void loadDirectShareIcon(SelectableTargetInfo info) {
+ LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
+ if (task == null) {
+ task = createLoadDirectShareIconTask(info);
+ mIconLoaders.put(info, task);
+ task.loadIcon();
+ }
+ }
+
+ @VisibleForTesting
+ protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) {
+ return new LoadDirectShareIconTask(info);
+ }
+
void updateAlphabeticalList() {
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override
@@ -332,7 +351,7 @@
Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
for (DisplayResolveInfo info : allTargets) {
String resolvedTarget = info.getResolvedComponentName().getPackageName()
- + '#' + info.getDisplayLabel();
+ + '#' + info.getDisplayLabel();
DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
if (multiDri == null) {
consolidated.put(resolvedTarget, info);
@@ -341,7 +360,7 @@
} else {
// create consolidated target from the single DisplayResolveInfo
MultiDisplayResolveInfo multiDisplayResolveInfo =
- new MultiDisplayResolveInfo(resolvedTarget, multiDri);
+ new MultiDisplayResolveInfo(resolvedTarget, multiDri);
multiDisplayResolveInfo.addTarget(info);
consolidated.put(resolvedTarget, multiDisplayResolveInfo);
}
@@ -731,7 +750,8 @@
* Necessary methods to communicate between {@link ChooserListAdapter}
* and {@link ChooserActivity}.
*/
- interface ChooserListCommunicator extends ResolverListCommunicator {
+ @VisibleForTesting
+ public interface ChooserListCommunicator extends ResolverListCommunicator {
int getMaxRankedTargets();
@@ -739,4 +759,35 @@
boolean isSendAction(Intent targetIntent);
}
+
+ /**
+ * Loads direct share targets icons.
+ */
+ @VisibleForTesting
+ public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Boolean> {
+ private final SelectableTargetInfo mTargetInfo;
+
+ private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
+ mTargetInfo = targetInfo;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ return mTargetInfo.loadIcon();
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isLoaded) {
+ if (isLoaded) {
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * An alias for execute to use with unit tests.
+ */
+ public void loadIcon() {
+ execute();
+ }
+ }
}
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index f6075b0..4a1f7eb 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -870,7 +870,12 @@
void onHandlePackagesChanged(ResolverListAdapter listAdapter);
}
- static class ViewHolder {
+ /**
+ * A view holder keeps a reference to a list view and provides functionality for managing its
+ * state.
+ */
+ @VisibleForTesting
+ public static class ViewHolder {
public View itemView;
public Drawable defaultItemViewBackground;
@@ -878,7 +883,8 @@
public TextView text2;
public ImageView icon;
- ViewHolder(View view) {
+ @VisibleForTesting
+ public ViewHolder(View view) {
itemView = view;
defaultItemViewBackground = view.getBackground();
text = (TextView) view.findViewById(com.android.internal.R.id.text1);
diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
index 4b9b7cb..d7f3a76 100644
--- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
+++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
@@ -37,6 +37,7 @@
import android.text.SpannableStringBuilder;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.ChooserActivity;
import com.android.internal.app.ResolverActivity;
import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
@@ -59,8 +60,11 @@
private final String mDisplayLabel;
private final PackageManager mPm;
private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
+ @GuardedBy("this")
+ private ShortcutInfo mShortcutInfo;
private Drawable mBadgeIcon = null;
private CharSequence mBadgeContentDescription;
+ @GuardedBy("this")
private Drawable mDisplayIcon;
private final Intent mFillInIntent;
private final int mFillInFlags;
@@ -78,6 +82,7 @@
mModifiedScore = modifiedScore;
mPm = mContext.getPackageManager();
mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
+ mShortcutInfo = shortcutInfo;
mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
if (sourceInfo != null) {
final ResolveInfo ri = sourceInfo.getResolveInfo();
@@ -92,8 +97,6 @@
}
}
}
- // TODO(b/121287224): do this in the background thread, and only for selected targets
- mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo);
if (sourceInfo != null) {
mBackupResolveInfo = null;
@@ -118,7 +121,10 @@
mChooserTarget = other.mChooserTarget;
mBadgeIcon = other.mBadgeIcon;
mBadgeContentDescription = other.mBadgeContentDescription;
- mDisplayIcon = other.mDisplayIcon;
+ synchronized (other) {
+ mShortcutInfo = other.mShortcutInfo;
+ mDisplayIcon = other.mDisplayIcon;
+ }
mFillInIntent = fillInIntent;
mFillInFlags = flags;
mModifiedScore = other.mModifiedScore;
@@ -141,6 +147,27 @@
return mSourceInfo;
}
+ /**
+ * Load display icon, if needed.
+ */
+ public boolean loadIcon() {
+ ShortcutInfo shortcutInfo;
+ Drawable icon;
+ synchronized (this) {
+ shortcutInfo = mShortcutInfo;
+ icon = mDisplayIcon;
+ }
+ boolean shouldLoadIcon = icon == null && shortcutInfo != null;
+ if (shouldLoadIcon) {
+ icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
+ synchronized (this) {
+ mDisplayIcon = icon;
+ mShortcutInfo = null;
+ }
+ }
+ return shouldLoadIcon;
+ }
+
private Drawable getChooserTargetIconDrawable(ChooserTarget target,
@Nullable ShortcutInfo shortcutInfo) {
Drawable directShareIcon = null;
@@ -271,10 +298,17 @@
}
@Override
- public Drawable getDisplayIcon(Context context) {
+ public synchronized Drawable getDisplayIcon(Context context) {
return mDisplayIcon;
}
+ /**
+ * @return true if display icon is available
+ */
+ public synchronized boolean hasDisplayIcon() {
+ return mDisplayIcon != null;
+ }
+
public ChooserTarget getChooserTarget() {
return mChooserTarget;
}
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 322354b..285258a 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -88,7 +88,6 @@
optional SettingProto odi_captions_volume_ui_enabled = 42 [ (android.privacy).dest = DEST_AUTOMATIC ];
// Setting for accessibility magnification for following typing.
optional SettingProto accessibility_magnification_follow_typing_enabled = 43 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto accessibility_software_cursor_enabled = 44 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Accessibility accessibility = 2;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f0b1b2a..dbfefd0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1040,25 +1040,38 @@
android:priority="900" />
<!-- Allows an application to read from external storage.
- <p>Any app that declares the {@link #WRITE_EXTERNAL_STORAGE} permission is implicitly
- granted this permission.</p>
+ <p class="note"><strong>Note: </strong>Starting in API level 33, this permission has no
+ effect. If your app accesses other apps' media files, request one or more of these permissions
+ instead: <a href="#READ_MEDIA_IMAGES"><code>READ_MEDIA_IMAGES</code></a>,
+ <a href="#READ_MEDIA_VIDEO"><code>READ_MEDIA_VIDEO</code></a>,
+ <a href="#READ_MEDIA_AUDIO"><code>READ_MEDIA_AUDIO</code></a>. Learn more about the
+ <a href="{@docRoot}training/data-storage/shared/media#storage-permission">storage
+ permissions</a> that are associated with media files.</p>
+
<p>This permission is enforced starting in API level 19. Before API level 19, this
permission is not enforced and all apps still have access to read from external storage.
You can test your app with the permission enforced by enabling <em>Protect USB
- storage</em> under Developer options in the Settings app on a device running Android 4.1 or
- higher.</p>
+ storage</em> under <b>Developer options</b> in the Settings app on a device running Android
+ 4.1 or higher.</p>
<p>Also starting in API level 19, this permission is <em>not</em> required to
- read/write files in your application-specific directories returned by
+ read or write files in your application-specific directories returned by
{@link android.content.Context#getExternalFilesDir} and
- {@link android.content.Context#getExternalCacheDir}.
- <p class="note"><strong>Note:</strong> If <em>both</em> your <a
+ {@link android.content.Context#getExternalCacheDir}.</p>
+ <p>Starting in API level 29, apps don't need to request this permission to access files in
+ their app-specific directory on external storage, or their own files in the
+ <a href="{@docRoot}reference/android/provider/MediaStore"><code>MediaStore</code></a>. Apps
+ shouldn't request this permission unless they need to access other apps' files in the
+ <code>MediaStore</code>. Read more about these changes in the
+ <a href="{@docRoot}training/data-storage#scoped-storage">scoped storage</a> section of the
+ developer documentation.</p>
+ <p>If <em>both</em> your <a
href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code
minSdkVersion}</a> and <a
href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
targetSdkVersion}</a> values are set to 3 or lower, the system implicitly
grants your app this permission. If you don't need this permission, be sure your <a
href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
- targetSdkVersion}</a> is 4 or higher.
+ targetSdkVersion}</a> is 4 or higher.</p>
<p> This is a soft restricted permission which cannot be held by an app it its
full form until the installer on record allowlists the permission.
diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml
index ded23fe..38a71f0 100644
--- a/core/res/res/layout/miniresolver.xml
+++ b/core/res/res/layout/miniresolver.xml
@@ -65,8 +65,7 @@
android:paddingTop="32dp"
android:paddingBottom="@dimen/resolver_button_bar_spacing"
android:orientation="vertical"
- android:background="?attr/colorBackground"
- android:layout_ignoreOffset="true">
+ android:background="?attr/colorBackground">
<RelativeLayout
style="?attr/buttonBarStyle"
android:layout_width="match_parent"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3834478..90d84dd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -658,6 +658,20 @@
-->
</integer-array>
+ <!-- The device states (supplied by DeviceStateManager) that should be treated as half-folded by
+ the display fold controller. Default is empty. -->
+ <integer-array name="config_halfFoldedDeviceStates">
+ <!-- Example:
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ -->
+ </integer-array>
+
+ <!-- Indicates whether the window manager reacts to half-fold device states by overriding
+ rotation. -->
+ <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool>
+
<!-- When a device enters any of these states, it should be woken up. States are defined in
device_state_configuration.xml. -->
<integer-array name="config_deviceStatesOnWhichToWakeUp">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 530891f..153e499 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4016,6 +4016,8 @@
<!-- For Foldables -->
<java-symbol type="array" name="config_foldedDeviceStates" />
+ <java-symbol type="array" name="config_halfFoldedDeviceStates" />
+ <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" />
<java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" />
<java-symbol type="array" name="config_deviceStatesOnWhichToSleep" />
<java-symbol type="string" name="config_foldedArea" />
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
new file mode 100644
index 0000000..57b9cb1
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Test;
+
+public final class ProgramSelectorTest {
+
+ private static final int FM_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_FM;
+ private static final int DAB_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_DAB;
+ private static final long FM_FREQUENCY = 88500;
+ private static final long AM_FREQUENCY = 700;
+ private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
+ private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_1 =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+ /* value= */ 0x1000011);
+ private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_2 =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+ /* value= */ 0x10000112);
+ private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+ /* value= */ 0x1001);
+ private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+ /* value= */ 94500);
+
+ @Test
+ public void getType_forIdentifier() {
+ assertWithMessage("Identifier type").that(FM_IDENTIFIER.getType())
+ .isEqualTo(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+ }
+
+ @Test
+ public void isCategoryType_withCategoryTypeForIdentifier() {
+ int typeChecked = ProgramSelector.IDENTIFIER_TYPE_VENDOR_START + 1;
+ ProgramSelector.Identifier fmIdentifier = new ProgramSelector.Identifier(
+ typeChecked, /* value= */ 99901);
+
+ assertWithMessage("Whether %s is a category identifier type", typeChecked)
+ .that(fmIdentifier.isCategoryType()).isTrue();
+ }
+
+ @Test
+ public void isCategoryType_withNonCategoryTypeForIdentifier() {
+ assertWithMessage("Is AMFM_FREQUENCY category identifier type")
+ .that(FM_IDENTIFIER.isCategoryType()).isFalse();
+ }
+
+ @Test
+ public void getValue_forIdentifier() {
+ assertWithMessage("Identifier value")
+ .that(FM_IDENTIFIER.getValue()).isEqualTo(FM_FREQUENCY);
+ }
+
+ @Test
+ public void equals_withDifferentTypesForIdentifiers_returnsFalse() {
+ assertWithMessage("Identifier with different identifier type")
+ .that(FM_IDENTIFIER).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1);
+ }
+
+ @Test
+ public void equals_withDifferentValuesForIdentifiers_returnsFalse() {
+ assertWithMessage("Identifier with different identifier value")
+ .that(DAB_SID_EXT_IDENTIFIER_2).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1);
+ }
+
+ @Test
+ public void equals_withSameKeyAndValueForIdentifiers_returnsTrue() {
+ ProgramSelector.Identifier fmIdentifierSame = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
+
+ assertWithMessage("Identifier of the same identifier")
+ .that(FM_IDENTIFIER).isEqualTo(fmIdentifierSame);
+ }
+
+ @Test
+ public void getProgramType() {
+ ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+ int programType = selector.getProgramType();
+
+ assertWithMessage("Program type").that(programType).isEqualTo(FM_PROGRAM_TYPE);
+ }
+
+ @Test
+ public void getPrimaryId() {
+ ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+ ProgramSelector.Identifier programId = selector.getPrimaryId();
+
+ assertWithMessage("Program Id").that(programId).isEqualTo(FM_IDENTIFIER);
+ }
+
+ @Test
+ public void getSecondaryIds_withEmptySecondaryIds() {
+ ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+ ProgramSelector.Identifier[] secondaryIds = selector.getSecondaryIds();
+
+ assertWithMessage("Secondary ids of selector initialized with empty secondary ids")
+ .that(secondaryIds).isEmpty();
+ }
+
+ @Test
+ public void getSecondaryIds_withNonEmptySecondaryIds() {
+ ProgramSelector.Identifier[] secondaryIdsExpected = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector selector = getDabSelector(secondaryIdsExpected, /* vendorIds= */ null);
+
+ ProgramSelector.Identifier[] secondaryIds = selector.getSecondaryIds();
+
+ assertWithMessage("Secondary identifier got")
+ .that(secondaryIds).isEqualTo(secondaryIdsExpected);
+ }
+
+ @Test
+ public void getFirstId_withIdInSelector() {
+ ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+ long firstIdValue = selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
+
+ assertWithMessage("Value of the first DAB_SID_EXT identifier")
+ .that(firstIdValue).isEqualTo(DAB_SID_EXT_IDENTIFIER_1.getValue());
+ }
+
+ @Test
+ public void getFirstId_withIdNotInSelector() {
+ ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2};
+ ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+ int idType = ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY;
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ selector.getFirstId(idType);
+ });
+
+ assertWithMessage("Exception for getting first identifier %s", idType)
+ .that(thrown).hasMessageThat().contains("Identifier " + idType + " not found");
+ }
+
+ @Test
+ public void getAllIds_withIdInSelector() {
+ ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector.Identifier[] allIdsExpected =
+ {DAB_SID_EXT_IDENTIFIER_1, DAB_SID_EXT_IDENTIFIER_2};
+ ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+ ProgramSelector.Identifier[] allIds =
+ selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
+
+ assertWithMessage("All DAB_SID_EXT identifiers in selector")
+ .that(allIds).isEqualTo(allIdsExpected);
+ }
+
+ @Test
+ public void getAllIds_withIdNotInSelector() {
+ ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+ ProgramSelector.Identifier[] allIds =
+ selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+
+ assertWithMessage("AMFM frequency identifiers found in selector")
+ .that(allIds).isEmpty();
+ }
+
+ @Test
+ public void getVendorIds_withEmptyVendorIds() {
+ ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+ long[] vendorIds = selector.getVendorIds();
+
+ assertWithMessage("Vendor Ids of selector initialized with empty vendor ids")
+ .that(vendorIds).isEmpty();
+ }
+
+ @Test
+ public void getVendorIds_withNonEmptyVendorIds() {
+ long[] vendorIdsExpected = {12345, 678};
+ ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, vendorIdsExpected);
+
+ long[] vendorIds = selector.getVendorIds();
+
+ assertWithMessage("Vendor Ids of selector initialized with non-empty vendor ids")
+ .that(vendorIds).isEqualTo(vendorIdsExpected);
+ }
+
+ @Test
+ public void withSecondaryPreferred() {
+ ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+ long[] vendorIdsExpected = {12345, 678};
+ ProgramSelector selector = getDabSelector(secondaryIds, vendorIdsExpected);
+ ProgramSelector.Identifier[] secondaryIdsExpected = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_1};
+
+ ProgramSelector selectorPreferred =
+ selector.withSecondaryPreferred(DAB_SID_EXT_IDENTIFIER_1);
+
+ assertWithMessage("Program type")
+ .that(selectorPreferred.getProgramType()).isEqualTo(selector.getProgramType());
+ assertWithMessage("Primary identifiers")
+ .that(selectorPreferred.getPrimaryId()).isEqualTo(selector.getPrimaryId());
+ assertWithMessage("Secondary identifiers")
+ .that(selectorPreferred.getSecondaryIds()).isEqualTo(secondaryIdsExpected);
+ assertWithMessage("Vendor Ids")
+ .that(selectorPreferred.getVendorIds()).isEqualTo(vendorIdsExpected);
+ }
+
+ @Test
+ public void createAmFmSelector_withValidFrequencyWithoutSubChannel() {
+ int band = RadioManager.BAND_AM;
+ ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, AM_FREQUENCY);
+
+ ProgramSelector selector = ProgramSelector.createAmFmSelector(band, (int) AM_FREQUENCY);
+
+ assertWithMessage("Program type")
+ .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM);
+ assertWithMessage("Primary identifiers")
+ .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+ assertWithMessage("Secondary identifiers")
+ .that(selector.getSecondaryIds()).isEmpty();
+ }
+
+ @Test
+ public void createAmFmSelector_withoutBandAndSubChannel() {
+ ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
+
+ ProgramSelector selector = ProgramSelector.createAmFmSelector(
+ RadioManager.BAND_INVALID, (int) FM_FREQUENCY);
+
+ assertWithMessage("Program type")
+ .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_FM);
+ assertWithMessage("Primary identifiers")
+ .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+ assertWithMessage("Secondary identifiers")
+ .that(selector.getSecondaryIds()).isEmpty();
+ }
+
+ @Test
+ public void createAmFmSelector_withValidFrequencyAndSubChannel() {
+ int band = RadioManager.BAND_AM_HD;
+ int subChannel = 2;
+ ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, AM_FREQUENCY);
+ ProgramSelector.Identifier[] secondaryIdExpected = {
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, subChannel - 1)
+ };
+
+ ProgramSelector selector = ProgramSelector.createAmFmSelector(band, (int) AM_FREQUENCY,
+ subChannel);
+
+ assertWithMessage("Program type")
+ .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM);
+ assertWithMessage("Primary identifiers")
+ .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+ assertWithMessage("Secondary identifiers")
+ .that(selector.getSecondaryIds()).isEqualTo(secondaryIdExpected);
+ }
+
+ @Test
+ public void createAmFmSelector_withInvalidFrequency_throwsIllegalArgumentException() {
+ int invalidFrequency = 50000;
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ ProgramSelector.createAmFmSelector(RadioManager.BAND_AM, invalidFrequency);
+ });
+
+ assertWithMessage("Exception for using invalid frequency %s", invalidFrequency)
+ .that(thrown).hasMessageThat().contains(
+ "Provided value is not a valid AM/FM frequency: " + invalidFrequency);
+ }
+
+ @Test
+ public void createAmFmSelector_withInvalidSubChannel_throwsIllegalArgumentException() {
+ int invalidSubChannel = 9;
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ ProgramSelector.createAmFmSelector(RadioManager.BAND_FM, (int) FM_FREQUENCY,
+ invalidSubChannel);
+ });
+
+ assertWithMessage("Exception for using invalid subchannel %s", invalidSubChannel)
+ .that(thrown).hasMessageThat().contains(
+ "Invalid subchannel: " + invalidSubChannel);
+ }
+
+ @Test
+ public void createAmFmSelector_withSubChannelNotSupported_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ ProgramSelector.createAmFmSelector(RadioManager.BAND_FM, (int) FM_FREQUENCY,
+ /* subChannel= */ 1);
+ });
+
+ assertWithMessage("Exception for using sub-channel on radio not supporting it")
+ .that(thrown)
+ .hasMessageThat().contains("Subchannels are not supported for non-HD radio");
+ }
+
+ @Test
+ public void equals_withDifferentSecondaryIds_returnTrue() {
+ ProgramSelector.Identifier[] secondaryIds1 = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector selector1 = getDabSelector(secondaryIds1, /* vendorIds= */ null);
+ ProgramSelector selector2 = getDabSelector(
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+
+ assertWithMessage("Selector with different secondary id")
+ .that(selector1).isEqualTo(selector2);
+ }
+
+ @Test
+ public void equals_withDifferentPrimaryIds_returnFalse() {
+ ProgramSelector selector1 = getFmSelector(
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+ ProgramSelector.Identifier fmIdentifier2 = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 88700);
+ ProgramSelector selector2 = new ProgramSelector(FM_PROGRAM_TYPE, fmIdentifier2,
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+
+ assertWithMessage("Selector with different primary id")
+ .that(selector1).isNotEqualTo(selector2);
+ }
+
+ @Test
+ public void strictEquals_withDifferentPrimaryIds_returnsFalse() {
+ ProgramSelector selector1 = getFmSelector(
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+ ProgramSelector.Identifier fmIdentifier2 = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 88700);
+ ProgramSelector selector2 = new ProgramSelector(FM_PROGRAM_TYPE, fmIdentifier2,
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+
+ assertWithMessage(
+ "Whether two selectors with different primary ids are strictly equal")
+ .that(selector1.strictEquals(selector2)).isFalse();
+ }
+
+ @Test
+ public void strictEquals_withDifferentSecondaryIds_returnsFalse() {
+ ProgramSelector.Identifier[] secondaryIds1 = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector selector1 = getDabSelector(secondaryIds1, /* vendorIds= */ null);
+ ProgramSelector.Identifier[] secondaryIds2 = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER};
+ ProgramSelector selector2 = getDabSelector(secondaryIds2, /* vendorIds= */ null);
+
+ assertWithMessage(
+ "Whether two selectors with different secondary ids are strictly equal")
+ .that(selector1.strictEquals(selector2)).isFalse();
+ }
+
+ @Test
+ public void strictEquals_withDifferentSecondaryIdsOrders_returnsTrue() {
+ ProgramSelector.Identifier[] secondaryIds1 = new ProgramSelector.Identifier[]{
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+ ProgramSelector selector1 = getDabSelector(secondaryIds1, /* vendorIds= */ null);
+ ProgramSelector.Identifier[] secondaryIds2 = new ProgramSelector.Identifier[]{
+ DAB_FREQUENCY_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER};
+ ProgramSelector selector2 = getDabSelector(secondaryIds2, /* vendorIds= */ null);
+
+ assertWithMessage(
+ "Whether two selectors with different secondary id orders are strictly equal")
+ .that(selector1.strictEquals(selector2)).isTrue();
+ }
+
+ private ProgramSelector getFmSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
+ @Nullable long[] vendorIds) {
+ return new ProgramSelector(FM_PROGRAM_TYPE, FM_IDENTIFIER, secondaryIds, vendorIds);
+ }
+
+ private ProgramSelector getDabSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
+ @Nullable long[] vendorIds) {
+ return new ProgramSelector(DAB_PROGRAM_TYPE, DAB_SID_EXT_IDENTIFIER_1, secondaryIds,
+ vendorIds);
+ }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
new file mode 100644
index 0000000..7f71921
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 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.broadcastradio.aidl;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.radio.RadioManager;
+import android.os.RemoteException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * Tests for AIDL HAL RadioModule.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class RadioModuleTest {
+
+ // Mocks
+ @Mock
+ private IBroadcastRadio mBroadcastRadioMock;
+
+ private final Object mLock = new Object();
+ // RadioModule under test
+ private RadioModule mRadioModule;
+
+ @Before
+ public void setup() throws RemoteException {
+ mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(
+ /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
+ /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
+ /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
+ /* isCaptureSupported= */ false, /* bands= */ null, /* isBgScanSupported= */ false,
+ /* supportedProgramTypes= */ new int[]{},
+ /* supportedIdentifierTypes */ new int[]{},
+ /* dabFrequencyTable= */ null, /* vendorInfo= */ null), mLock);
+
+ // TODO(b/241118988): test non-null image for getImage method
+ when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(null);
+ }
+
+ @Test
+ public void getService() {
+ assertWithMessage("Service of radio module")
+ .that(mRadioModule.getService()).isEqualTo(mBroadcastRadioMock);
+ }
+
+ @Test
+ public void setInternalHalCallback_callbackSetInHal() throws RemoteException {
+ mRadioModule.setInternalHalCallback();
+
+ verify(mBroadcastRadioMock).setTunerCallback(any());
+ }
+
+ @Test
+ public void getImage_withValidIdFromRadioModule() {
+ int imageId = 1;
+
+ Bitmap imageTest = mRadioModule.getImage(imageId);
+
+ assertWithMessage("Image got from radio module").that(imageTest).isNull();
+ }
+
+ @Test
+ public void getImage_withInvalidIdFromRadioModule_throwsIllegalArgumentException() {
+ int invalidImageId = IBroadcastRadio.INVALID_IMAGE;
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ mRadioModule.getImage(invalidImageId);
+ });
+
+ assertWithMessage("Exception for getting image with invalid ID")
+ .that(thrown).hasMessageThat().contains("Image ID is missing");
+ }
+}
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index ed2b101..3768063 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -368,4 +368,20 @@
PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
assertEquals(n1, "cache_key.bluetooth.get_state");
}
+
+ @Test
+ public void testOnTrimMemory() {
+ TestCache cache = new TestCache(MODULE, "trimMemoryTest");
+ // The cache is not active until it has been invalidated once.
+ cache.invalidateCache();
+ // Populate the cache with six entries.
+ for (int i = 0; i < 6; i++) {
+ cache.query(i);
+ }
+ // The maximum number of entries in TestCache is 4, so even though six entries were
+ // created, only four are retained.
+ assertEquals(4, cache.size());
+ PropertyInvalidatedCache.onTrimMemory();
+ assertEquals(0, cache.size());
+ }
}
diff --git a/core/tests/coretests/src/android/os/VibratorTest.java b/core/tests/coretests/src/android/os/VibratorTest.java
index 7a66bef..7ebebc9 100644
--- a/core/tests/coretests/src/android/os/VibratorTest.java
+++ b/core/tests/coretests/src/android/os/VibratorTest.java
@@ -21,12 +21,17 @@
import static junit.framework.TestCase.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
@@ -34,6 +39,7 @@
import android.content.ContextWrapper;
import android.hardware.vibrator.IVibrator;
import android.media.AudioAttributes;
+import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
@@ -46,6 +52,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.junit.MockitoJUnitRunner;
/**
@@ -65,6 +72,7 @@
private Context mContextSpy;
private Vibrator mVibratorSpy;
+ private TestLooper mTestLooper;
@Before
public void setUp() {
@@ -73,6 +81,7 @@
ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
mVibratorSpy = spy(new SystemVibrator(mContextSpy));
+ mTestLooper = new TestLooper();
}
@Test
@@ -395,6 +404,108 @@
}
@Test
+ public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() {
+ VibratorManager mockVibratorManager = mock(VibratorManager.class);
+ when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]);
+
+ Vibrator.OnVibratorStateChangedListener mockListener =
+ mock(Vibrator.OnVibratorStateChangedListener.class);
+ SystemVibrator.MultiVibratorStateListener multiVibratorListener =
+ new SystemVibrator.MultiVibratorStateListener(
+ mTestLooper.getNewExecutor(), mockListener);
+
+ multiVibratorListener.register(mockVibratorManager);
+
+ // Never tries to register a listener to an individual vibrator.
+ assertFalse(multiVibratorListener.hasRegisteredListeners());
+ verify(mockVibratorManager, never()).getVibrator(anyInt());
+ }
+
+ @Test
+ public void onVibratorStateChanged_singleVibrator_forwardsAllCallbacks() {
+ VibratorManager mockVibratorManager = mock(VibratorManager.class);
+ when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1 });
+ when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
+
+ Vibrator.OnVibratorStateChangedListener mockListener =
+ mock(Vibrator.OnVibratorStateChangedListener.class);
+ SystemVibrator.MultiVibratorStateListener multiVibratorListener =
+ new SystemVibrator.MultiVibratorStateListener(
+ mTestLooper.getNewExecutor(), mockListener);
+
+ multiVibratorListener.register(mockVibratorManager);
+ assertTrue(multiVibratorListener.hasRegisteredListeners());
+
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ true);
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
+
+ mTestLooper.dispatchAll();
+
+ InOrder inOrder = inOrder(mockListener);
+ inOrder.verify(mockListener).onVibratorStateChanged(eq(false));
+ inOrder.verify(mockListener).onVibratorStateChanged(eq(true));
+ inOrder.verify(mockListener).onVibratorStateChanged(eq(false));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void onVibratorStateChanged_multipleVibrators_triggersOnlyWhenAllVibratorsInitialized() {
+ VibratorManager mockVibratorManager = mock(VibratorManager.class);
+ when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 });
+ when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
+
+ Vibrator.OnVibratorStateChangedListener mockListener =
+ mock(Vibrator.OnVibratorStateChangedListener.class);
+ SystemVibrator.MultiVibratorStateListener multiVibratorListener =
+ new SystemVibrator.MultiVibratorStateListener(
+ mTestLooper.getNewExecutor(), mockListener);
+
+ multiVibratorListener.register(mockVibratorManager);
+ assertTrue(multiVibratorListener.hasRegisteredListeners());
+
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
+ mTestLooper.dispatchAll();
+ verify(mockListener, never()).onVibratorStateChanged(anyBoolean());
+
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 1, /* vibrating= */ false);
+ mTestLooper.dispatchAll();
+ verify(mockListener).onVibratorStateChanged(eq(false));
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void onVibratorStateChanged_multipleVibrators_stateChangeIsDeduped() {
+ VibratorManager mockVibratorManager = mock(VibratorManager.class);
+ when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 });
+ when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
+
+ Vibrator.OnVibratorStateChangedListener mockListener =
+ mock(Vibrator.OnVibratorStateChangedListener.class);
+ SystemVibrator.MultiVibratorStateListener multiVibratorListener =
+ new SystemVibrator.MultiVibratorStateListener(
+ mTestLooper.getNewExecutor(), mockListener);
+
+ multiVibratorListener.register(mockVibratorManager);
+ assertTrue(multiVibratorListener.hasRegisteredListeners());
+
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); // none
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 1, /* vibrating= */ false); // false
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ true); // true
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 1, /* vibrating= */ true); // true
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); // true
+ multiVibratorListener.onVibrating(/* vibratorIdx= */ 1, /* vibrating= */ false); // false
+
+ mTestLooper.dispatchAll();
+
+ InOrder inOrder = inOrder(mockListener);
+ inOrder.verify(mockListener).onVibratorStateChanged(eq(false));
+ inOrder.verify(mockListener).onVibratorStateChanged(eq(true));
+ inOrder.verify(mockListener).onVibratorStateChanged(eq(false));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
public void vibrate_withVibrationAttributes_usesGivenAttributes() {
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
VibrationAttributes attributes = new VibrationAttributes.Builder().setUsage(
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
new file mode 100644
index 0000000..8218b98
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Bundle
+import android.service.chooser.ChooserTarget
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.R
+import com.android.internal.app.ChooserListAdapter.LoadDirectShareIconTask
+import com.android.internal.app.chooser.SelectableTargetInfo
+import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator
+import com.android.internal.app.chooser.TargetInfo
+import com.android.server.testutils.any
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ChooserListAdapterTest {
+ private val packageManager = mock<PackageManager> {
+ whenever(resolveActivity(any(), anyInt())).thenReturn(mock())
+ }
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+ private val resolverListController = mock<ResolverListController>()
+ private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> {
+ whenever(maxRankedTargets).thenReturn(0)
+ }
+ private val selectableTargetInfoCommunicator =
+ mock<SelectableTargetInfoCommunicator> {
+ whenever(targetIntent).thenReturn(mock())
+ }
+ private val chooserActivityLogger = mock<ChooserActivityLogger>()
+
+ private fun createChooserListAdapter(
+ taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
+ ) =
+ ChooserListAdapterOverride(
+ context,
+ emptyList(),
+ emptyArray(),
+ emptyList(),
+ false,
+ resolverListController,
+ chooserListCommunicator,
+ selectableTargetInfoCommunicator,
+ packageManager,
+ chooserActivityLogger,
+ taskProvider
+ )
+
+ @Test
+ fun testDirectShareTargetLoadingIconIsStarted() {
+ val view = createView()
+ val viewHolder = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolder
+ val targetInfo = createSelectableTargetInfo()
+ val iconTask = mock<LoadDirectShareIconTask>()
+ val testSubject = createChooserListAdapter { iconTask }
+ testSubject.testViewBind(view, targetInfo, 0)
+
+ verify(iconTask, times(1)).loadIcon()
+ }
+
+ @Test
+ fun testOnlyOneTaskPerTarget() {
+ val view = createView()
+ val viewHolderOne = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolderOne
+ val targetInfo = createSelectableTargetInfo()
+ val iconTaskOne = mock<LoadDirectShareIconTask>()
+ val testTaskProvider = mock<() -> LoadDirectShareIconTask> {
+ whenever(invoke()).thenReturn(iconTaskOne)
+ }
+ val testSubject = createChooserListAdapter { testTaskProvider.invoke() }
+ testSubject.testViewBind(view, targetInfo, 0)
+
+ val viewHolderTwo = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolderTwo
+ whenever(testTaskProvider()).thenReturn(mock())
+
+ testSubject.testViewBind(view, targetInfo, 0)
+
+ verify(iconTaskOne, times(1)).loadIcon()
+ verify(testTaskProvider, times(1)).invoke()
+ }
+
+ private fun createSelectableTargetInfo(): SelectableTargetInfo =
+ SelectableTargetInfo(
+ context,
+ null,
+ createChooserTarget(),
+ 1f,
+ selectableTargetInfoCommunicator,
+ null
+ )
+
+ private fun createChooserTarget(): ChooserTarget =
+ ChooserTarget(
+ "Title",
+ null,
+ 1f,
+ ComponentName("package", "package.Class"),
+ Bundle()
+ )
+
+ private fun createView(): View {
+ val view = FrameLayout(context)
+ TextView(context).apply {
+ id = R.id.text1
+ view.addView(this)
+ }
+ TextView(context).apply {
+ id = R.id.text2
+ view.addView(this)
+ }
+ ImageView(context).apply {
+ id = R.id.icon
+ view.addView(this)
+ }
+ return view
+ }
+}
+
+private class ChooserListAdapterOverride(
+ context: Context?,
+ payloadIntents: List<Intent>?,
+ initialIntents: Array<out Intent>?,
+ rList: List<ResolveInfo>?,
+ filterLastUsed: Boolean,
+ resolverListController: ResolverListController?,
+ chooserListCommunicator: ChooserListCommunicator?,
+ selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?,
+ packageManager: PackageManager?,
+ chooserActivityLogger: ChooserActivityLogger?,
+ private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
+) : ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ chooserListCommunicator,
+ selectableTargetInfoCommunicator,
+ packageManager,
+ chooserActivityLogger
+) {
+ override fun createLoadDirectShareIconTask(
+ info: SelectableTargetInfo?
+ ): LoadDirectShareIconTask =
+ taskProvider.invoke(info)
+
+ fun testViewBind(view: View?, info: TargetInfo?, position: Int) {
+ onBindView(view, info, position)
+ }
+}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 8ca1607..699e794 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -236,6 +236,8 @@
<permission name="android.permission.MODIFY_DAY_NIGHT_MODE"/>
<permission name="android.permission.ACCESS_LOWPAN_STATE"/>
<permission name="android.permission.BACKUP"/>
+ <!-- Needed for GMSCore Location API test only -->
+ <permission name="android.permission.LOCATION_BYPASS"/>
<!-- Needed for test only -->
<permission name="android.permission.BATTERY_PREDICTION"/>
<permission name="android.permission.BATTERY_STATS"/>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f9f2906..ca543f4 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1105,6 +1105,12 @@
"group": "WM_DEBUG_FOCUS",
"at": "com\/android\/server\/wm\/WindowState.java"
},
+ "-1043981272": {
+ "message": "Reverting orientation. Rotating to %s from %s rather than %s.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"-1042574499": {
"message": "Attempted to add Accessibility overlay window with unknown token %s. Aborting.",
"level": "WARN",
@@ -4303,6 +4309,12 @@
"group": "WM_ERROR",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
+ "2066210760": {
+ "message": "foldStateChanged: displayId %d, halfFoldStateChanged %s, saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, mLastOrientation: %d, mRotation: %d",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"2070726247": {
"message": "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s",
"level": "DEBUG",
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index c6731d1..48dd3e6 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -1347,7 +1347,8 @@
nInitDisplayInfo(largestWidth, largestHeight, defaultDisplay.getRefreshRate(),
wideColorDataspace, defaultDisplay.getAppVsyncOffsetNanos(),
- defaultDisplay.getPresentationDeadlineNanos());
+ defaultDisplay.getPresentationDeadlineNanos(),
+ defaultDisplay.getOverlaySupport().supportFp16ForHdr());
mDisplayInitialized = true;
}
@@ -1527,7 +1528,8 @@
private static native void nSetDisplayDensityDpi(int densityDpi);
private static native void nInitDisplayInfo(int width, int height, float refreshRate,
- int wideColorDataspace, long appVsyncOffsetNanos, long presentationDeadlineNanos);
+ int wideColorDataspace, long appVsyncOffsetNanos, long presentationDeadlineNanos,
+ boolean supportsFp16ForHdr);
private static native void nSetDrawingEnabled(boolean drawingEnabled);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index d76ad3d..48c5f64 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -109,7 +109,6 @@
if (mTaskViewTransitions != null) {
mTaskViewTransitions.addTaskView(this);
}
- setUseAlpha();
getHolder().addCallback(this);
mGuard.open("release");
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 2d9c2a9..f544a7c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -938,7 +938,6 @@
addView(mAnimatingOutSurfaceContainer);
mAnimatingOutSurfaceView = new SurfaceView(getContext());
- mAnimatingOutSurfaceView.setUseAlpha();
mAnimatingOutSurfaceView.setZOrderOnTop(true);
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
mContext.getResources());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 30124a5..616d447 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -745,6 +745,15 @@
// Directly move PiP to its final destination bounds without animation.
mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds);
}
+
+ // if the pip window size is beyond allowed bounds user resize to normal bounds
+ if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x
+ || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x
+ || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y
+ || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) {
+ mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction);
+ }
+
} else {
updateDisplayLayout.run();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index 89d85e4..41ff0b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -96,6 +96,7 @@
private final Rect mDisplayBounds = new Rect();
private final Function<Rect, Rect> mMovementBoundsSupplier;
private final Runnable mUpdateMovementBoundsRunnable;
+ private final Consumer<Rect> mUpdateResizeBoundsCallback;
private int mDelta;
private float mTouchSlop;
@@ -137,6 +138,13 @@
mPhonePipMenuController = menuActivityController;
mPipUiEventLogger = pipUiEventLogger;
mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
+
+ mUpdateResizeBoundsCallback = (rect) -> {
+ mUserResizeBounds.set(rect);
+ mMotionHelper.synchronizePinnedStackBounds();
+ mUpdateMovementBoundsRunnable.run();
+ resetState();
+ };
}
public void init() {
@@ -508,15 +516,50 @@
}
}
+ private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
+ final int leftEdge = bounds.left;
+
+
+ final int fromLeft = Math.abs(leftEdge - movementBounds.left);
+ final int fromRight = Math.abs(movementBounds.right - leftEdge);
+
+ // The PIP will be snapped to either the right or left edge, so calculate which one
+ // is closest to the current position.
+ final int newLeft = fromLeft < fromRight
+ ? movementBounds.left : movementBounds.right;
+
+ bounds.offsetTo(newLeft, mLastResizeBounds.top);
+ }
+
+ /**
+ * Resizes the pip window and updates user-resized bounds.
+ *
+ * @param bounds target bounds to resize to
+ * @param snapFraction snap fraction to apply after resizing
+ */
+ void userResizeTo(Rect bounds, float snapFraction) {
+ Rect finalBounds = new Rect(bounds);
+
+ // get the current movement bounds
+ final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds);
+
+ // snap the target bounds to the either left or right edge, by choosing the closer one
+ snapToMovementBoundsEdge(finalBounds, movementBounds);
+
+ // apply the requested snap fraction onto the target bounds
+ mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction);
+
+ // resize from current bounds to target bounds without animation
+ mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null);
+ // set the flag that pip has been resized
+ mPipBoundsState.setHasUserResizedPip(true);
+
+ // finish the resize operation and update the state of the bounds
+ mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback);
+ }
+
private void finishResize() {
if (!mLastResizeBounds.isEmpty()) {
- final Consumer<Rect> callback = (rect) -> {
- mUserResizeBounds.set(mLastResizeBounds);
- mMotionHelper.synchronizePinnedStackBounds();
- mUpdateMovementBoundsRunnable.run();
- resetState();
- };
-
// Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped
// position correctly. Drag-resize does not need to move, so just finalize resize.
if (mOngoingPinchToResize) {
@@ -526,24 +569,23 @@
|| mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) {
resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y);
}
- final int leftEdge = mLastResizeBounds.left;
- final Rect movementBounds =
- mPipBoundsAlgorithm.getMovementBounds(mLastResizeBounds);
- final int fromLeft = Math.abs(leftEdge - movementBounds.left);
- final int fromRight = Math.abs(movementBounds.right - leftEdge);
- // The PIP will be snapped to either the right or left edge, so calculate which one
- // is closest to the current position.
- final int newLeft = fromLeft < fromRight
- ? movementBounds.left : movementBounds.right;
- mLastResizeBounds.offsetTo(newLeft, mLastResizeBounds.top);
+
+ // get the current movement bounds
+ final Rect movementBounds = mPipBoundsAlgorithm
+ .getMovementBounds(mLastResizeBounds);
+
+ // snap mLastResizeBounds to the correct edge based on movement bounds
+ snapToMovementBoundsEdge(mLastResizeBounds, movementBounds);
+
final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
mLastResizeBounds, movementBounds);
mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds,
- PINCH_RESIZE_SNAP_DURATION, mAngle, callback);
+ PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback);
} else {
mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
- PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback);
+ PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE,
+ mUpdateResizeBoundsCallback);
}
final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f;
mPipDismissTargetHandler
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 1f3f31e..975d4bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -825,6 +825,16 @@
}
/**
+ * Resizes the pip window and updates user resized bounds
+ *
+ * @param bounds target bounds to resize to
+ * @param snapFraction snap fraction to apply after resizing
+ */
+ void userResizeTo(Rect bounds, float snapFraction) {
+ mPipResizeGestureHandler.userResizeTo(bounds, snapFraction);
+ }
+
+ /**
* Gesture controlling normal movement of the PIP.
*/
private class DefaultPipTouchGesture extends PipTouchGesture {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index aaaccd8..394d6f6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -322,6 +322,11 @@
boolean isOpening = isOpeningType(info.getType());
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
+ if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) {
+ // Currently system windows are controlled by WindowState, so don't change their
+ // surfaces. Otherwise their window tokens could be hidden unexpectedly.
+ continue;
+ }
final SurfaceControl leash = change.getLeash();
final int mode = info.getChanges().get(i).getMode();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index dba037d..3bd2ae7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.pip.phone;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -55,6 +56,7 @@
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class PipResizeGestureHandlerTest extends ShellTestCase {
+ private static final float DEFAULT_SNAP_FRACTION = 2.0f;
private static final int STEP_SIZE = 40;
private final MotionEvent.PointerProperties[] mPp = new MotionEvent.PointerProperties[2];
@@ -196,6 +198,51 @@
< mPipBoundsState.getBounds().width());
}
+ @Test
+ public void testUserResizeTo() {
+ // resizing the bounds to normal bounds at first
+ mPipResizeGestureHandler.userResizeTo(mPipBoundsState.getNormalBounds(),
+ DEFAULT_SNAP_FRACTION);
+
+ assertPipBoundsUserResizedTo(mPipBoundsState.getNormalBounds());
+
+ verify(mPipTaskOrganizer, times(1))
+ .scheduleUserResizePip(any(), any(), any());
+
+ verify(mPipTaskOrganizer, times(1))
+ .scheduleFinishResizePip(any(), any());
+
+ // bounds with max size
+ final Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x,
+ mPipBoundsState.getMaxSize().y);
+
+ // resizing the bounds to maximum bounds the second time
+ mPipResizeGestureHandler.userResizeTo(maxBounds, DEFAULT_SNAP_FRACTION);
+
+ assertPipBoundsUserResizedTo(maxBounds);
+
+ // another call to scheduleUserResizePip() and scheduleFinishResizePip() makes
+ // the total number of invocations 2 for each method
+ verify(mPipTaskOrganizer, times(2))
+ .scheduleUserResizePip(any(), any(), any());
+
+ verify(mPipTaskOrganizer, times(2))
+ .scheduleFinishResizePip(any(), any());
+ }
+
+ private void assertPipBoundsUserResizedTo(Rect bounds) {
+ // check user-resized bounds
+ assertEquals(mPipResizeGestureHandler.getUserResizeBounds().width(), bounds.width());
+ assertEquals(mPipResizeGestureHandler.getUserResizeBounds().height(), bounds.height());
+
+ // check if the bounds are the same
+ assertEquals(mPipBoundsState.getBounds().width(), bounds.width());
+ assertEquals(mPipBoundsState.getBounds().height(), bounds.height());
+
+ // a flag should be set to indicate pip has been resized by the user
+ assertTrue(mPipBoundsState.hasUserResizedPip());
+ }
+
private MotionEvent obtainMotionEvent(int action, int topLeft, int bottomRight) {
final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[2];
for (int i = 0; i < 2; i++) {
diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp
index f06fa24..0240c86 100644
--- a/libs/hwui/DeviceInfo.cpp
+++ b/libs/hwui/DeviceInfo.cpp
@@ -104,6 +104,10 @@
}
}
+void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) {
+ get()->mSupportFp16ForHdr = supportFp16ForHdr;
+}
+
void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) {
mVsyncPeriod = vsyncPeriod;
}
diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h
index 2e6e36a..577780b 100644
--- a/libs/hwui/DeviceInfo.h
+++ b/libs/hwui/DeviceInfo.h
@@ -59,6 +59,9 @@
}
static void setWideColorDataspace(ADataSpace dataspace);
+ static void setSupportFp16ForHdr(bool supportFp16ForHdr);
+ static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; };
+
// this value is only valid after the GPU has been initialized and there is a valid graphics
// context or if you are using the HWUI_NULL_GPU
int maxTextureSize() const;
@@ -88,6 +91,7 @@
int mMaxTextureSize;
sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB();
+ bool mSupportFp16ForHdr = false;
SkColorType mWideColorType = SkColorType::kN32_SkColorType;
int mDisplaysSize = 0;
int mPhysicalDisplayIndex = -1;
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 704fba9..f603e23 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -867,17 +867,19 @@
DeviceInfo::setDensity(density);
}
-static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint physicalWidth,
+static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv* env, jclass, jint physicalWidth,
jint physicalHeight, jfloat refreshRate,
jint wideColorDataspace,
jlong appVsyncOffsetNanos,
- jlong presentationDeadlineNanos) {
+ jlong presentationDeadlineNanos,
+ jboolean supportFp16ForHdr) {
DeviceInfo::setWidth(physicalWidth);
DeviceInfo::setHeight(physicalHeight);
DeviceInfo::setRefreshRate(refreshRate);
DeviceInfo::setWideColorDataspace(static_cast<ADataSpace>(wideColorDataspace));
DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos);
DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos);
+ DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr);
}
static void android_view_ThreadedRenderer_setDrawingEnabled(JNIEnv*, jclass, jboolean enabled) {
@@ -1027,7 +1029,7 @@
{"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark},
{"nSetDisplayDensityDpi", "(I)V",
(void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
- {"nInitDisplayInfo", "(IIFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
+ {"nInitDisplayInfo", "(IIFIJJZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
{"preload", "()V", (void*)android_view_ThreadedRenderer_preload},
{"isWebViewOverlaysEnabled", "()Z",
(void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled},
diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java
index fdf0f59..7a412a0 100644
--- a/location/java/android/location/GnssCapabilities.java
+++ b/location/java/android/location/GnssCapabilities.java
@@ -183,8 +183,6 @@
/**
* Returns {@code true} if GNSS chipset supports scheduling, {@code false} otherwise.
- *
- * @hide
*/
public boolean hasScheduling() {
return (mTopFlags & TOP_HAL_CAPABILITY_SCHEDULING) != 0;
@@ -193,8 +191,6 @@
/**
* Returns {@code true} if GNSS chipset supports Mobile Station Based assistance, {@code false}
* otherwise.
- *
- * @hide
*/
public boolean hasMsb() {
return (mTopFlags & TOP_HAL_CAPABILITY_MSB) != 0;
@@ -203,8 +199,6 @@
/**
* Returns {@code true} if GNSS chipset supports Mobile Station Assisted assitance,
* {@code false} otherwise.
- *
- * @hide
*/
public boolean hasMsa() {
return (mTopFlags & TOP_HAL_CAPABILITY_MSA) != 0;
@@ -212,8 +206,6 @@
/**
* Returns {@code true} if GNSS chipset supports single shot locating, {@code false} otherwise.
- *
- * @hide
*/
public boolean hasSingleShot() {
return (mTopFlags & TOP_HAL_CAPABILITY_SINGLE_SHOT) != 0;
@@ -221,8 +213,6 @@
/**
* Returns {@code true} if GNSS chipset supports on demand time, {@code false} otherwise.
- *
- * @hide
*/
public boolean hasOnDemandTime() {
return (mTopFlags & TOP_HAL_CAPABILITY_ON_DEMAND_TIME) != 0;
@@ -230,10 +220,7 @@
/**
* Returns {@code true} if GNSS chipset supports geofencing, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasGeofencing() {
return (mTopFlags & TOP_HAL_CAPABILITY_GEOFENCING) != 0;
}
@@ -272,9 +259,9 @@
/**
* Returns {@code true} if GNSS chipset supports low power mode, {@code false} otherwise.
*
- * @hide
+ * <p>The low power mode is defined in GNSS HAL. When the low power mode is active, the GNSS
+ * hardware must make strong tradeoffs to substantially restrict power use.
*/
- @SystemApi
public boolean hasLowPowerMode() {
return (mTopFlags & TOP_HAL_CAPABILITY_LOW_POWER_MODE) != 0;
}
@@ -294,20 +281,14 @@
/**
* Returns {@code true} if GNSS chipset supports satellite blocklists, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasSatelliteBlocklist() {
return (mTopFlags & TOP_HAL_CAPABILITY_SATELLITE_BLOCKLIST) != 0;
}
/**
* Returns {@code true} if GNSS chipset supports satellite PVT, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasSatellitePvt() {
return (mTopFlags & TOP_HAL_CAPABILITY_SATELLITE_PVT) != 0;
}
@@ -315,10 +296,7 @@
/**
* Returns {@code true} if GNSS chipset supports measurement corrections, {@code false}
* otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasMeasurementCorrections() {
return (mTopFlags & TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS) != 0;
}
@@ -345,10 +323,7 @@
/**
* Returns {@code true} if GNSS chipset supports correlation vectors as part of measurements
* outputs, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasMeasurementCorrelationVectors() {
return (mTopFlags & TOP_HAL_CAPABILITY_CORRELATION_VECTOR) != 0;
}
@@ -356,10 +331,7 @@
/**
* Returns {@code true} if GNSS chipset will benefit from measurement corrections for driving
* use case if provided, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasMeasurementCorrectionsForDriving() {
return (mTopFlags & TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS_FOR_DRIVING) != 0;
}
@@ -367,10 +339,7 @@
/**
* Returns {@code true} if GNSS chipset supports line-of-sight satellite identification
* measurement corrections, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasMeasurementCorrectionsLosSats() {
return (mMeasurementCorrectionsFlags & SUB_HAL_MEASUREMENT_CORRECTIONS_CAPABILITY_LOS_SATS)
!= 0;
@@ -379,10 +348,7 @@
/**
* Returns {@code true} if GNSS chipset supports per satellite excess-path-length measurement
* corrections, {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasMeasurementCorrectionsExcessPathLength() {
return (mMeasurementCorrectionsFlags
& SUB_HAL_MEASUREMENT_CORRECTIONS_CAPABILITY_EXCESS_PATH_LENGTH) != 0;
@@ -404,10 +370,7 @@
/**
* Returns {@code true} if GNSS chipset supports reflecting plane measurement corrections,
* {@code false} otherwise.
- *
- * @hide
*/
- @SystemApi
public boolean hasMeasurementCorrectionsReflectingPlane() {
return (mMeasurementCorrectionsFlags
& SUB_HAL_MEASUREMENT_CORRECTIONS_CAPABILITY_REFLECTING_PLANE) != 0;
@@ -416,8 +379,6 @@
/**
* Returns {@code true} if GNSS chipset supports measuring power totals, {@code false}
* otherwise.
- *
- * @hide
*/
public boolean hasPowerTotal() {
return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_TOTAL) != 0;
@@ -426,8 +387,6 @@
/**
* Returns {@code true} if GNSS chipset supports measuring single-band tracking power,
* {@code false} otherwise.
- *
- * @hide
*/
public boolean hasPowerSinglebandTracking() {
return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_SINGLEBAND_TRACKING) != 0;
@@ -436,8 +395,6 @@
/**
* Returns {@code true} if GNSS chipset supports measuring multi-band tracking power,
* {@code false} otherwise.
- *
- * @hide
*/
public boolean hasPowerMultibandTracking() {
return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_MULTIBAND_TRACKING) != 0;
@@ -446,8 +403,6 @@
/**
* Returns {@code true} if GNSS chipset supports measuring single-band acquisition power,
* {@code false} otherwise.
- *
- * @hide
*/
public boolean hasPowerSinglebandAcquisition() {
return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_SINGLEBAND_ACQUISITION) != 0;
@@ -456,8 +411,6 @@
/**
* Returns {@code true} if GNSS chipset supports measuring multi-band acquisition power,
* {@code false} otherwise.
- *
- * @hide
*/
public boolean hasPowerMultibandAcquisition() {
return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_MULTIBAND_ACQUISITION) != 0;
@@ -466,8 +419,6 @@
/**
* Returns {@code true} if GNSS chipset supports measuring OEM defined mode power, {@code false}
* otherwise.
- *
- * @hide
*/
public boolean hasPowerOtherModes() {
return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_OTHER_MODES) != 0;
@@ -626,8 +577,6 @@
/**
* Sets scheduling capability.
- *
- * @hide
*/
public @NonNull Builder setHasScheduling(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SCHEDULING, capable);
@@ -636,8 +585,6 @@
/**
* Sets Mobile Station Based capability.
- *
- * @hide
*/
public @NonNull Builder setHasMsb(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_MSB, capable);
@@ -646,8 +593,6 @@
/**
* Sets Mobile Station Assisted capability.
- *
- * @hide
*/
public @NonNull Builder setHasMsa(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_MSA, capable);
@@ -656,8 +601,6 @@
/**
* Sets single shot locating capability.
- *
- * @hide
*/
public @NonNull Builder setHasSingleShot(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SINGLE_SHOT, capable);
@@ -666,8 +609,6 @@
/**
* Sets on demand time capability.
- *
- * @hide
*/
public @NonNull Builder setHasOnDemandTime(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ON_DEMAND_TIME, capable);
@@ -676,10 +617,7 @@
/**
* Sets geofencing capability.
- *
- * @hide
*/
- @SystemApi
public @NonNull Builder setHasGeofencing(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_GEOFENCING, capable);
return this;
@@ -704,9 +642,9 @@
/**
* Sets low power mode capability.
*
- * @hide
+ * <p>The low power mode is defined in GNSS HAL. When the low power mode is active, the GNSS
+ * hardware must make strong tradeoffs to substantially restrict power use.
*/
- @SystemApi
public @NonNull Builder setHasLowPowerMode(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_LOW_POWER_MODE, capable);
return this;
@@ -714,10 +652,7 @@
/**
* Sets satellite blocklist capability.
- *
- * @hide
*/
- @SystemApi
public @NonNull Builder setHasSatelliteBlocklist(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SATELLITE_BLOCKLIST, capable);
return this;
@@ -725,10 +660,7 @@
/**
* Sets satellite PVT capability.
- *
- * @hide
*/
- @SystemApi
public @NonNull Builder setHasSatellitePvt(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SATELLITE_PVT, capable);
return this;
@@ -736,10 +668,7 @@
/**
* Sets measurement corrections capability.
- *
- * @hide
*/
- @SystemApi
public @NonNull Builder setHasMeasurementCorrections(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS, capable);
return this;
@@ -755,10 +684,7 @@
/**
* Sets correlation vector capability.
- *
- * @hide
*/
- @SystemApi
public @NonNull Builder setHasMeasurementCorrelationVectors(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_CORRELATION_VECTOR, capable);
return this;
@@ -766,10 +692,7 @@
/**
* Sets measurement corrections for driving capability.
- *
- * @hide
*/
- @SystemApi
public @NonNull Builder setHasMeasurementCorrectionsForDriving(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS_FOR_DRIVING,
capable);
@@ -777,11 +700,8 @@
}
/**
- * Sets measurement corrections line-of-sight satellites capabilitity.
- *
- * @hide
+ * Sets measurement corrections line-of-sight satellites capability.
*/
- @SystemApi
public @NonNull Builder setHasMeasurementCorrectionsLosSats(boolean capable) {
mMeasurementCorrectionsFlags = setFlag(mMeasurementCorrectionsFlags,
SUB_HAL_MEASUREMENT_CORRECTIONS_CAPABILITY_LOS_SATS, capable);
@@ -789,11 +709,8 @@
}
/**
- * Sets measurement corrections excess path length capabilitity.
- *
- * @hide
+ * Sets measurement corrections excess path length capability.
*/
- @SystemApi
public @NonNull Builder setHasMeasurementCorrectionsExcessPathLength(boolean capable) {
mMeasurementCorrectionsFlags = setFlag(mMeasurementCorrectionsFlags,
SUB_HAL_MEASUREMENT_CORRECTIONS_CAPABILITY_EXCESS_PATH_LENGTH, capable);
@@ -801,11 +718,8 @@
}
/**
- * Sets measurement corrections reflecting plane capabilitity.
- *
- * @hide
+ * Sets measurement corrections reflecting plane capability.
*/
- @SystemApi
public @NonNull Builder setHasMeasurementCorrectionsReflectingPlane(boolean capable) {
mMeasurementCorrectionsFlags = setFlag(mMeasurementCorrectionsFlags,
SUB_HAL_MEASUREMENT_CORRECTIONS_CAPABILITY_REFLECTING_PLANE, capable);
@@ -813,9 +727,7 @@
}
/**
- * Sets power totals capabilitity.
- *
- * @hide
+ * Sets power totals capability.
*/
public @NonNull Builder setHasPowerTotal(boolean capable) {
mPowerFlags = setFlag(mPowerFlags, SUB_HAL_POWER_CAPABILITY_TOTAL, capable);
@@ -823,9 +735,7 @@
}
/**
- * Sets power single-band tracking capabilitity.
- *
- * @hide
+ * Sets power single-band tracking capability.
*/
public @NonNull Builder setHasPowerSinglebandTracking(boolean capable) {
mPowerFlags = setFlag(mPowerFlags, SUB_HAL_POWER_CAPABILITY_SINGLEBAND_TRACKING,
@@ -834,9 +744,7 @@
}
/**
- * Sets power multi-band tracking capabilitity.
- *
- * @hide
+ * Sets power multi-band tracking capability.
*/
public @NonNull Builder setHasPowerMultibandTracking(boolean capable) {
mPowerFlags = setFlag(mPowerFlags, SUB_HAL_POWER_CAPABILITY_MULTIBAND_TRACKING,
@@ -845,9 +753,7 @@
}
/**
- * Sets power single-band acquisition capabilitity.
- *
- * @hide
+ * Sets power single-band acquisition capability.
*/
public @NonNull Builder setHasPowerSinglebandAcquisition(boolean capable) {
mPowerFlags = setFlag(mPowerFlags, SUB_HAL_POWER_CAPABILITY_SINGLEBAND_ACQUISITION,
@@ -856,9 +762,7 @@
}
/**
- * Sets power multi-band acquisition capabilitity.
- *
- * @hide
+ * Sets power multi-band acquisition capability.
*/
public @NonNull Builder setHasPowerMultibandAcquisition(boolean capable) {
mPowerFlags = setFlag(mPowerFlags, SUB_HAL_POWER_CAPABILITY_MULTIBAND_ACQUISITION,
@@ -867,9 +771,7 @@
}
/**
- * Sets power other modes capabilitity.
- *
- * @hide
+ * Sets OEM-defined power modes capability.
*/
public @NonNull Builder setHasPowerOtherModes(boolean capable) {
mPowerFlags = setFlag(mPowerFlags, SUB_HAL_POWER_CAPABILITY_OTHER_MODES, capable);
diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl
index 9bf126b..31fb8d0 100644
--- a/media/java/android/media/session/ISession.aidl
+++ b/media/java/android/media/session/ISession.aidl
@@ -35,7 +35,7 @@
ISessionController getController();
void setFlags(int flags);
void setActive(boolean active);
- void setMediaButtonReceiver(in PendingIntent mbr, String sessionPackageName);
+ void setMediaButtonReceiver(in PendingIntent mbr);
void setMediaButtonBroadcastReceiver(in ComponentName broadcastReceiver);
void setLaunchPendingIntent(in PendingIntent pi);
void destroySession();
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 9e265d8..1bd12af 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -286,7 +286,7 @@
@Deprecated
public void setMediaButtonReceiver(@Nullable PendingIntent mbr) {
try {
- mBinder.setMediaButtonReceiver(mbr, mContext.getPackageName());
+ mBinder.setMediaButtonReceiver(mbr);
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
}
diff --git a/packages/BackupEncryption/Android.bp b/packages/BackupEncryption/Android.bp
deleted file mode 100644
index 0244f28..0000000
--- a/packages/BackupEncryption/Android.bp
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// 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 {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_app {
- name: "BackupEncryption",
- defaults: ["platform_app_defaults"],
- srcs: ["src/**/*.java"],
- static_libs: ["backup-encryption-protos", "backuplib"],
- optimize: { enabled: false },
- platform_apis: true,
- certificate: "platform",
- privileged: true,
-}
-
-java_library {
- name: "backup-encryption-protos",
- proto: { type: "nano" },
- srcs: ["proto/**/*.proto"],
-}
diff --git a/packages/BackupEncryption/AndroidManifest.xml b/packages/BackupEncryption/AndroidManifest.xml
deleted file mode 100644
index 4d174e3..0000000
--- a/packages/BackupEncryption/AndroidManifest.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright (c) 2016 Google Inc.
- *
- * 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.server.backup.encryption"
- android:sharedUserId="android.uid.system" >
-
- <application android:allowBackup="false" >
- <!-- This service does not need to be exported because it shares uid with the system server
- which is the only client. -->
- <service android:name=".BackupEncryptionService"
- android:exported="false">
- <intent-filter>
- <action android:name="android.encryption.BACKUP_ENCRYPTION" />
- </intent-filter>
- </service>
- </application>
-</manifest>
diff --git a/packages/BackupEncryption/OWNERS b/packages/BackupEncryption/OWNERS
deleted file mode 100644
index d99779e..0000000
--- a/packages/BackupEncryption/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include /services/backup/OWNERS
diff --git a/packages/BackupEncryption/proguard.flags b/packages/BackupEncryption/proguard.flags
deleted file mode 100644
index 851ce8c..0000000
--- a/packages/BackupEncryption/proguard.flags
+++ /dev/null
@@ -1 +0,0 @@
--keep class com.android.server.backup.encryption
diff --git a/packages/BackupEncryption/proto/backup_chunks_metadata.proto b/packages/BackupEncryption/proto/backup_chunks_metadata.proto
deleted file mode 100644
index 2fdedbf..0000000
--- a/packages/BackupEncryption/proto/backup_chunks_metadata.proto
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-syntax = "proto2";
-
-package android_backup_crypto;
-
-option java_package = "com.android.server.backup.encryption.protos";
-option java_outer_classname = "ChunksMetadataProto";
-
-// Cipher type with which the chunks are encrypted. For now we only support AES/GCM/NoPadding, but
-// this is for backwards-compatibility in case we need to change the default Cipher in the future.
-enum CipherType {
- UNKNOWN_CIPHER_TYPE = 0;
- // Chunk is prefixed with a 12-byte nonce. The tag length is 16 bytes.
- AES_256_GCM = 1;
-}
-
-// Checksum type with which the plaintext is verified.
-enum ChecksumType {
- UNKNOWN_CHECKSUM_TYPE = 0;
- SHA_256 = 1;
-}
-
-enum ChunkOrderingType {
- CHUNK_ORDERING_TYPE_UNSPECIFIED = 0;
- // The chunk ordering contains a list of the start position of each chunk in the encrypted file,
- // ordered as in the plaintext file. This allows us to recreate the original plaintext file
- // during decryption. We use this mode for full backups where the order of the data in the file
- // is important.
- EXPLICIT_STARTS = 1;
- // The chunk ordering does not contain any start positions, and instead each encrypted chunk in
- // the backup file is prefixed with its length. This allows us to decrypt each chunk but does
- // not give any information about the order. However, we use this mode for key value backups
- // where the order does not matter.
- INLINE_LENGTHS = 2;
-}
-
-// Chunk entry (for local state)
-message Chunk {
- // SHA-256 MAC of the plaintext of the chunk
- optional bytes hash = 1;
- // Number of bytes in encrypted chunk
- optional int32 length = 2;
-}
-
-// List of the chunks in the blob, along with the length of each chunk. From this is it possible to
-// extract individual chunks. (i.e., start position is equal to the sum of the lengths of all
-// preceding chunks.)
-//
-// This is local state stored on the device. It is never sent to the backup server. See
-// ChunkOrdering for how the device restores the chunks in the correct order.
-// Next tag : 6
-message ChunkListing {
- repeated Chunk chunks = 1;
-
- // Cipher algorithm with which the chunks are encrypted.
- optional CipherType cipher_type = 2;
-
- // Defines the type of chunk order used to encode the backup file on the server, so that we can
- // consistently use the same type between backups. If unspecified this backup file was created
- // before INLINE_LENGTHS was supported, thus assume it is EXPLICIT_STARTS.
- optional ChunkOrderingType chunk_ordering_type = 5;
-
- // The document ID returned from Scotty server after uploading the blob associated with this
- // listing. This needs to be sent when uploading new diff scripts.
- optional string document_id = 3;
-
- // Fingerprint mixer salt used for content defined chunking. This is randomly generated for each
- // package during the initial non-incremental backup and reused for incremental backups.
- optional bytes fingerprint_mixer_salt = 4;
-}
-
-// Ordering information about plaintext and checksum. This is used on restore to reconstruct the
-// blob in its correct order. (The chunk order is randomized so as to give the server less
-// information about which parts of the backup are changing over time.) This proto is encrypted
-// before being uploaded to the server, with a key unknown to the server.
-message ChunkOrdering {
- // For backups where ChunksMetadata#chunk_ordering_type = EXPLICIT STARTS:
- // Ordered start positions of chunks. i.e., the file is the chunk starting at this position,
- // followed by the chunk starting at this position, followed by ... etc. You can compute the
- // lengths of the chunks by sorting this list then looking at the start position of the next
- // chunk after the chunk you care about. This is guaranteed to work as all chunks are
- // represented in this list.
- //
- // For backups where ChunksMetadata#chunk_ordering_type = INLINE_LENGTHS:
- // This field is unused. See ChunkOrderingType#INLINE_LENGTHS.
- repeated int32 starts = 1 [packed = true];
-
- // Checksum of plaintext content. (i.e., in correct order.)
- //
- // Each chunk also has a MAC, as generated by GCM, so this is NOT Mac-then-Encrypt, which has
- // security implications. This is an additional checksum to verify that once the chunks have
- // been reordered, that the file matches the expected plaintext. This prevents the device
- // restoring garbage data in case of a mismatch between the ChunkOrdering and the backup blob.
- optional bytes checksum = 2;
-}
-
-// Additional metadata about a backup blob that needs to be synced to the server. This is used on
-// restore to reconstruct the blob in its correct order. (The chunk order is randomized so as to
-// give the server less information about which parts of the backup are changing over time.) This
-// data structure is only ever uploaded to the server encrypted with a key unknown to the server.
-// Next tag : 6
-message ChunksMetadata {
- // Cipher algorithm with which the chunk listing and chunks are encrypted.
- optional CipherType cipher_type = 1;
-
- // Defines the type of chunk order this metadata contains. If unspecified this backup file was
- // created before INLINE_LENGTHS was supported, thus assume it is EXPLICIT_STARTS.
- optional ChunkOrderingType chunk_ordering_type = 5
- [default = CHUNK_ORDERING_TYPE_UNSPECIFIED];
-
- // Encrypted bytes of ChunkOrdering
- optional bytes chunk_ordering = 2;
-
- // The type of algorithm used for the checksum of the plaintext. (See ChunkOrdering.) This is
- // for forwards compatibility in case we change the algorithm in the future. For now, always
- // SHA-256.
- optional ChecksumType checksum_type = 3;
-
- // This used to be the plaintext tertiary key. No longer used.
- reserved 4;
-}
\ No newline at end of file
diff --git a/packages/BackupEncryption/proto/key_value_listing.proto b/packages/BackupEncryption/proto/key_value_listing.proto
deleted file mode 100644
index 001e697..0000000
--- a/packages/BackupEncryption/proto/key_value_listing.proto
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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
- */
-
-syntax = "proto2";
-
-package android_backup_crypto;
-
-option java_package = "com.android.server.backup.encryption.protos";
-option java_outer_classname = "KeyValueListingProto";
-
-// An entry of a key-value pair.
-message KeyValueEntry {
- // Plaintext key of the key-value pair.
- optional string key = 1;
- // SHA-256 MAC of the plaintext of the chunk containing the pair
- optional bytes hash = 2;
-}
-
-// Describes the key/value pairs currently in the backup blob, mapping from the
-// plaintext key to the hash of the chunk containing the pair.
-//
-// This is local state stored on the device. It is never sent to the
-// backup server. See ChunkOrdering for how the device restores the
-// key-value pairs in the correct order.
-message KeyValueListing {
- repeated KeyValueEntry entries = 1;
-}
diff --git a/packages/BackupEncryption/proto/key_value_pair.proto b/packages/BackupEncryption/proto/key_value_pair.proto
deleted file mode 100644
index 177fa30..0000000
--- a/packages/BackupEncryption/proto/key_value_pair.proto
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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
- */
-
-syntax = "proto2";
-
-package android_backup_crypto;
-
-option java_package = "com.android.server.backup.encryption.protos";
-option java_outer_classname = "KeyValuePairProto";
-
-// Serialized form of a key-value pair, when it is to be encrypted in a blob.
-// The backup blob for a key-value database consists of repeated encrypted
-// key-value pairs like this, in a randomized order. See ChunkOrdering for how
-// these are then reconstructed during a restore.
-message KeyValuePair {
- optional string key = 1;
- optional bytes value = 2;
-}
diff --git a/packages/BackupEncryption/proto/wrapped_key.proto b/packages/BackupEncryption/proto/wrapped_key.proto
deleted file mode 100644
index 817b7b40..0000000
--- a/packages/BackupEncryption/proto/wrapped_key.proto
+++ /dev/null
@@ -1,52 +0,0 @@
-syntax = "proto2";
-
-package android_backup_crypto;
-
-option java_package = "com.android.server.backup.encryption.protos";
-option java_outer_classname = "WrappedKeyProto";
-
-// Metadata associated with a tertiary key.
-message KeyMetadata {
- // Type of Cipher algorithm the key is used for.
- enum Type {
- UNKNOWN = 0;
- // No padding. Uses 12-byte nonce. Tag length 16 bytes.
- AES_256_GCM = 1;
- }
-
- // What kind of Cipher algorithm the key is used for. We assume at the moment
- // that this will always be AES_256_GCM and throw if this is not the case.
- // Provided here for forwards compatibility in case at some point we need to
- // change Cipher algorithm.
- optional Type type = 1;
-}
-
-// An encrypted tertiary key.
-message WrappedKey {
- // The Cipher with which the key was encrypted.
- enum WrapAlgorithm {
- UNKNOWN = 0;
- // No padding. Uses 16-byte nonce (see nonce field). Tag length 16 bytes.
- // The nonce is 16-bytes as this is wrapped with a key in AndroidKeyStore.
- // AndroidKeyStore requires that it generates the IV, and it generates a
- // 16-byte IV for you. You CANNOT provide your own IV.
- AES_256_GCM = 1;
- }
-
- // Cipher algorithm used to wrap the key. We assume at the moment that this
- // is always AES_256_GC and throw if this is not the case. Provided here for
- // forwards compatibility if at some point we need to change Cipher algorithm.
- optional WrapAlgorithm wrap_algorithm = 1;
-
- // The nonce used to initialize the Cipher in AES/256/GCM mode.
- optional bytes nonce = 2;
-
- // The encrypted bytes of the key material.
- optional bytes key = 3;
-
- // Associated key metadata.
- optional KeyMetadata metadata = 4;
-
- // Deprecated field; Do not use
- reserved 5;
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java
deleted file mode 100644
index bb1336f..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * 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;
-
-import static com.android.internal.util.Preconditions.checkState;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.RecoveryController;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.KeyStoreException;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-
-/**
- * State about encrypted backups that needs to be remembered.
- */
-public class CryptoSettings {
-
- private static final String TAG = "CryptoSettings";
-
- private static final String SHARED_PREFERENCES_NAME = "crypto_settings";
-
- private static final String KEY_IS_INITIALIZED = "isInitialized";
- private static final String KEY_ACTIVE_SECONDARY_ALIAS = "activeSecondary";
- private static final String KEY_NEXT_SECONDARY_ALIAS = "nextSecondary";
- private static final String SECONDARY_KEY_LAST_ROTATED_AT = "secondaryKeyLastRotatedAt";
- private static final String[] SETTINGS_FOR_BACKUP = {
- KEY_IS_INITIALIZED,
- KEY_ACTIVE_SECONDARY_ALIAS,
- KEY_NEXT_SECONDARY_ALIAS,
- SECONDARY_KEY_LAST_ROTATED_AT
- };
-
- private static final long DEFAULT_SECONDARY_KEY_ROTATION_PERIOD =
- TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS);
-
- private static final String KEY_ANCESTRAL_SECONDARY_KEY_VERSION =
- "ancestral_secondary_key_version";
-
- private final SharedPreferences mSharedPreferences;
- private final Context mContext;
-
- /**
- * A new instance.
- *
- * @param context For looking up the {@link SharedPreferences}, for storing state.
- * @return The instance.
- */
- public static CryptoSettings getInstance(Context context) {
- // We need single process mode because CryptoSettings may be used from several processes
- // simultaneously.
- SharedPreferences sharedPreferences =
- context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- return new CryptoSettings(sharedPreferences, context);
- }
-
- /**
- * A new instance using {@link SharedPreferences} in the default mode.
- *
- * <p>This will not work across multiple processes but will work in tests.
- */
- @VisibleForTesting
- public static CryptoSettings getInstanceForTesting(Context context) {
- SharedPreferences sharedPreferences =
- context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- return new CryptoSettings(sharedPreferences, context);
- }
-
- private CryptoSettings(SharedPreferences sharedPreferences, Context context) {
- mSharedPreferences = Objects.requireNonNull(sharedPreferences);
- mContext = Objects.requireNonNull(context);
- }
-
- /**
- * The alias of the current active secondary key. This should be used to retrieve the key from
- * AndroidKeyStore.
- */
- public Optional<String> getActiveSecondaryKeyAlias() {
- return getStringInSharedPrefs(KEY_ACTIVE_SECONDARY_ALIAS);
- }
-
- /**
- * The alias of the secondary key to which the client is rotating. The rotation is not
- * immediate, which is why this setting is needed. Once the next key is created, it can take up
- * to 72 hours potentially (or longer if the user has no network) for the next key to be synced
- * with the keystore. Only after that has happened does the client attempt to re-wrap all
- * tertiary keys and commit the rotation.
- */
- public Optional<String> getNextSecondaryKeyAlias() {
- return getStringInSharedPrefs(KEY_NEXT_SECONDARY_ALIAS);
- }
-
- /**
- * If the settings have been initialized.
- */
- public boolean getIsInitialized() {
- return mSharedPreferences.getBoolean(KEY_IS_INITIALIZED, false);
- }
-
- /**
- * Sets the alias of the currently active secondary key.
- *
- * @param activeAlias The alias, as in AndroidKeyStore.
- * @throws IllegalArgumentException if the alias is not in the user's keystore.
- */
- public void setActiveSecondaryKeyAlias(String activeAlias) throws IllegalArgumentException {
- assertIsValidAlias(activeAlias);
- mSharedPreferences.edit().putString(KEY_ACTIVE_SECONDARY_ALIAS, activeAlias).apply();
- }
-
- /**
- * Sets the alias of the secondary key to which the client is rotating.
- *
- * @param nextAlias The alias, as in AndroidKeyStore.
- * @throws KeyStoreException if unable to check whether alias is valid in the keystore.
- * @throws IllegalArgumentException if the alias is not in the user's keystore.
- */
- public void setNextSecondaryAlias(String nextAlias) throws IllegalArgumentException {
- assertIsValidAlias(nextAlias);
- mSharedPreferences.edit().putString(KEY_NEXT_SECONDARY_ALIAS, nextAlias).apply();
- }
-
- /**
- * Unsets the alias of the key to which the client is rotating. This is generally performed once
- * a rotation is complete.
- */
- public void removeNextSecondaryKeyAlias() {
- mSharedPreferences.edit().remove(KEY_NEXT_SECONDARY_ALIAS).apply();
- }
-
- /**
- * Sets the timestamp of when the secondary key was last rotated.
- *
- * @param timestamp The timestamp to set.
- */
- public void setSecondaryLastRotated(long timestamp) {
- mSharedPreferences.edit().putLong(SECONDARY_KEY_LAST_ROTATED_AT, timestamp).apply();
- }
-
- /**
- * Returns a timestamp of when the secondary key was last rotated.
- *
- * @return The timestamp.
- */
- public Optional<Long> getSecondaryLastRotated() {
- if (!mSharedPreferences.contains(SECONDARY_KEY_LAST_ROTATED_AT)) {
- return Optional.empty();
- }
- return Optional.of(mSharedPreferences.getLong(SECONDARY_KEY_LAST_ROTATED_AT, -1));
- }
-
- /**
- * Sets the settings to have been initialized. (Otherwise loading should try to initialize
- * again.)
- */
- private void setIsInitialized() {
- mSharedPreferences.edit().putBoolean(KEY_IS_INITIALIZED, true).apply();
- }
-
- /**
- * Initializes with the given key alias.
- *
- * @param alias The secondary key alias to be set as active.
- * @throws IllegalArgumentException if the alias does not reference a valid key.
- * @throws IllegalStateException if attempting to initialize an already initialized settings.
- */
- public void initializeWithKeyAlias(String alias) throws IllegalArgumentException {
- checkState(
- !getIsInitialized(), "Attempting to initialize an already initialized settings.");
- setActiveSecondaryKeyAlias(alias);
- setIsInitialized();
- }
-
- /** Returns the secondary key version of the encrypted backup set to restore from (if set). */
- public Optional<String> getAncestralSecondaryKeyVersion() {
- return Optional.ofNullable(
- mSharedPreferences.getString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, null));
- }
-
- /** Sets the secondary key version of the encrypted backup set to restore from. */
- public void setAncestralSecondaryKeyVersion(String ancestralSecondaryKeyVersion) {
- mSharedPreferences
- .edit()
- .putString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, ancestralSecondaryKeyVersion)
- .apply();
- }
-
- /** The number of milliseconds between secondary key rotation */
- public long backupSecondaryKeyRotationIntervalMs() {
- return DEFAULT_SECONDARY_KEY_ROTATION_PERIOD;
- }
-
- /** Deletes all crypto settings related to backup (as opposed to restore). */
- public void clearAllSettingsForBackup() {
- Editor sharedPrefsEditor = mSharedPreferences.edit();
- for (String backupSettingKey : SETTINGS_FOR_BACKUP) {
- sharedPrefsEditor.remove(backupSettingKey);
- }
- sharedPrefsEditor.apply();
-
- Slog.d(TAG, "Cleared crypto settings for backup");
- }
-
- /**
- * Throws {@link IllegalArgumentException} if the alias does not refer to a key that is in
- * the {@link RecoveryController}.
- */
- private void assertIsValidAlias(String alias) throws IllegalArgumentException {
- try {
- if (!RecoveryController.getInstance(mContext).getAliases().contains(alias)) {
- throw new IllegalArgumentException(alias + " is not in RecoveryController");
- }
- } catch (InternalRecoveryServiceException e) {
- throw new IllegalArgumentException("Problem accessing recovery service", e);
- }
- }
-
- private Optional<String> getStringInSharedPrefs(String key) {
- return Optional.ofNullable(mSharedPreferences.getString(key, null));
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/EncryptionKeyHelper.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/EncryptionKeyHelper.java
deleted file mode 100644
index 2035b66..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/EncryptionKeyHelper.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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;
-
-import android.content.Context;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.RecoveryController;
-
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.UnrecoverableKeyException;
-
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-
-class EncryptionKeyHelper {
- private static SecureRandom sSecureRandom = new SecureRandom();
-
- private final Context mContext;
- private final RecoverableKeyStoreSecondaryKeyManager
- .RecoverableKeyStoreSecondaryKeyManagerProvider
- mSecondaryKeyManagerProvider;
-
- EncryptionKeyHelper(Context context) {
- mContext = context;
- mSecondaryKeyManagerProvider =
- () ->
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(mContext), sSecureRandom);
- }
-
- RecoverableKeyStoreSecondaryKeyManager
- .RecoverableKeyStoreSecondaryKeyManagerProvider getKeyManagerProvider() {
- return mSecondaryKeyManagerProvider;
- }
-
- RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
- throws UnrecoverableKeyException, InternalRecoveryServiceException {
- String keyAlias = CryptoSettings.getInstance(mContext).getActiveSecondaryKeyAlias().get();
- return mSecondaryKeyManagerProvider.get().get(keyAlias).get();
- }
-
- SecretKey getTertiaryKey(
- String packageName,
- RecoverableKeyStoreSecondaryKey secondaryKey)
- throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
- NoSuchAlgorithmException, IOException, NoSuchPaddingException,
- InvalidKeyException {
- TertiaryKeyManager tertiaryKeyManager =
- new TertiaryKeyManager(
- mContext,
- sSecureRandom,
- TertiaryKeyRotationScheduler.getInstance(mContext),
- secondaryKey,
- packageName);
- return tertiaryKeyManager.getKey();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java
deleted file mode 100644
index f3ab2bde..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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;
-
-import android.app.backup.BackupTransport;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/** Accepts the full backup data stream and sends it to the server. */
-public interface FullBackupDataProcessor {
- /**
- * Prepares the upload.
- *
- * <p>After this, call {@link #start()} to establish the connection.
- *
- * @param inputStream to read the backup data from, calling {@link #finish} or {@link #cancel}
- * will close the stream
- * @return {@code true} if the connection was set up successfully, otherwise {@code false}
- */
- boolean initiate(InputStream inputStream) throws IOException;
-
- /**
- * Starts the upload, establishing the connection to the server.
- *
- * <p>After this, call {@link #pushData(int)} to request that the processor reads data from the
- * socket, and uploads it to the server.
- *
- * <p>After this you must call one of {@link #cancel()}, {@link #finish()}, {@link
- * #handleCheckSizeRejectionZeroBytes()}, {@link #handleCheckSizeRejectionQuotaExceeded()} or
- * {@link #handleSendBytesQuotaExceeded()} to close the upload.
- */
- void start();
-
- /**
- * Requests that the processor read {@code numBytes} from the input stream passed in {@link
- * #initiate(InputStream)} and upload them to the server.
- *
- * @return {@link BackupTransport#TRANSPORT_OK} if the upload succeeds, or {@link
- * BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
- * quota, or {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
- */
- int pushData(int numBytes);
-
- /** Cancels the upload and tears down the connection. */
- void cancel();
-
- /**
- * Finish the upload and tear down the connection.
- *
- * <p>Call this after there is no more data to push with {@link #pushData(int)}.
- *
- * @return One of {@link BackupTransport#TRANSPORT_OK} if the app upload succeeds, {@link
- * BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
- * quota, {@link BackupTransport#TRANSPORT_ERROR} for server 500s, or {@link
- * BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
- */
- int finish();
-
- /**
- * Notifies the processor that the current upload should be terminated because the estimated
- * size is zero.
- */
- void handleCheckSizeRejectionZeroBytes();
-
- /**
- * Notifies the processor that the current upload should be terminated because the estimated
- * size exceeds the quota.
- */
- void handleCheckSizeRejectionQuotaExceeded();
-
- /**
- * Notifies this class that the current upload should be terminated because the quota was
- * exceeded during upload.
- */
- void handleSendBytesQuotaExceeded();
-
- /**
- * Attaches {@link FullBackupCallbacks} which the processor will notify when the backup
- * succeeds.
- */
- void attachCallbacks(FullBackupCallbacks fullBackupCallbacks);
-
- /**
- * Implemented by the caller of the processor to receive notification of when the backup
- * succeeds.
- */
- interface FullBackupCallbacks {
- /** The processor calls this to indicate that the current backup has succeeded. */
- void onSuccess();
-
- /** The processor calls this if the upload failed for a non-transient reason. */
- void onTransferFailed();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java
deleted file mode 100644
index e4c4049..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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;
-
-import java.io.IOException;
-
-/**
- * Retrieves the data during a full restore, decrypting it if necessary.
- *
- * <p>Use {@link FullRestoreDataProcessorFactory} to construct the encrypted or unencrypted
- * processor as appropriate during restore.
- */
-public interface FullRestoreDataProcessor {
- /** Return value of {@link #readNextChunk} when there is no more data to download. */
- int END_OF_STREAM = -1;
-
- /**
- * Reads the next chunk of restore data and writes it to the given buffer.
- *
- * <p>Where necessary, will open the connection to the server and/or decrypt the backup file.
- *
- * <p>The implementation may retry various errors. If the retries fail it will throw the
- * relevant exception.
- *
- * @return the number of bytes read, or {@link #END_OF_STREAM} if there is no more data
- * @throws IOException when downloading from the network or writing to disk
- */
- int readNextChunk(byte[] buffer) throws IOException;
-
- /**
- * Closes the connection to the server, deletes any temporary files and optionally sends a log
- * with the given finish type.
- *
- * @param finishType one of {@link FullRestoreDownloader.FinishType}
- */
- void finish(FullRestoreDownloader.FinishType finishType);
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDownloader.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDownloader.java
deleted file mode 100644
index afcca79..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDownloader.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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;
-
-import java.io.IOException;
-
-/** Interface for classes which will provide backup data */
-public abstract class FullRestoreDownloader {
- /** Enum to provide information on why a download finished */
- public enum FinishType {
- UNKNOWN_FINISH(0),
- // Finish the downloading and successfully write data to Android OS.
- FINISHED(1),
- // Download failed with any kind of exception.
- TRANSFER_FAILURE(2),
- // Download failed due to auth failure on the device.
- AUTH_FAILURE(3),
- // Aborted by Android Framework.
- FRAMEWORK_ABORTED(4);
-
- private int mValue;
-
- FinishType(int value) {
- mValue = value;
- }
- }
-
- /** Get the next data chunk from the backing store */
- public abstract int readNextChunk(byte[] buffer) throws IOException;
-
- /** Called when we've finished restoring the data */
- public abstract void finish(FinishType finishType);
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/KeyValueEncrypter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/KeyValueEncrypter.java
deleted file mode 100644
index db2dd2f..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/KeyValueEncrypter.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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;
-
-import android.content.Context;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.KeyWrapUtils;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
-import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.SecureRandom;
-import java.util.Map;
-
-public class KeyValueEncrypter {
- private static final String TAG = "KeyValueEncrypter";
-
- private final Context mContext;
- private final EncryptionKeyHelper mKeyHelper;
-
- public KeyValueEncrypter(Context context) {
- mContext = context;
- mKeyHelper = new EncryptionKeyHelper(mContext);
- }
-
- public void encryptKeyValueData(
- String packageName, ParcelFileDescriptor inputFd, OutputStream outputStream)
- throws Exception {
- EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
- new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
- EncryptedKvBackupTask backupTask =
- backupTaskFactory.newInstance(
- mContext,
- new SecureRandom(),
- new FileBackupServer(outputStream),
- CryptoSettings.getInstance(mContext),
- mKeyHelper.getKeyManagerProvider(),
- inputFd,
- packageName);
- backupTask.performBackup(/* incremental */ false);
- }
-
- public void decryptKeyValueData(String packageName,
- InputStream encryptedInputStream, ParcelFileDescriptor outputFd) throws Exception {
- RecoverableKeyStoreSecondaryKey secondaryKey = mKeyHelper.getActiveSecondaryKey();
-
- EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
- new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
- EncryptedKvRestoreTask restoreTask =
- restoreTaskFactory.newInstance(
- mContext,
- mKeyHelper.getKeyManagerProvider(),
- new InputStreamFullRestoreDownloader(encryptedInputStream),
- secondaryKey.getAlias(),
- KeyWrapUtils.wrap(
- secondaryKey.getSecretKey(),
- mKeyHelper.getTertiaryKey(packageName, secondaryKey)));
-
- restoreTask.getRestoreData(outputFd);
- }
-
- // TODO(b/142455725): Extract into a commong class.
- private static class FileBackupServer implements CryptoBackupServer {
- private static final String EMPTY_DOC_ID = "";
-
- private final OutputStream mOutputStream;
-
- FileBackupServer(OutputStream outputStream) {
- mOutputStream = outputStream;
- }
-
- @Override
- public String uploadIncrementalBackup(
- String packageName,
- String oldDocId,
- byte[] diffScript,
- WrappedKeyProto.WrappedKey tertiaryKey) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public String uploadNonIncrementalBackup(
- String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
- try {
- mOutputStream.write(data);
- } catch (IOException e) {
- Log.w(TAG, "Failed to write encrypted data to file: ", e);
- }
-
- return EMPTY_DOC_ID;
- }
-
- @Override
- public void setActiveSecondaryKeyAlias(
- String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
- // Do nothing.
- }
- }
-
- // TODO(b/142455725): Extract into a commong class.
- private static class InputStreamFullRestoreDownloader extends FullRestoreDownloader {
- private final InputStream mInputStream;
-
- InputStreamFullRestoreDownloader(InputStream inputStream) {
- mInputStream = inputStream;
- }
-
- @Override
- public int readNextChunk(byte[] buffer) throws IOException {
- return mInputStream.read(buffer);
- }
-
- @Override
- public void finish(FinishType finishType) {
- try {
- mInputStream.close();
- } catch (IOException e) {
- Log.w(TAG, "Error while reading restore data");
- }
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java
deleted file mode 100644
index 66be25b..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/** Utility methods for dealing with Streams */
-public class StreamUtils {
- private static final int MAX_COPY_BUFFER_SIZE = 1024; // 1k copy buffer size.
-
- /**
- * Close a Closeable and silently ignore any IOExceptions.
- *
- * @param closeable The closeable to close
- */
- public static void closeQuietly(Closeable closeable) {
- try {
- closeable.close();
- } catch (IOException ioe) {
- // Silently ignore
- }
- }
-
- /**
- * Copy data from an InputStream to an OutputStream upto a given number of bytes.
- *
- * @param in The source InputStream
- * @param out The destination OutputStream
- * @param limit The maximum number of bytes to copy
- * @throws IOException Thrown if there is a problem performing the copy.
- */
- public static void copyStream(InputStream in, OutputStream out, int limit) throws IOException {
- int bufferSize = Math.min(MAX_COPY_BUFFER_SIZE, limit);
- byte[] buffer = new byte[bufferSize];
-
- int copied = 0;
- while (copied < limit) {
- int maxReadSize = Math.min(bufferSize, limit - copied);
- int read = in.read(buffer, 0, maxReadSize);
- if (read < 0) {
- return; // Reached the stream end before the limit
- }
- out.write(buffer, 0, read);
- copied += read;
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkHash.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkHash.java
deleted file mode 100644
index 1630eb8..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkHash.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunk;
-
-import com.android.internal.util.Preconditions;
-
-import java.util.Arrays;
-import java.util.Base64;
-
-/**
- * Represents the SHA-256 hash of the plaintext of a chunk, which is frequently used as a key.
- *
- * <p>This class is {@link Comparable} and implements {@link #equals(Object)} and {@link
- * #hashCode()}.
- */
-public class ChunkHash implements Comparable<ChunkHash> {
- /** The length of the hash in bytes. The hash is a SHA-256, so this is 256 bits. */
- public static final int HASH_LENGTH_BYTES = 256 / 8;
-
- private static final int UNSIGNED_MASK = 0xFF;
-
- private final byte[] mHash;
-
- /** Constructs a new instance which wraps the given SHA-256 hash bytes. */
- public ChunkHash(byte[] hash) {
- Preconditions.checkArgument(hash.length == HASH_LENGTH_BYTES, "Hash must have 256 bits");
- mHash = hash;
- }
-
- public byte[] getHash() {
- return mHash;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof ChunkHash)) {
- return false;
- }
-
- ChunkHash chunkHash = (ChunkHash) o;
- return Arrays.equals(mHash, chunkHash.mHash);
- }
-
- @Override
- public int hashCode() {
- return Arrays.hashCode(mHash);
- }
-
- @Override
- public int compareTo(ChunkHash other) {
- return lexicographicalCompareUnsignedBytes(getHash(), other.getHash());
- }
-
- @Override
- public String toString() {
- return Base64.getEncoder().encodeToString(mHash);
- }
-
- private static int lexicographicalCompareUnsignedBytes(byte[] left, byte[] right) {
- int minLength = Math.min(left.length, right.length);
- for (int i = 0; i < minLength; i++) {
- int result = toInt(left[i]) - toInt(right[i]);
- if (result != 0) {
- return result;
- }
- }
- return left.length - right.length;
- }
-
- private static int toInt(byte value) {
- return value & UNSIGNED_MASK;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkListingMap.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkListingMap.java
deleted file mode 100644
index 51d7d53..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkListingMap.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.chunk;
-
-import android.annotation.Nullable;
-
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Chunk listing in a format optimized for quick look up of chunks via their hash keys. This is
- * useful when building an incremental backup. After a chunk has been produced, the algorithm can
- * quickly look up whether the chunk existed in the previous backup by checking this chunk listing.
- * It can then tell the server to use that chunk, through telling it the position and length of the
- * chunk in the previous backup's blob.
- */
-public class ChunkListingMap {
-
- private final Map<ChunkHash, Entry> mChunksByHash;
-
- /** Construct a map from a {@link ChunksMetadataProto.ChunkListing} protobuf */
- public static ChunkListingMap fromProto(ChunksMetadataProto.ChunkListing chunkListingProto) {
- Map<ChunkHash, Entry> entries = new HashMap<>();
-
- long start = 0;
-
- for (ChunksMetadataProto.Chunk chunk : chunkListingProto.chunks) {
- entries.put(new ChunkHash(chunk.hash), new Entry(start, chunk.length));
- start += chunk.length;
- }
-
- return new ChunkListingMap(entries);
- }
-
- private ChunkListingMap(Map<ChunkHash, Entry> chunksByHash) {
- // This is only called from the {@link #fromProto} method, so we don't
- // need to take a copy.
- this.mChunksByHash = chunksByHash;
- }
-
- /** Returns {@code true} if there is a chunk with the given SHA-256 MAC key in the listing. */
- public boolean hasChunk(ChunkHash hash) {
- return mChunksByHash.containsKey(hash);
- }
-
- /**
- * Returns the entry for the chunk with the given hash.
- *
- * @param hash The SHA-256 MAC of the plaintext of the chunk.
- * @return The entry, containing position and length of the chunk in the backup blob, or null if
- * it does not exist.
- */
- @Nullable
- public Entry getChunkEntry(ChunkHash hash) {
- return mChunksByHash.get(hash);
- }
-
- /** Information about a chunk entry in a backup blob - i.e., its position and length. */
- public static final class Entry {
- private final int mLength;
- private final long mStart;
-
- private Entry(long start, int length) {
- mLength = length;
- mStart = start;
- }
-
- /** Returns the length of the chunk in bytes. */
- public int getLength() {
- return mLength;
- }
-
- /** Returns the start position of the chunk in the backup blob, in bytes. */
- public long getStart() {
- return mStart;
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkOrderingType.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkOrderingType.java
deleted file mode 100644
index 9cda339..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/ChunkOrderingType.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunk;
-
-import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
-import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.EXPLICIT_STARTS;
-import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.INLINE_LENGTHS;
-
-import android.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/** IntDef corresponding to the ChunkOrderingType enum in the ChunksMetadataProto protobuf. */
-@IntDef({CHUNK_ORDERING_TYPE_UNSPECIFIED, EXPLICIT_STARTS, INLINE_LENGTHS})
-@Retention(RetentionPolicy.SOURCE)
-public @interface ChunkOrderingType {}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java
deleted file mode 100644
index edf1b9a..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunk;
-
-import java.util.Arrays;
-
-/**
- * Holds the bytes of an encrypted {@link ChunksMetadataProto.ChunkOrdering}.
- *
- * <p>TODO(b/116575321): After all code is ported, remove the factory method and rename
- * encryptedChunkOrdering() to getBytes().
- */
-public class EncryptedChunkOrdering {
- /**
- * Constructs a new object holding the given bytes of an encrypted {@link
- * ChunksMetadataProto.ChunkOrdering}.
- *
- * <p>Note that this just holds an ordering which is already encrypted, it does not encrypt the
- * ordering.
- */
- public static EncryptedChunkOrdering create(byte[] encryptedChunkOrdering) {
- return new EncryptedChunkOrdering(encryptedChunkOrdering);
- }
-
- private final byte[] mEncryptedChunkOrdering;
-
- /** Get the encrypted chunk ordering */
- public byte[] encryptedChunkOrdering() {
- return mEncryptedChunkOrdering;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof EncryptedChunkOrdering)) {
- return false;
- }
-
- EncryptedChunkOrdering encryptedChunkOrdering = (EncryptedChunkOrdering) o;
- return Arrays.equals(
- mEncryptedChunkOrdering, encryptedChunkOrdering.mEncryptedChunkOrdering);
- }
-
- @Override
- public int hashCode() {
- return Arrays.hashCode(mEncryptedChunkOrdering);
- }
-
- private EncryptedChunkOrdering(byte[] encryptedChunkOrdering) {
- mEncryptedChunkOrdering = encryptedChunkOrdering;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/BackupFileBuilder.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/BackupFileBuilder.java
deleted file mode 100644
index 4010bfd..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/BackupFileBuilder.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * 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.checkArgument;
-import static com.android.internal.util.Preconditions.checkState;
-
-import android.annotation.Nullable;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunk.ChunkListingMap;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * Writes batches of {@link EncryptedChunk} to a diff script, and generates the associated {@link
- * ChunksMetadataProto.ChunkListing} and {@link ChunksMetadataProto.ChunkOrdering}.
- */
-public class BackupFileBuilder {
- private static final String TAG = "BackupFileBuilder";
-
- private static final int BYTES_PER_KILOBYTE = 1024;
-
- private final BackupWriter mBackupWriter;
- private final EncryptedChunkEncoder mEncryptedChunkEncoder;
- private final ChunkListingMap mOldChunkListing;
- private final ChunksMetadataProto.ChunkListing mNewChunkListing;
- private final ChunksMetadataProto.ChunkOrdering mChunkOrdering;
- private final List<ChunksMetadataProto.Chunk> mKnownChunks = new ArrayList<>();
- private final List<Integer> mKnownStarts = new ArrayList<>();
- private final Map<ChunkHash, Long> mChunkStartPositions;
-
- private long mNewChunksSizeBytes;
- private boolean mFinished;
-
- /**
- * Constructs a new instance which writes raw data to the given {@link OutputStream}, without
- * generating a diff.
- *
- * <p>This class never closes the output stream.
- */
- public static BackupFileBuilder createForNonIncremental(OutputStream outputStream) {
- return new BackupFileBuilder(
- new RawBackupWriter(outputStream), new ChunksMetadataProto.ChunkListing());
- }
-
- /**
- * Constructs a new instance which writes a diff script to the given {@link OutputStream} using
- * a {@link SingleStreamDiffScriptWriter}.
- *
- * <p>This class never closes the output stream.
- *
- * @param oldChunkListing against which the diff will be generated.
- */
- public static BackupFileBuilder createForIncremental(
- OutputStream outputStream, ChunksMetadataProto.ChunkListing oldChunkListing) {
- return new BackupFileBuilder(
- DiffScriptBackupWriter.newInstance(outputStream), oldChunkListing);
- }
-
- private BackupFileBuilder(
- BackupWriter backupWriter, ChunksMetadataProto.ChunkListing oldChunkListing) {
- this.mBackupWriter = backupWriter;
- // TODO(b/77188289): Use InlineLengthsEncryptedChunkEncoder for key-value backups
- this.mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
- this.mOldChunkListing = ChunkListingMap.fromProto(oldChunkListing);
-
- mNewChunkListing = new ChunksMetadataProto.ChunkListing();
- mNewChunkListing.cipherType = ChunksMetadataProto.AES_256_GCM;
- mNewChunkListing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
-
- mChunkOrdering = new ChunksMetadataProto.ChunkOrdering();
- mChunkStartPositions = new HashMap<>();
- }
-
- /**
- * Writes the given chunks to the output stream, and adds them to the new chunk listing and
- * chunk ordering.
- *
- * <p>Sorts the chunks in lexicographical order before writing.
- *
- * @param allChunks The hashes of all the chunks, in the order they appear in the plaintext.
- * @param newChunks A map from hash to {@link EncryptedChunk} containing the new chunks not
- * present in the previous backup.
- */
- public void writeChunks(List<ChunkHash> allChunks, Map<ChunkHash, EncryptedChunk> newChunks)
- throws IOException {
- checkState(!mFinished, "Cannot write chunks after flushing.");
-
- List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
- Collections.sort(sortedChunks);
- for (ChunkHash chunkHash : sortedChunks) {
- // As we have already included this chunk in the backup file, don't add it again to
- // deduplicate identical chunks.
- if (!mChunkStartPositions.containsKey(chunkHash)) {
- // getBytesWritten() gives us the start of the chunk.
- mChunkStartPositions.put(chunkHash, mBackupWriter.getBytesWritten());
-
- writeChunkToFileAndListing(chunkHash, newChunks);
- }
- }
-
- long totalSizeKb = mBackupWriter.getBytesWritten() / BYTES_PER_KILOBYTE;
- long newChunksSizeKb = mNewChunksSizeBytes / BYTES_PER_KILOBYTE;
- Slog.d(
- TAG,
- "Total backup size: "
- + totalSizeKb
- + " kb, new chunks size: "
- + newChunksSizeKb
- + " kb");
-
- for (ChunkHash chunkHash : allChunks) {
- mKnownStarts.add(mChunkStartPositions.get(chunkHash).intValue());
- }
- }
-
- /**
- * Returns a new listing for all of the chunks written so far, setting the given fingerprint
- * mixer salt (this overrides the {@link ChunksMetadataProto.ChunkListing#fingerprintMixerSalt}
- * in the old {@link ChunksMetadataProto.ChunkListing} passed into the
- * {@link #BackupFileBuilder).
- */
- public ChunksMetadataProto.ChunkListing getNewChunkListing(
- @Nullable byte[] fingerprintMixerSalt) {
- // TODO: b/141537803 Add check to ensure this is called only once per instance
- mNewChunkListing.fingerprintMixerSalt =
- fingerprintMixerSalt != null
- ? Arrays.copyOf(fingerprintMixerSalt, fingerprintMixerSalt.length)
- : new byte[0];
- mNewChunkListing.chunks = mKnownChunks.toArray(new ChunksMetadataProto.Chunk[0]);
- return mNewChunkListing;
- }
-
- /** Returns a new ordering for all of the chunks written so far, setting the given checksum. */
- public ChunksMetadataProto.ChunkOrdering getNewChunkOrdering(byte[] checksum) {
- // TODO: b/141537803 Add check to ensure this is called only once per instance
- mChunkOrdering.starts = new int[mKnownStarts.size()];
- for (int i = 0; i < mKnownStarts.size(); i++) {
- mChunkOrdering.starts[i] = mKnownStarts.get(i).intValue();
- }
- mChunkOrdering.checksum = Arrays.copyOf(checksum, checksum.length);
- return mChunkOrdering;
- }
-
- /**
- * Finishes the backup file by writing the chunk metadata and metadata position.
- *
- * <p>Once this is called, calling {@link #writeChunks(List, Map)} will throw {@link
- * IllegalStateException}.
- */
- public void finish(ChunksMetadataProto.ChunksMetadata metadata) throws IOException {
- Objects.requireNonNull(metadata, "Metadata cannot be null");
-
- long startOfMetadata = mBackupWriter.getBytesWritten();
- mBackupWriter.writeBytes(ChunksMetadataProto.ChunksMetadata.toByteArray(metadata));
- mBackupWriter.writeBytes(toByteArray(startOfMetadata));
-
- mBackupWriter.flush();
- mFinished = true;
- }
-
- /**
- * Checks if the given chunk hash references an existing chunk or a new chunk, and adds this
- * chunk to the backup file and new chunk listing.
- */
- private void writeChunkToFileAndListing(
- ChunkHash chunkHash, Map<ChunkHash, EncryptedChunk> newChunks) throws IOException {
- Objects.requireNonNull(chunkHash, "Hash cannot be null");
-
- if (mOldChunkListing.hasChunk(chunkHash)) {
- ChunkListingMap.Entry oldChunk = mOldChunkListing.getChunkEntry(chunkHash);
- mBackupWriter.writeChunk(oldChunk.getStart(), oldChunk.getLength());
-
- checkArgument(oldChunk.getLength() >= 0, "Chunk must have zero or positive length");
- addChunk(chunkHash.getHash(), oldChunk.getLength());
- } else if (newChunks.containsKey(chunkHash)) {
- EncryptedChunk newChunk = newChunks.get(chunkHash);
- mEncryptedChunkEncoder.writeChunkToWriter(mBackupWriter, newChunk);
- int length = mEncryptedChunkEncoder.getEncodedLengthOfChunk(newChunk);
- mNewChunksSizeBytes += length;
-
- checkArgument(length >= 0, "Chunk must have zero or positive length");
- addChunk(chunkHash.getHash(), length);
- } else {
- throw new IllegalArgumentException(
- "Chunk did not exist in old chunks or new chunks: " + chunkHash);
- }
- }
-
- private void addChunk(byte[] chunkHash, int length) {
- ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
- chunk.hash = Arrays.copyOf(chunkHash, chunkHash.length);
- chunk.length = length;
- mKnownChunks.add(chunk);
- }
-
- private static byte[] toByteArray(long value) {
- // Note that this code needs to stay compatible with GWT, which has known
- // bugs when narrowing byte casts of long values occur.
- byte[] result = new byte[8];
- for (int i = 7; i >= 0; i--) {
- result[i] = (byte) (value & 0xffL);
- value >>= 8;
- }
- return result;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/BackupWriter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/BackupWriter.java
deleted file mode 100644
index baa820c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/BackupWriter.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import java.io.IOException;
-
-/** Writes backup data either as a diff script or as raw data, determined by the implementation. */
-public interface BackupWriter {
- /** Writes the given bytes to the output. */
- void writeBytes(byte[] bytes) throws IOException;
-
- /**
- * Writes an existing chunk from the previous backup to the output.
- *
- * <p>Note: not all implementations support this method.
- */
- void writeChunk(long start, int length) throws IOException;
-
- /** Returns the number of bytes written, included bytes copied from the old file. */
- long getBytesWritten();
-
- /**
- * Indicates that no more bytes or chunks will be written.
- *
- * <p>After calling this, you may not call {@link #writeBytes(byte[])} or {@link
- * #writeChunk(long, int)}
- */
- void flush() throws IOException;
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ByteRange.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ByteRange.java
deleted file mode 100644
index 004d9e3..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ByteRange.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.internal.util.Preconditions;
-
-/** Representation of a range of bytes to be downloaded. */
-final class ByteRange {
- private final long mStart;
- private final long mEnd;
-
- /** Creates a range of bytes which includes {@code mStart} and {@code mEnd}. */
- ByteRange(long start, long end) {
- Preconditions.checkArgument(start >= 0);
- Preconditions.checkArgument(end >= start);
- mStart = start;
- mEnd = end;
- }
-
- /** Returns the start of the {@code ByteRange}. The start is included in the range. */
- long getStart() {
- return mStart;
- }
-
- /** Returns the end of the {@code ByteRange}. The end is included in the range. */
- long getEnd() {
- return mEnd;
- }
-
- /** Returns the number of bytes included in the {@code ByteRange}. */
- int getLength() {
- return (int) (mEnd - mStart + 1);
- }
-
- /** Creates a new {@link ByteRange} from {@code mStart} to {@code mEnd + length}. */
- ByteRange extend(long length) {
- Preconditions.checkArgument(length > 0);
- return new ByteRange(mStart, mEnd + length);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof ByteRange)) {
- return false;
- }
-
- ByteRange byteRange = (ByteRange) o;
- return (mEnd == byteRange.mEnd && mStart == byteRange.mStart);
- }
-
- @Override
- public int hashCode() {
- int result = 17;
- result = 31 * result + (int) (mStart ^ (mStart >>> 32));
- result = 31 * result + (int) (mEnd ^ (mEnd >>> 32));
- return result;
- }
-
- @Override
- public String toString() {
- return String.format("ByteRange{mStart=%d, mEnd=%d}", mStart, mEnd);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ChunkEncryptor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ChunkEncryptor.java
deleted file mode 100644
index 48abc8c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ChunkEncryptor.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-
-/** Encrypts chunks of a file using AES/GCM. */
-public class ChunkEncryptor {
- 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 final SecretKey mSecretKey;
- private final SecureRandom mSecureRandom;
-
- /**
- * A new instance using {@code mSecretKey} to encrypt chunks and {@code mSecureRandom} to
- * generate nonces.
- */
- public ChunkEncryptor(SecretKey secretKey, SecureRandom secureRandom) {
- this.mSecretKey = secretKey;
- this.mSecureRandom = secureRandom;
- }
-
- /**
- * Transforms {@code plaintext} into an {@link EncryptedChunk}.
- *
- * @param plaintextHash The hash of the plaintext to encrypt, to attach as the key of the chunk.
- * @param plaintext Bytes to encrypt.
- * @throws InvalidKeyException If the given secret key is not a valid AES key for decryption.
- * @throws IllegalBlockSizeException If the input data cannot be encrypted using
- * AES/GCM/NoPadding. This should never be the case.
- */
- public EncryptedChunk encrypt(ChunkHash plaintextHash, byte[] plaintext)
- throws InvalidKeyException, IllegalBlockSizeException {
- byte[] nonce = generateNonce();
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- cipher.init(
- Cipher.ENCRYPT_MODE,
- mSecretKey,
- new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, nonce));
- } catch (NoSuchAlgorithmException
- | NoSuchPaddingException
- | InvalidAlgorithmParameterException e) {
- // This can not happen - AES/GCM/NoPadding is supported.
- throw new AssertionError(e);
- }
- byte[] encryptedBytes;
- try {
- encryptedBytes = cipher.doFinal(plaintext);
- } catch (BadPaddingException e) {
- // This can not happen - BadPaddingException can only be thrown in decrypt mode.
- throw new AssertionError("Impossible: threw BadPaddingException in encrypt mode.");
- }
-
- return EncryptedChunk.create(/*key=*/ plaintextHash, nonce, encryptedBytes);
- }
-
- private byte[] generateNonce() {
- byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
- mSecureRandom.nextBytes(nonce);
- return nonce;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ChunkHasher.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ChunkHasher.java
deleted file mode 100644
index 02d498c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ChunkHasher.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-
-/** Computes the SHA-256 HMAC of a chunk of bytes. */
-public class ChunkHasher {
- private static final String MAC_ALGORITHM = "HmacSHA256";
-
- private final SecretKey mSecretKey;
-
- /** Constructs a new hasher which computes the HMAC using the given secret key. */
- public ChunkHasher(SecretKey secretKey) {
- this.mSecretKey = secretKey;
- }
-
- /** Returns the SHA-256 over the given bytes. */
- public ChunkHash computeHash(byte[] plaintext) throws InvalidKeyException {
- try {
- Mac mac = Mac.getInstance(MAC_ALGORITHM);
- mac.init(mSecretKey);
- return new ChunkHash(mac.doFinal(plaintext));
- } catch (NoSuchAlgorithmException e) {
- // This can not happen - AES/GCM/NoPadding is available as part of the framework.
- throw new AssertionError(e);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/Chunker.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/Chunker.java
deleted file mode 100644
index c9a6293..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/Chunker.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-
-/** Splits an input stream into chunks, which are to be encrypted separately. */
-public interface Chunker {
- /**
- * Splits the input stream into chunks.
- *
- * @param inputStream The input stream.
- * @param chunkConsumer A function that processes each chunk as it is produced.
- * @throws IOException If there is a problem reading the input stream.
- * @throws GeneralSecurityException if the consumer function throws an error.
- */
- void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
- throws IOException, GeneralSecurityException;
-
- /** Function that consumes chunks. */
- interface ChunkConsumer {
- /**
- * Invoked for each chunk.
- *
- * @param chunk Plaintext bytes of chunk.
- * @throws GeneralSecurityException if there is an issue encrypting the chunk.
- */
- void accept(byte[] chunk) throws GeneralSecurityException;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java
deleted file mode 100644
index ae2e150..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.checkState;
-
-import android.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-/** Writes plaintext chunks to a file, building a digest of the plaintext of the resulting file. */
-public class DecryptedChunkFileOutput implements DecryptedChunkOutput {
- @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
-
- private final File mOutputFile;
- private final MessageDigest mMessageDigest;
- @Nullable private FileOutputStream mFileOutputStream;
- private boolean mClosed;
- @Nullable private byte[] mDigest;
-
- /**
- * Constructs a new instance which writes chunks to the given file and uses the default message
- * digest algorithm.
- */
- public DecryptedChunkFileOutput(File outputFile) {
- mOutputFile = outputFile;
- try {
- mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(
- "Impossible condition: JCE thinks it does not support AES.", e);
- }
- }
-
- @Override
- public DecryptedChunkOutput open() throws IOException {
- checkState(mFileOutputStream == null, "Cannot open twice");
- mFileOutputStream = new FileOutputStream(mOutputFile);
- return this;
- }
-
- @Override
- public void processChunk(byte[] plaintextBuffer, int length) throws IOException {
- checkState(mFileOutputStream != null, "Must open before processing chunks");
- mFileOutputStream.write(plaintextBuffer, /*off=*/ 0, length);
- mMessageDigest.update(plaintextBuffer, /*offset=*/ 0, length);
- }
-
- @Override
- public byte[] getDigest() {
- checkState(mClosed, "Must close before getting mDigest");
-
- // After the first call to mDigest() the MessageDigest is reset, thus we must store the
- // result.
- if (mDigest == null) {
- mDigest = mMessageDigest.digest();
- }
- return mDigest;
- }
-
- @Override
- public void close() throws IOException {
- mFileOutputStream.close();
- mClosed = true;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DiffScriptBackupWriter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DiffScriptBackupWriter.java
deleted file mode 100644
index 69fb5cb..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DiffScriptBackupWriter.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/** Writes backup data to a diff script, using a {@link SingleStreamDiffScriptWriter}. */
-public class DiffScriptBackupWriter implements BackupWriter {
- /**
- * The maximum size of a chunk in the diff script. The diff script writer {@code mWriter} will
- * buffer this many bytes in memory.
- */
- private static final int ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES = 1024 * 1024;
-
- private final SingleStreamDiffScriptWriter mWriter;
- private long mBytesWritten;
-
- /**
- * Constructs a new writer which writes the diff script to the given output stream, using the
- * maximum new chunk size {@code ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES}.
- */
- public static DiffScriptBackupWriter newInstance(OutputStream outputStream) {
- SingleStreamDiffScriptWriter writer =
- new SingleStreamDiffScriptWriter(
- outputStream, ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES);
- return new DiffScriptBackupWriter(writer);
- }
-
- @VisibleForTesting
- DiffScriptBackupWriter(SingleStreamDiffScriptWriter writer) {
- mWriter = writer;
- }
-
- @Override
- public void writeBytes(byte[] bytes) throws IOException {
- for (byte b : bytes) {
- mWriter.writeByte(b);
- }
-
- mBytesWritten += bytes.length;
- }
-
- @Override
- public void writeChunk(long start, int length) throws IOException {
- mWriter.writeChunk(start, length);
- mBytesWritten += length;
- }
-
- @Override
- public long getBytesWritten() {
- return mBytesWritten;
- }
-
- @Override
- public void flush() throws IOException {
- mWriter.flush();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DiffScriptWriter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DiffScriptWriter.java
deleted file mode 100644
index 49d1571..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/DiffScriptWriter.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/** Writer that formats a Diff Script and writes it to an output source. */
-interface DiffScriptWriter {
- /** Adds a new byte to the diff script. */
- void writeByte(byte b) throws IOException;
-
- /** Adds a known chunk to the diff script. */
- void writeChunk(long chunkStart, int chunkLength) throws IOException;
-
- /** Indicates that no more bytes or chunks will be added to the diff script. */
- void flush() throws IOException;
-
- interface Factory {
- DiffScriptWriter create(OutputStream outputStream);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/EncryptedChunk.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/EncryptedChunk.java
deleted file mode 100644
index cde59fa..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/EncryptedChunk.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.internal.util.Preconditions;
-import com.android.server.backup.encryption.chunk.ChunkHash;
-
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * A chunk of a file encrypted using AES/GCM.
- *
- * <p>TODO(b/116575321): After all code is ported, remove the factory method and rename
- * encryptedBytes(), key() and nonce().
- */
-public class EncryptedChunk {
- public static final int KEY_LENGTH_BYTES = ChunkHash.HASH_LENGTH_BYTES;
- public static final int NONCE_LENGTH_BYTES = 12;
-
- /**
- * Constructs a new instance with the given key, nonce, and encrypted bytes.
- *
- * @param key SHA-256 Hmac of the chunk plaintext.
- * @param nonce Nonce with which the bytes of the chunk were encrypted.
- * @param encryptedBytes Encrypted bytes of the chunk.
- */
- public static EncryptedChunk create(ChunkHash key, byte[] nonce, byte[] encryptedBytes) {
- Preconditions.checkArgument(
- nonce.length == NONCE_LENGTH_BYTES, "Nonce does not have the correct length.");
- return new EncryptedChunk(key, nonce, encryptedBytes);
- }
-
- private ChunkHash mKey;
- private byte[] mNonce;
- private byte[] mEncryptedBytes;
-
- private EncryptedChunk(ChunkHash key, byte[] nonce, byte[] encryptedBytes) {
- mKey = key;
- mNonce = nonce;
- mEncryptedBytes = encryptedBytes;
- }
-
- /** The SHA-256 Hmac of the plaintext bytes of the chunk. */
- public ChunkHash key() {
- return mKey;
- }
-
- /** The nonce with which the chunk was encrypted. */
- public byte[] nonce() {
- return mNonce;
- }
-
- /** The encrypted bytes of the chunk. */
- public byte[] encryptedBytes() {
- return mEncryptedBytes;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof EncryptedChunk)) {
- return false;
- }
-
- EncryptedChunk encryptedChunkOrdering = (EncryptedChunk) o;
- return Arrays.equals(mEncryptedBytes, encryptedChunkOrdering.mEncryptedBytes)
- && Arrays.equals(mNonce, encryptedChunkOrdering.mNonce)
- && mKey.equals(encryptedChunkOrdering.mKey);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mKey, Arrays.hashCode(mNonce), Arrays.hashCode(mEncryptedBytes));
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/EncryptedChunkEncoder.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/EncryptedChunkEncoder.java
deleted file mode 100644
index 16beda3..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/EncryptedChunkEncoder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.server.backup.encryption.chunk.ChunkOrderingType;
-
-import java.io.IOException;
-
-/** Encodes an {@link EncryptedChunk} as bytes to write to the encrypted backup file. */
-public interface EncryptedChunkEncoder {
- /**
- * Encodes the given chunk and asks the writer to write it.
- *
- * <p>The chunk will be encoded in the format [nonce]+[encrypted data].
- *
- * <p>TODO(b/116575321): Choose a more descriptive method name after the code move is done.
- */
- void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException;
-
- /**
- * Returns the length in bytes that this chunk would be if encoded with {@link
- * #writeChunkToWriter}.
- */
- int getEncodedLengthOfChunk(EncryptedChunk chunk);
-
- /**
- * Returns the {@link ChunkOrderingType} that must be included in the backup file, when using
- * this decoder, so that the file may be correctly decoded.
- */
- @ChunkOrderingType
- int getChunkOrderingType();
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoder.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoder.java
deleted file mode 100644
index 6b9be9f..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoder.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.server.backup.encryption.chunk.ChunkOrderingType;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import java.io.IOException;
-
-/**
- * Encodes an {@link EncryptedChunk} as bytes, prepending the length of the chunk.
- *
- * <p>This allows us to decode the backup file during restore without any extra information about
- * the boundaries of the chunks. The backup file should contain a chunk ordering in mode {@link
- * ChunksMetadataProto#INLINE_LENGTHS}.
- *
- * <p>We use this implementation during key value backup.
- */
-public class InlineLengthsEncryptedChunkEncoder implements EncryptedChunkEncoder {
- public static final int BYTES_LENGTH = Integer.SIZE / Byte.SIZE;
-
- private final LengthlessEncryptedChunkEncoder mLengthlessEncryptedChunkEncoder =
- new LengthlessEncryptedChunkEncoder();
-
- @Override
- public void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException {
- int length = mLengthlessEncryptedChunkEncoder.getEncodedLengthOfChunk(chunk);
- writer.writeBytes(toByteArray(length));
- mLengthlessEncryptedChunkEncoder.writeChunkToWriter(writer, chunk);
- }
-
- @Override
- public int getEncodedLengthOfChunk(EncryptedChunk chunk) {
- return BYTES_LENGTH + mLengthlessEncryptedChunkEncoder.getEncodedLengthOfChunk(chunk);
- }
-
- @Override
- @ChunkOrderingType
- public int getChunkOrderingType() {
- return ChunksMetadataProto.INLINE_LENGTHS;
- }
-
- /**
- * Returns a big-endian representation of {@code value} in a 4-element byte array; equivalent to
- * {@code ByteBuffer.allocate(4).putInt(value).array()}. For example, the input value {@code
- * 0x12131415} would yield the byte array {@code {0x12, 0x13, 0x14, 0x15}}.
- *
- * <p>Equivalent to guava's Ints.toByteArray.
- */
- static byte[] toByteArray(int value) {
- return new byte[] {
- (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value
- };
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoder.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoder.java
deleted file mode 100644
index e707350..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoder.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import com.android.server.backup.encryption.chunk.ChunkOrderingType;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import java.io.IOException;
-
-/**
- * Encodes an {@link EncryptedChunk} as bytes without including any information about the length of
- * the chunk.
- *
- * <p>In order for us to decode the backup file during restore it must include a chunk ordering in
- * mode {@link ChunksMetadataProto#EXPLICIT_STARTS}, which contains the boundaries of the chunks in
- * the encrypted file. This information allows us to decode the backup file and divide it into
- * chunks without including the length of each chunk inline.
- *
- * <p>We use this implementation during full backup.
- */
-public class LengthlessEncryptedChunkEncoder implements EncryptedChunkEncoder {
- @Override
- public void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException {
- writer.writeBytes(chunk.nonce());
- writer.writeBytes(chunk.encryptedBytes());
- }
-
- @Override
- public int getEncodedLengthOfChunk(EncryptedChunk chunk) {
- return chunk.nonce().length + chunk.encryptedBytes().length;
- }
-
- @Override
- @ChunkOrderingType
- public int getChunkOrderingType() {
- return ChunksMetadataProto.EXPLICIT_STARTS;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/OutputStreamWrapper.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/OutputStreamWrapper.java
deleted file mode 100644
index 4aea601..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/OutputStreamWrapper.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import java.io.OutputStream;
-
-/** An interface that wraps one {@link OutputStream} with another for filtration purposes. */
-public interface OutputStreamWrapper {
- /** Wraps a given {@link OutputStream}. */
- OutputStream wrap(OutputStream outputStream);
-}
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
deleted file mode 100644
index b0a562c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * 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 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.Objects;
-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 = Objects.requireNonNull(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 {
- Objects.requireNonNull(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/chunking/RawBackupWriter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/RawBackupWriter.java
deleted file mode 100644
index b211b0f..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/RawBackupWriter.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/** Writes data straight to an output stream. */
-public class RawBackupWriter implements BackupWriter {
- private final OutputStream mOutputStream;
- private long mBytesWritten;
-
- /** Constructs a new writer which writes bytes to the given output stream. */
- public RawBackupWriter(OutputStream outputStream) {
- this.mOutputStream = outputStream;
- }
-
- @Override
- public void writeBytes(byte[] bytes) throws IOException {
- mOutputStream.write(bytes);
- mBytesWritten += bytes.length;
- }
-
- @Override
- public void writeChunk(long start, int length) throws IOException {
- throw new UnsupportedOperationException("RawBackupWriter cannot write existing chunks");
- }
-
- @Override
- public long getBytesWritten() {
- return mBytesWritten;
- }
-
- @Override
- public void flush() throws IOException {
- mOutputStream.flush();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriter.java
deleted file mode 100644
index 0e4bd58..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriter.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import android.annotation.Nullable;
-
-import com.android.internal.util.Preconditions;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.util.Locale;
-
-/**
- * A {@link DiffScriptWriter} that writes an entire diff script to a single {@link OutputStream}.
- */
-public class SingleStreamDiffScriptWriter implements DiffScriptWriter {
- static final byte LINE_SEPARATOR = 0xA;
- private static final Charset UTF_8 = Charset.forName("UTF-8");
-
- private final int mMaxNewByteChunkSize;
- private final OutputStream mOutputStream;
- private final byte[] mByteBuffer;
- private int mBufferSize = 0;
- // Each chunk could be written immediately to the output stream. However,
- // it is possible that chunks may overlap. We therefore cache the most recent
- // reusable chunk and try to merge it with future chunks.
- private ByteRange mReusableChunk;
-
- public SingleStreamDiffScriptWriter(OutputStream outputStream, int maxNewByteChunkSize) {
- mOutputStream = outputStream;
- mMaxNewByteChunkSize = maxNewByteChunkSize;
- mByteBuffer = new byte[maxNewByteChunkSize];
- }
-
- @Override
- public void writeByte(byte b) throws IOException {
- if (mReusableChunk != null) {
- writeReusableChunk();
- }
- mByteBuffer[mBufferSize++] = b;
- if (mBufferSize == mMaxNewByteChunkSize) {
- writeByteBuffer();
- }
- }
-
- @Override
- public void writeChunk(long chunkStart, int chunkLength) throws IOException {
- Preconditions.checkArgument(chunkStart >= 0);
- Preconditions.checkArgument(chunkLength > 0);
- if (mBufferSize != 0) {
- writeByteBuffer();
- }
-
- if (mReusableChunk != null && mReusableChunk.getEnd() + 1 == chunkStart) {
- // The new chunk overlaps the old, so combine them into a single byte range.
- mReusableChunk = mReusableChunk.extend(chunkLength);
- } else {
- writeReusableChunk();
- mReusableChunk = new ByteRange(chunkStart, chunkStart + chunkLength - 1);
- }
- }
-
- @Override
- public void flush() throws IOException {
- Preconditions.checkState(!(mBufferSize != 0 && mReusableChunk != null));
- if (mBufferSize != 0) {
- writeByteBuffer();
- }
- if (mReusableChunk != null) {
- writeReusableChunk();
- }
- mOutputStream.flush();
- }
-
- private void writeByteBuffer() throws IOException {
- mOutputStream.write(Integer.toString(mBufferSize).getBytes(UTF_8));
- mOutputStream.write(LINE_SEPARATOR);
- mOutputStream.write(mByteBuffer, 0, mBufferSize);
- mOutputStream.write(LINE_SEPARATOR);
- mBufferSize = 0;
- }
-
- private void writeReusableChunk() throws IOException {
- if (mReusableChunk != null) {
- mOutputStream.write(
- String.format(
- Locale.US,
- "%d-%d",
- mReusableChunk.getStart(),
- mReusableChunk.getEnd())
- .getBytes(UTF_8));
- mOutputStream.write(LINE_SEPARATOR);
- mReusableChunk = null;
- }
- }
-
- /** A factory that creates {@link SingleStreamDiffScriptWriter}s. */
- public static class Factory implements DiffScriptWriter.Factory {
- private final int mMaxNewByteChunkSize;
- private final OutputStreamWrapper mOutputStreamWrapper;
-
- public Factory(int maxNewByteChunkSize, @Nullable OutputStreamWrapper outputStreamWrapper) {
- mMaxNewByteChunkSize = maxNewByteChunkSize;
- mOutputStreamWrapper = outputStreamWrapper;
- }
-
- @Override
- public SingleStreamDiffScriptWriter create(OutputStream outputStream) {
- if (mOutputStreamWrapper != null) {
- outputStream = mOutputStreamWrapper.wrap(outputStream);
- }
- return new SingleStreamDiffScriptWriter(outputStream, mMaxNewByteChunkSize);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunker.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunker.java
deleted file mode 100644
index 18011f6..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunker.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import com.android.server.backup.encryption.chunking.Chunker;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/** Splits a stream of bytes into variable-sized chunks, using content-defined chunking. */
-public class ContentDefinedChunker implements Chunker {
- private static final int WINDOW_SIZE = 31;
- private static final byte DEFAULT_OUT_BYTE = (byte) 0;
-
- private final byte[] mChunkBuffer;
- private final RabinFingerprint64 mRabinFingerprint64;
- private final FingerprintMixer mFingerprintMixer;
- private final BreakpointPredicate mBreakpointPredicate;
- private final int mMinChunkSize;
- private final int mMaxChunkSize;
-
- /**
- * Constructor.
- *
- * @param minChunkSize The minimum size of a chunk. No chunk will be produced of a size smaller
- * than this except possibly at the very end of the stream.
- * @param maxChunkSize The maximum size of a chunk. No chunk will be produced of a larger size.
- * @param rabinFingerprint64 Calculates fingerprints, with which to determine breakpoints.
- * @param breakpointPredicate Given a Rabin fingerprint, returns whether this ought to be a
- * breakpoint.
- */
- public ContentDefinedChunker(
- int minChunkSize,
- int maxChunkSize,
- RabinFingerprint64 rabinFingerprint64,
- FingerprintMixer fingerprintMixer,
- BreakpointPredicate breakpointPredicate) {
- checkArgument(
- minChunkSize >= WINDOW_SIZE,
- "Minimum chunk size must be greater than window size.");
- checkArgument(
- maxChunkSize >= minChunkSize,
- "Maximum chunk size cannot be smaller than minimum chunk size.");
- mChunkBuffer = new byte[maxChunkSize];
- mRabinFingerprint64 = rabinFingerprint64;
- mBreakpointPredicate = breakpointPredicate;
- mFingerprintMixer = fingerprintMixer;
- mMinChunkSize = minChunkSize;
- mMaxChunkSize = maxChunkSize;
- }
-
- /**
- * Breaks the input stream into variable-sized chunks.
- *
- * @param inputStream The input bytes to break into chunks.
- * @param chunkConsumer A function to process each chunk as it's generated.
- * @throws IOException Thrown if there is an issue reading from the input stream.
- * @throws GeneralSecurityException Thrown if the {@link ChunkConsumer} throws it.
- */
- @Override
- public void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
- throws IOException, GeneralSecurityException {
- int chunkLength;
- int initialReadLength = mMinChunkSize - WINDOW_SIZE;
-
- // Performance optimization - there is no reason to calculate fingerprints for windows
- // ending before the minimum chunk size.
- while ((chunkLength =
- inputStream.read(mChunkBuffer, /*off=*/ 0, /*len=*/ initialReadLength))
- != -1) {
- int b;
- long fingerprint = 0L;
-
- while ((b = inputStream.read()) != -1) {
- byte inByte = (byte) b;
- byte outByte = getCurrentWindowStartByte(chunkLength);
- mChunkBuffer[chunkLength++] = inByte;
-
- fingerprint =
- mRabinFingerprint64.computeFingerprint64(inByte, outByte, fingerprint);
-
- if (chunkLength >= mMaxChunkSize
- || (chunkLength >= mMinChunkSize
- && mBreakpointPredicate.isBreakpoint(
- mFingerprintMixer.mix(fingerprint)))) {
- chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
- chunkLength = 0;
- break;
- }
- }
-
- if (chunkLength > 0) {
- chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
- }
- }
- }
-
- private byte getCurrentWindowStartByte(int chunkLength) {
- if (chunkLength < mMinChunkSize) {
- return DEFAULT_OUT_BYTE;
- } else {
- return mChunkBuffer[chunkLength - WINDOW_SIZE];
- }
- }
-
- /** Whether the current fingerprint indicates the end of a chunk. */
- public interface BreakpointPredicate {
-
- /**
- * Returns {@code true} if the fingerprint of the last {@code WINDOW_SIZE} bytes indicates
- * the chunk ought to end at this position.
- *
- * @param fingerprint Fingerprint of the last {@code WINDOW_SIZE} bytes.
- * @return Whether this ought to be a chunk breakpoint.
- */
- boolean isBreakpoint(long fingerprint);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/FingerprintMixer.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/FingerprintMixer.java
deleted file mode 100644
index e9f3050..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/FingerprintMixer.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-
-import javax.crypto.SecretKey;
-
-/**
- * Helper for mixing fingerprint with key material.
- *
- * <p>We do this as otherwise the Rabin fingerprint leaks information about the plaintext. i.e., if
- * two users have the same file, it will be partitioned by Rabin in the same way, allowing us to
- * infer that it is the same as another user's file.
- *
- * <p>By mixing the fingerprint with the user's secret key, the chunking method is different on a
- * per key basis. Each application has its own {@link SecretKey}, so we cannot infer that a file is
- * the same even across multiple applications owned by the same user, never mind across multiple
- * users.
- *
- * <p>Instead of directly mixing the fingerprint with the user's secret, we first securely and
- * deterministically derive a secondary chunking key. As Rabin is not a cryptographically secure
- * hash, it might otherwise leak information about the user's secret. This prevents that from
- * happening.
- */
-public class FingerprintMixer {
- public static final int SALT_LENGTH_BYTES = 256 / Byte.SIZE;
- private static final String DERIVED_KEY_NAME = "RabinFingerprint64Mixer";
-
- private final long mAddend;
- private final long mMultiplicand;
-
- /**
- * A new instance from a given secret key and salt. Salt must be the same across incremental
- * backups, or a different chunking strategy will be used each time, defeating the dedup.
- *
- * @param secretKey The application-specific secret.
- * @param salt The salt.
- * @throws InvalidKeyException If the encoded form of {@code secretKey} is inaccessible.
- */
- public FingerprintMixer(SecretKey secretKey, byte[] salt) throws InvalidKeyException {
- checkArgument(salt.length == SALT_LENGTH_BYTES, "Requires a 256-bit salt.");
- byte[] keyBytes = secretKey.getEncoded();
- if (keyBytes == null) {
- throw new InvalidKeyException("SecretKey must support encoding for FingerprintMixer.");
- }
- byte[] derivedKey =
- Hkdf.hkdf(keyBytes, salt, DERIVED_KEY_NAME.getBytes(StandardCharsets.UTF_8));
- ByteBuffer buffer = ByteBuffer.wrap(derivedKey);
- mAddend = buffer.getLong();
- // Multiplicand must be odd - otherwise we lose some bits of the Rabin fingerprint when
- // mixing
- mMultiplicand = buffer.getLong() | 1;
- }
-
- /**
- * Mixes the fingerprint with the derived key material. This is performed by adding part of the
- * derived key and multiplying by another part of the derived key (which is forced to be odd, so
- * that the operation is reversible).
- *
- * @param fingerprint A 64-bit Rabin fingerprint.
- * @return The mixed fingerprint.
- */
- long mix(long fingerprint) {
- return ((fingerprint + mAddend) * mMultiplicand);
- }
-
- /** The addend part of the derived key. */
- long getAddend() {
- return mAddend;
- }
-
- /** The multiplicand part of the derived key. */
- long getMultiplicand() {
- return mMultiplicand;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/Hkdf.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/Hkdf.java
deleted file mode 100644
index d0776ae..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/Hkdf.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Objects;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * Secure HKDF utils. Allows client to deterministically derive additional key material from a base
- * secret. If the derived key material is compromised, this does not in of itself compromise the
- * root secret.
- *
- * <p>TODO(b/116575321): After all code is ported, rename this class to HkdfUtils.
- */
-public final class Hkdf {
- private static final byte[] CONSTANT_01 = {0x01};
- private static final String HmacSHA256 = "HmacSHA256";
- private static final String AES = "AES";
-
- /**
- * Implements HKDF (RFC 5869) with the SHA-256 hash and a 256-bit output key length.
- *
- * <p>IMPORTANT: The use or edit of this method requires a security review.
- *
- * @param mainKey Main key from which to derive sub-keys.
- * @param salt A randomly generated 256-bit byte string.
- * @param data Arbitrary information that is bound to the derived key (i.e., used in its
- * creation).
- * @return Raw derived key bytes = HKDF-SHA256(mainKey, salt, data).
- * @throws InvalidKeyException If the salt can not be used as a valid key.
- */
- static byte[] hkdf(byte[] mainKey, byte[] salt, byte[] data) throws InvalidKeyException {
- Objects.requireNonNull(mainKey, "HKDF requires main key to be set.");
- Objects.requireNonNull(salt, "HKDF requires a salt.");
- Objects.requireNonNull(data, "No data provided to HKDF.");
- return hkdfSha256Expand(hkdfSha256Extract(mainKey, salt), data);
- }
-
- private Hkdf() {}
-
- /**
- * The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. This function is
- * used to pre-process the {@code inputKeyMaterial} and mix it with the {@code salt}, producing
- * output suitable for use with HKDF expansion function (which produces the actual derived key).
- *
- * <p>IMPORTANT: The use or edit of this method requires a security review.
- *
- * @see #hkdfSha256Expand(byte[], byte[])
- * @return HMAC-SHA256(salt, inputKeyMaterial) (salt is the "key" for the HMAC)
- * @throws InvalidKeyException If the salt can not be used as a valid key.
- */
- private static byte[] hkdfSha256Extract(byte[] inputKeyMaterial, byte[] salt)
- throws InvalidKeyException {
- // Note that the SecretKey encoding format is defined to be RAW, so the encoded form should
- // be consistent across implementations.
- Mac sha256;
- try {
- sha256 = Mac.getInstance(HmacSHA256);
- } catch (NoSuchAlgorithmException e) {
- // This can not happen - HmacSHA256 is supported by the platform.
- throw new AssertionError(e);
- }
- sha256.init(new SecretKeySpec(salt, AES));
-
- return sha256.doFinal(inputKeyMaterial);
- }
-
- /**
- * Special case of HKDF (RFC 5869) expansion function, using the SHA-256 hash function and
- * allowing for a maximum output length of 256 bits.
- *
- * <p>IMPORTANT: The use or edit of this method requires a security review.
- *
- * @param pseudoRandomKey Generated by {@link #hkdfSha256Extract(byte[], byte[])}.
- * @param info Arbitrary information the derived key should be bound to.
- * @return Raw derived key bytes = HMAC-SHA256(pseudoRandomKey, info | 0x01).
- * @throws InvalidKeyException If the salt can not be used as a valid key.
- */
- private static byte[] hkdfSha256Expand(byte[] pseudoRandomKey, byte[] info)
- throws InvalidKeyException {
- // Note that RFC 5869 computes number of blocks N = ceil(hash length / output length), but
- // here we only deal with a 256 bit hash up to a 256 bit output, yielding N=1.
- Mac sha256;
- try {
- sha256 = Mac.getInstance(HmacSHA256);
- } catch (NoSuchAlgorithmException e) {
- // This can not happen - HmacSHA256 is supported by the platform.
- throw new AssertionError(e);
- }
- sha256.init(new SecretKeySpec(pseudoRandomKey, AES));
-
- sha256.update(info);
- sha256.update(CONSTANT_01);
- return sha256.doFinal();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpoint.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpoint.java
deleted file mode 100644
index e867e7c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpoint.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker.BreakpointPredicate;
-
-/**
- * Function to determine whether a 64-bit fingerprint ought to be a chunk breakpoint.
- *
- * <p>This works by checking whether there are at least n leading zeros in the fingerprint. n is
- * calculated to on average cause a breakpoint after a given number of trials (provided in the
- * constructor). This allows us to choose a number of trials that gives a desired average chunk
- * size. This works because the fingerprint is pseudo-randomly distributed.
- */
-public class IsChunkBreakpoint implements BreakpointPredicate {
- private final int mLeadingZeros;
- private final long mBitmask;
-
- /**
- * A new instance that causes a breakpoint after a given number of trials on average.
- *
- * @param averageNumberOfTrialsUntilBreakpoint The number of trials after which on average to
- * create a new chunk. If this is not a power of 2, some precision is sacrificed (i.e., on
- * average, breaks will actually happen after the nearest power of 2 to the average number
- * of trials passed in).
- */
- public IsChunkBreakpoint(long averageNumberOfTrialsUntilBreakpoint) {
- checkArgument(
- averageNumberOfTrialsUntilBreakpoint >= 0,
- "Average number of trials must be non-negative");
-
- // Want n leading zeros after t trials.
- // P(leading zeros = n) = 1/2^n
- // Expected num trials to get n leading zeros = 1/2^-n
- // t = 1/2^-n
- // n = log2(t)
- mLeadingZeros = (int) Math.round(log2(averageNumberOfTrialsUntilBreakpoint));
- mBitmask = ~(~0L >>> mLeadingZeros);
- }
-
- /**
- * Returns {@code true} if {@code fingerprint} indicates that there should be a chunk
- * breakpoint.
- */
- @Override
- public boolean isBreakpoint(long fingerprint) {
- return (fingerprint & mBitmask) == 0;
- }
-
- /** Returns the number of leading zeros in the fingerprint that causes a breakpoint. */
- public int getLeadingZeros() {
- return mLeadingZeros;
- }
-
- /**
- * Calculates log base 2 of x. Not the most efficient possible implementation, but it's simple,
- * obviously correct, and is only invoked on object construction.
- */
- private static double log2(double x) {
- return Math.log(x) / Math.log(2);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64.java
deleted file mode 100644
index 1e14ffa..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-/** Helper to calculate a 64-bit Rabin fingerprint over a 31-byte window. */
-public class RabinFingerprint64 {
- private static final long DEFAULT_IRREDUCIBLE_POLYNOMIAL_64 = 0x000000000000001BL;
- private static final int POLYNOMIAL_DEGREE = 64;
- private static final int SLIDING_WINDOW_SIZE_BYTES = 31;
-
- private final long mPoly64;
- // Auxiliary tables to speed up the computation of Rabin fingerprints.
- private final long[] mTableFP64 = new long[256];
- private final long[] mTableOutByte = new long[256];
-
- /**
- * Constructs a new instance over the given irreducible 64-degree polynomial. It is up to the
- * caller to determine that the polynomial is irreducible. If it is not the fingerprinting will
- * not behave as expected.
- *
- * @param poly64 The polynomial.
- */
- public RabinFingerprint64(long poly64) {
- mPoly64 = poly64;
- }
-
- /** Constructs a new instance using {@code x^64 + x^4 + x + 1} as the irreducible polynomial. */
- public RabinFingerprint64() {
- this(DEFAULT_IRREDUCIBLE_POLYNOMIAL_64);
- computeFingerprintTables64();
- computeFingerprintTables64Windowed();
- }
-
- /**
- * Computes the fingerprint for the new sliding window given the fingerprint of the previous
- * sliding window, the byte sliding in, and the byte sliding out.
- *
- * @param inChar The new char coming into the sliding window.
- * @param outChar The left most char sliding out of the window.
- * @param fingerPrint Fingerprint for previous window.
- * @return New fingerprint for the new sliding window.
- */
- public long computeFingerprint64(byte inChar, byte outChar, long fingerPrint) {
- return (fingerPrint << 8)
- ^ (inChar & 0xFF)
- ^ mTableFP64[(int) (fingerPrint >>> 56)]
- ^ mTableOutByte[outChar & 0xFF];
- }
-
- /** Compute auxiliary tables to speed up the fingerprint computation. */
- private void computeFingerprintTables64() {
- long[] degreesRes64 = new long[POLYNOMIAL_DEGREE];
- degreesRes64[0] = mPoly64;
- for (int i = 1; i < POLYNOMIAL_DEGREE; i++) {
- if ((degreesRes64[i - 1] & (1L << 63)) == 0) {
- degreesRes64[i] = degreesRes64[i - 1] << 1;
- } else {
- degreesRes64[i] = (degreesRes64[i - 1] << 1) ^ mPoly64;
- }
- }
- for (int i = 0; i < 256; i++) {
- int currIndex = i;
- for (int j = 0; (currIndex > 0) && (j < 8); j++) {
- if ((currIndex & 0x1) == 1) {
- mTableFP64[i] ^= degreesRes64[j];
- }
- currIndex >>>= 1;
- }
- }
- }
-
- /**
- * Compute auxiliary table {@code mTableOutByte} to facilitate the computing of fingerprints for
- * sliding windows. This table is to take care of the effect on the fingerprint when the
- * leftmost byte in the window slides out.
- */
- private void computeFingerprintTables64Windowed() {
- // Auxiliary array degsRes64[8] defined by: <code>degsRes64[i] = x^(8 *
- // SLIDING_WINDOW_SIZE_BYTES + i) mod this.mPoly64.</code>
- long[] degsRes64 = new long[8];
- degsRes64[0] = mPoly64;
- for (int i = 65; i < 8 * (SLIDING_WINDOW_SIZE_BYTES + 1); i++) {
- if ((degsRes64[(i - 1) % 8] & (1L << 63)) == 0) {
- degsRes64[i % 8] = degsRes64[(i - 1) % 8] << 1;
- } else {
- degsRes64[i % 8] = (degsRes64[(i - 1) % 8] << 1) ^ mPoly64;
- }
- }
- for (int i = 0; i < 256; i++) {
- int currIndex = i;
- for (int j = 0; (currIndex > 0) && (j < 8); j++) {
- if ((currIndex & 0x1) == 1) {
- mTableOutByte[i] ^= degsRes64[j];
- }
- currIndex >>>= 1;
- }
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/client/CryptoBackupServer.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/client/CryptoBackupServer.java
deleted file mode 100644
index d7f7dc7..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/client/CryptoBackupServer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.client;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.util.Map;
-
-/**
- * Contains methods for communicating with the parts of the backup server relevant to encryption.
- */
-public interface CryptoBackupServer {
- /**
- * Uploads an incremental backup to the server.
- *
- * <p>Handles setting up and tearing down the connection.
- *
- * @param packageName the package to associate the data with
- * @param oldDocId the id of the previous backup doc in Drive
- * @param diffScript containing the actual backup data
- * @param tertiaryKey the wrapped key used to encrypt this backup
- * @return the id of the new backup doc in Drive.
- */
- String uploadIncrementalBackup(
- String packageName,
- String oldDocId,
- byte[] diffScript,
- WrappedKeyProto.WrappedKey tertiaryKey);
-
- /**
- * Uploads non-incremental backup to the server.
- *
- * <p>Handles setting up and tearing down the connection.
- *
- * @param packageName the package to associate the data with
- * @param data the actual backup data
- * @param tertiaryKey the wrapped key used to encrypt this backup
- * @return the id of the new backup doc in Drive.
- */
- String uploadNonIncrementalBackup(
- String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey);
-
- /**
- * Sets the alias of the active secondary key. This is the alias used to refer to the key in the
- * {@link java.security.KeyStore}. It is also used to key storage for tertiary keys on the
- * backup server. Also has to upload all existing tertiary keys, wrapped with the new key.
- *
- * @param keyAlias The ID of the secondary key.
- * @param tertiaryKeys The tertiary keys, wrapped with the new secondary key.
- */
- void setActiveSecondaryKeyAlias(
- String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys);
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java
deleted file mode 100644
index 9e31385..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.client;
-
-/**
- * Error thrown when the user attempts to retrieve a key set from the server, but is asking for keys
- * from an inactive secondary.
- *
- * <p>Although we could just return old keys, there is no good reason to do this. It almost
- * certainly indicates a logic error on the client.
- */
-public class UnexpectedActiveSecondaryOnServerException extends Exception {
- public UnexpectedActiveSecondaryOnServerException(String message) {
- super(message);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/KeyWrapUtils.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/KeyWrapUtils.java
deleted file mode 100644
index a043c1f..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/KeyWrapUtils.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.keys;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Locale;
-
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-
-/** Utility functions for wrapping and unwrapping tertiary keys. */
-public class KeyWrapUtils {
- private static final String AES_GCM_MODE = "AES/GCM/NoPadding";
- private static final int GCM_TAG_LENGTH_BYTES = 16;
- private static final int BITS_PER_BYTE = 8;
- private static final int GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE;
- private static final String KEY_ALGORITHM = "AES";
-
- /**
- * Uses the secondary key to unwrap the wrapped tertiary key.
- *
- * @param secondaryKey The secondary key used to wrap the tertiary key.
- * @param wrappedKey The wrapped tertiary key.
- * @return The unwrapped tertiary key.
- * @throws InvalidKeyException if the provided secondary key cannot unwrap the tertiary key.
- */
- public static SecretKey unwrap(SecretKey secondaryKey, WrappedKeyProto.WrappedKey wrappedKey)
- throws InvalidKeyException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, NoSuchPaddingException {
- if (wrappedKey.wrapAlgorithm != WrappedKeyProto.WrappedKey.AES_256_GCM) {
- throw new InvalidKeyException(
- String.format(
- Locale.US,
- "Could not unwrap key wrapped with %s algorithm",
- wrappedKey.wrapAlgorithm));
- }
-
- if (wrappedKey.metadata == null) {
- throw new InvalidKeyException("Metadata missing from wrapped tertiary key.");
- }
-
- if (wrappedKey.metadata.type != WrappedKeyProto.KeyMetadata.AES_256_GCM) {
- throw new InvalidKeyException(
- String.format(
- Locale.US,
- "Wrapped key was unexpected %s algorithm. Only support"
- + " AES/GCM/NoPadding.",
- wrappedKey.metadata.type));
- }
-
- Cipher cipher = getCipher();
-
- cipher.init(
- Cipher.UNWRAP_MODE,
- secondaryKey,
- new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.nonce));
-
- return (SecretKey) cipher.unwrap(wrappedKey.key, KEY_ALGORITHM, Cipher.SECRET_KEY);
- }
-
- /**
- * Wraps the tertiary key with the secondary key.
- *
- * @param secondaryKey The secondary key to use for wrapping.
- * @param tertiaryKey The key to wrap.
- * @return The wrapped key.
- * @throws InvalidKeyException if the key is not good for wrapping.
- * @throws IllegalBlockSizeException if there is an issue wrapping.
- */
- public static WrappedKeyProto.WrappedKey wrap(SecretKey secondaryKey, SecretKey tertiaryKey)
- throws InvalidKeyException, IllegalBlockSizeException, NoSuchAlgorithmException,
- NoSuchPaddingException {
- Cipher cipher = getCipher();
- cipher.init(Cipher.WRAP_MODE, secondaryKey);
-
- WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
- wrappedKey.key = cipher.wrap(tertiaryKey);
- wrappedKey.nonce = cipher.getIV();
- wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.AES_256_GCM;
- wrappedKey.metadata = new WrappedKeyProto.KeyMetadata();
- wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.AES_256_GCM;
- return wrappedKey;
- }
-
- /**
- * Rewraps a tertiary key with a new secondary key.
- *
- * @param oldSecondaryKey The old secondary key, used to unwrap the tertiary key.
- * @param newSecondaryKey The new secondary key, used to rewrap the tertiary key.
- * @param tertiaryKey The tertiary key, wrapped by {@code oldSecondaryKey}.
- * @return The tertiary key, wrapped by {@code newSecondaryKey}.
- * @throws InvalidKeyException if the key is not good for wrapping or unwrapping.
- * @throws IllegalBlockSizeException if there is an issue wrapping.
- */
- public static WrappedKeyProto.WrappedKey rewrap(
- SecretKey oldSecondaryKey,
- SecretKey newSecondaryKey,
- WrappedKeyProto.WrappedKey tertiaryKey)
- throws InvalidKeyException, IllegalBlockSizeException,
- InvalidAlgorithmParameterException, NoSuchAlgorithmException,
- NoSuchPaddingException {
- return wrap(newSecondaryKey, unwrap(oldSecondaryKey, tertiaryKey));
- }
-
- private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
- return Cipher.getInstance(AES_GCM_MODE);
- }
-
- // Statics only
- private KeyWrapUtils() {}
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKey.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKey.java
deleted file mode 100644
index 436c6de8..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKey.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import android.annotation.IntDef;
-import android.content.Context;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.RecoveryController;
-import android.util.Slog;
-
-import java.util.Objects;
-
-import javax.crypto.SecretKey;
-
-/**
- * Wraps a {@link RecoveryController}'s {@link SecretKey}. These are kept in "AndroidKeyStore" (a
- * provider for {@link java.security.KeyStore} and {@link javax.crypto.KeyGenerator}. They are also
- * synced with the recoverable key store, wrapped by the primary key. This allows them to be
- * recovered on a user's subsequent device through providing their lock screen secret.
- */
-public class RecoverableKeyStoreSecondaryKey {
- private static final String TAG = "RecoverableKeyStoreSecondaryKey";
-
- private final String mAlias;
- private final SecretKey mSecretKey;
-
- /**
- * A new instance.
- *
- * @param alias The alias. It is keyed with this in AndroidKeyStore and the recoverable key
- * store.
- * @param secretKey The key.
- */
- public RecoverableKeyStoreSecondaryKey(String alias, SecretKey secretKey) {
- mAlias = Objects.requireNonNull(alias);
- mSecretKey = Objects.requireNonNull(secretKey);
- }
-
- /**
- * The ID, as stored in the recoverable {@link java.security.KeyStore}, and as used to identify
- * wrapped tertiary keys on the backup server.
- */
- public String getAlias() {
- return mAlias;
- }
-
- /** The secret key, to be used to wrap tertiary keys. */
- public SecretKey getSecretKey() {
- return mSecretKey;
- }
-
- /**
- * The status of the key. i.e., whether it's been synced to remote trusted hardware.
- *
- * @param context The application context.
- * @return One of {@link Status#SYNCED}, {@link Status#NOT_SYNCED} or {@link Status#DESTROYED}.
- */
- public @Status int getStatus(Context context) {
- try {
- return getStatusInternal(context);
- } catch (InternalRecoveryServiceException e) {
- Slog.wtf(TAG, "Internal error getting recovery status", e);
- // Return NOT_SYNCED by default, as we do not want the backups to fail or to repeatedly
- // attempt to reinitialize.
- return Status.NOT_SYNCED;
- }
- }
-
- private @Status int getStatusInternal(Context context) throws InternalRecoveryServiceException {
- int status = RecoveryController.getInstance(context).getRecoveryStatus(mAlias);
- switch (status) {
- case RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE:
- return Status.DESTROYED;
- case RecoveryController.RECOVERY_STATUS_SYNCED:
- return Status.SYNCED;
- case RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS:
- return Status.NOT_SYNCED;
- default:
- // Throw an exception if we encounter a status that doesn't match any of the above.
- throw new InternalRecoveryServiceException(
- "Unexpected status from getRecoveryStatus: " + status);
- }
- }
-
- /** Status of a key in the recoverable key store. */
- @IntDef({Status.NOT_SYNCED, Status.SYNCED, Status.DESTROYED})
- public @interface Status {
- /**
- * The key has not yet been synced to remote trusted hardware. This may be because the user
- * has not yet unlocked their device.
- */
- int NOT_SYNCED = 1;
-
- /**
- * The key has been synced with remote trusted hardware. It should now be recoverable on
- * another device.
- */
- int SYNCED = 2;
-
- /** The key has been lost forever. This can occur if the user disables their lock screen. */
- int DESTROYED = 3;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManager.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManager.java
deleted file mode 100644
index c89076b..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import android.content.Context;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.LockScreenRequiredException;
-import android.security.keystore.recovery.RecoveryController;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import libcore.util.HexEncoding;
-
-import java.security.SecureRandom;
-import java.security.UnrecoverableKeyException;
-import java.util.Optional;
-
-import javax.crypto.SecretKey;
-
-/**
- * Manages generating, deleting, and retrieving secondary keys through {@link RecoveryController}.
- *
- * <p>The recoverable key store will be synced remotely via the {@link RecoveryController}, allowing
- * recovery of keys on other devices owned by the user.
- */
-public class RecoverableKeyStoreSecondaryKeyManager {
- private static final String BACKUP_KEY_ALIAS_PREFIX =
- "com.android.server.backup/recoverablekeystore/";
- private static final int BACKUP_KEY_SUFFIX_LENGTH_BITS = 128;
- private static final int BITS_PER_BYTE = 8;
-
- /** A new instance. */
- public static RecoverableKeyStoreSecondaryKeyManager getInstance(Context context) {
- return new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(context), new SecureRandom());
- }
-
- private final RecoveryController mRecoveryController;
- private final SecureRandom mSecureRandom;
-
- @VisibleForTesting
- public RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController recoveryController, SecureRandom secureRandom) {
- mRecoveryController = recoveryController;
- mSecureRandom = secureRandom;
- }
-
- /**
- * Generates a new recoverable key using the {@link RecoveryController}.
- *
- * @throws InternalRecoveryServiceException if an unexpected error occurred generating the key.
- * @throws LockScreenRequiredException if the user does not have a lock screen. A lock screen is
- * required to generate a recoverable key.
- */
- public RecoverableKeyStoreSecondaryKey generate()
- throws InternalRecoveryServiceException, LockScreenRequiredException,
- UnrecoverableKeyException {
- String alias = generateId();
- mRecoveryController.generateKey(alias);
- SecretKey key = (SecretKey) mRecoveryController.getKey(alias);
- if (key == null) {
- throw new InternalRecoveryServiceException(
- String.format(
- "Generated key %s but could not get it back immediately afterwards.",
- alias));
- }
- return new RecoverableKeyStoreSecondaryKey(alias, key);
- }
-
- /**
- * Removes the secondary key. This means the key will no longer be recoverable.
- *
- * @param alias The alias of the key.
- * @throws InternalRecoveryServiceException if there was a {@link RecoveryController} error.
- */
- public void remove(String alias) throws InternalRecoveryServiceException {
- mRecoveryController.removeKey(alias);
- }
-
- /**
- * Returns the {@link RecoverableKeyStoreSecondaryKey} with {@code alias} if it is in the {@link
- * RecoveryController}. Otherwise, {@link Optional#empty()}.
- */
- public Optional<RecoverableKeyStoreSecondaryKey> get(String alias)
- throws InternalRecoveryServiceException, UnrecoverableKeyException {
- SecretKey secretKey = (SecretKey) mRecoveryController.getKey(alias);
- return Optional.ofNullable(secretKey)
- .map(key -> new RecoverableKeyStoreSecondaryKey(alias, key));
- }
-
- /**
- * Generates a new key alias. This has more entropy than a UUID - it can be considered
- * universally unique.
- */
- private String generateId() {
- byte[] id = new byte[BACKUP_KEY_SUFFIX_LENGTH_BITS / BITS_PER_BYTE];
- mSecureRandom.nextBytes(id);
- return BACKUP_KEY_ALIAS_PREFIX + HexEncoding.encodeToString(id);
- }
-
- /** Constructs a {@link RecoverableKeyStoreSecondaryKeyManager}. */
- public interface RecoverableKeyStoreSecondaryKeyManagerProvider {
- /** Returns a newly constructed {@link RecoverableKeyStoreSecondaryKeyManager}. */
- RecoverableKeyStoreSecondaryKeyManager get();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RestoreKeyFetcher.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RestoreKeyFetcher.java
deleted file mode 100644
index 6fb958b..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/RestoreKeyFetcher.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.keys;
-
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.util.Optional;
-
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-
-/** Fetches the secondary key and uses it to unwrap the tertiary key during restore. */
-public class RestoreKeyFetcher {
-
- /**
- * Retrieves the secondary key with the given alias and uses it to unwrap the given wrapped
- * tertiary key.
- *
- * @param secondaryKeyManagerProvider Provider which creates {@link
- * RecoverableKeyStoreSecondaryKeyManager}
- * @param secondaryKeyAlias Alias of the secondary key used to wrap the tertiary key
- * @param wrappedTertiaryKey Tertiary key wrapped with the secondary key above
- * @return The unwrapped tertiary key
- */
- public static SecretKey unwrapTertiaryKey(
- RecoverableKeyStoreSecondaryKeyManagerProvider secondaryKeyManagerProvider,
- String secondaryKeyAlias,
- WrappedKeyProto.WrappedKey wrappedTertiaryKey)
- throws KeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
- NoSuchPaddingException {
- Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
- getSecondaryKey(secondaryKeyManagerProvider, secondaryKeyAlias);
- if (!secondaryKey.isPresent()) {
- throw new KeyException("No key:" + secondaryKeyAlias);
- }
-
- return KeyWrapUtils.unwrap(secondaryKey.get().getSecretKey(), wrappedTertiaryKey);
- }
-
- private static Optional<RecoverableKeyStoreSecondaryKey> getSecondaryKey(
- RecoverableKeyStoreSecondaryKeyManagerProvider secondaryKeyManagerProvider,
- String secondaryKeyAlias)
- throws KeyException {
- try {
- return secondaryKeyManagerProvider.get().get(secondaryKeyAlias);
- } catch (InternalRecoveryServiceException | UnrecoverableKeyException e) {
- throw new KeyException("Could not retrieve key:" + secondaryKeyAlias, e);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java
deleted file mode 100644
index 91b57cf..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.keys;
-
-import android.content.Context;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;
-
-import java.io.File;
-import java.time.Clock;
-import java.util.Optional;
-
-/**
- * Helps schedule rotations of secondary keys.
- *
- * <p>TODO(b/72028016) Replace with a job.
- */
-public class SecondaryKeyRotationScheduler {
-
- private static final String TAG = "SecondaryKeyRotationScheduler";
- private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";
-
- private final Context mContext;
- private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
- private final CryptoSettings mCryptoSettings;
- private final Clock mClock;
-
- public SecondaryKeyRotationScheduler(
- Context context,
- RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
- CryptoSettings cryptoSettings,
- Clock clock) {
- mContext = context;
- mCryptoSettings = cryptoSettings;
- mClock = clock;
- mSecondaryKeyManager = secondaryKeyManager;
- }
-
- /**
- * Returns {@code true} if a sentinel file for forcing secondary key rotation is present. This
- * is only for testing purposes.
- */
- private boolean isForceRotationTestSentinelPresent() {
- File file = new File(mContext.getFilesDir(), SENTINEL_FILE_PATH);
- if (file.exists()) {
- file.delete();
- return true;
- }
- return false;
- }
-
- /** Start the key rotation task if it's time to do so */
- public void startRotationIfScheduled() {
- if (isForceRotationTestSentinelPresent()) {
- Slog.i(TAG, "Found force flag for secondary rotation. Starting now.");
- startRotation();
- return;
- }
-
- Optional<Long> maybeLastRotated = mCryptoSettings.getSecondaryLastRotated();
- if (!maybeLastRotated.isPresent()) {
- Slog.v(TAG, "No previous rotation, scheduling from now.");
- scheduleRotationFromNow();
- return;
- }
-
- long lastRotated = maybeLastRotated.get();
- long now = mClock.millis();
-
- if (lastRotated > now) {
- Slog.i(TAG, "Last rotation was in the future. Clock must have changed. Rotate now.");
- startRotation();
- return;
- }
-
- long millisSinceLastRotation = now - lastRotated;
- long rotationInterval = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();
- if (millisSinceLastRotation >= rotationInterval) {
- Slog.i(
- TAG,
- "Last rotation was more than "
- + rotationInterval
- + "ms ("
- + millisSinceLastRotation
- + "ms) in the past. Rotate now.");
- startRotation();
- }
-
- Slog.v(TAG, "No rotation required, last " + lastRotated + ".");
- }
-
- private void startRotation() {
- scheduleRotationFromNow();
- new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager).run();
- }
-
- private void scheduleRotationFromNow() {
- mCryptoSettings.setSecondaryLastRotated(mClock.millis());
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
deleted file mode 100644
index a425c72..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-
-/** 256-bit AES key generator. Each app should have its own separate AES key. */
-public class TertiaryKeyGenerator {
- private static final int KEY_SIZE_BITS = 256;
- private static final String KEY_ALGORITHM = "AES";
-
- private final KeyGenerator mKeyGenerator;
-
- /** New instance generating keys using {@code secureRandom}. */
- public TertiaryKeyGenerator(SecureRandom secureRandom) {
- try {
- mKeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- mKeyGenerator.init(KEY_SIZE_BITS, secureRandom);
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(
- "Impossible condition: JCE thinks it does not support AES.", e);
- }
- }
-
- /** Generates a new random AES key. */
- public SecretKey generate() {
- return mKeyGenerator.generateKey();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java
deleted file mode 100644
index a783579..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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.keys;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Optional;
-
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-
-/**
- * Gets the correct tertiary key to use during a backup, rotating it if required.
- *
- * <p>Calling any method on this class will count a incremental backup against the app, and the key
- * will be rotated if required.
- */
-public class TertiaryKeyManager {
-
- private static final String TAG = "TertiaryKeyMgr";
-
- private final TertiaryKeyStore mKeyStore;
- private final TertiaryKeyGenerator mKeyGenerator;
- private final TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler;
- private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
- private final String mPackageName;
-
- private boolean mKeyRotated;
- @Nullable private SecretKey mTertiaryKey;
-
- public TertiaryKeyManager(
- Context context,
- SecureRandom secureRandom,
- TertiaryKeyRotationScheduler tertiaryKeyRotationScheduler,
- RecoverableKeyStoreSecondaryKey secondaryKey,
- String packageName) {
- mSecondaryKey = secondaryKey;
- mPackageName = packageName;
- mKeyGenerator = new TertiaryKeyGenerator(secureRandom);
- mKeyStore = TertiaryKeyStore.newInstance(context, secondaryKey);
- mTertiaryKeyRotationScheduler = tertiaryKeyRotationScheduler;
- }
-
- /**
- * Returns either the previously used tertiary key, or a new tertiary key if there was no
- * previous key or it needed to be rotated.
- */
- public SecretKey getKey()
- throws InvalidKeyException, IOException, IllegalBlockSizeException,
- NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException {
- init();
- return mTertiaryKey;
- }
-
- /** Returns the key given by {@link #getKey()} wrapped by the secondary key. */
- public WrappedKeyProto.WrappedKey getWrappedKey()
- throws InvalidKeyException, IOException, IllegalBlockSizeException,
- NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException {
- init();
- return KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), mTertiaryKey);
- }
-
- /**
- * Returns {@code true} if a new tertiary key was generated at the start of this session,
- * otherwise {@code false}.
- */
- public boolean wasKeyRotated()
- throws InvalidKeyException, IllegalBlockSizeException, IOException,
- NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException {
- init();
- return mKeyRotated;
- }
-
- private void init()
- throws IllegalBlockSizeException, InvalidKeyException, IOException,
- NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidAlgorithmParameterException {
- if (mTertiaryKey != null) {
- return;
- }
-
- Optional<SecretKey> key = getExistingKeyIfNotRotated();
-
- if (!key.isPresent()) {
- Slog.d(TAG, "Generating new tertiary key for " + mPackageName);
-
- key = Optional.of(mKeyGenerator.generate());
- mKeyRotated = true;
- mTertiaryKeyRotationScheduler.recordKeyRotation(mPackageName);
- mKeyStore.save(mPackageName, key.get());
- }
-
- mTertiaryKey = key.get();
-
- mTertiaryKeyRotationScheduler.recordBackup(mPackageName);
- }
-
- private Optional<SecretKey> getExistingKeyIfNotRotated()
- throws InvalidKeyException, IOException, InvalidAlgorithmParameterException,
- NoSuchAlgorithmException, NoSuchPaddingException {
- if (mTertiaryKeyRotationScheduler.isKeyRotationDue(mPackageName)) {
- Slog.i(TAG, "Tertiary key rotation was required for " + mPackageName);
- return Optional.empty();
- } else {
- Slog.i(TAG, "Tertiary key rotation was not required");
- return mKeyStore.load(mPackageName);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java
deleted file mode 100644
index f16a68d..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.keys;
-
-import android.content.Context;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-/**
- * Schedules tertiary key rotations in a staggered fashion.
- *
- * <p>Apps are due a key rotation after a certain number of backups. Rotations are then staggerered
- * over a period of time, through restricting the number of rotations allowed in a 24-hour window.
- * This will causes the apps to enter a staggered cycle of regular rotations.
- *
- * <p>Note: the methods in this class are not optimized to be super fast. They make blocking IO to
- * ensure that scheduler information is committed to disk, so that it is available after the user
- * turns their device off and on. This ought to be fine as
- *
- * <ul>
- * <li>It will be invoked before a backup, so should never be invoked on the UI thread
- * <li>It will be invoked before a backup, so the vast amount of time is spent on the backup, not
- * writing tiny amounts of data to disk.
- * </ul>
- */
-public class TertiaryKeyRotationScheduler {
- /** Default number of key rotations allowed within 24 hours. */
- private static final int KEY_ROTATION_LIMIT = 2;
-
- /** A new instance, using {@code context} to determine where to store state. */
- public static TertiaryKeyRotationScheduler getInstance(Context context) {
- TertiaryKeyRotationWindowedCount windowedCount =
- TertiaryKeyRotationWindowedCount.getInstance(context);
- TertiaryKeyRotationTracker tracker = TertiaryKeyRotationTracker.getInstance(context);
- return new TertiaryKeyRotationScheduler(tracker, windowedCount, KEY_ROTATION_LIMIT);
- }
-
- private final TertiaryKeyRotationTracker mTracker;
- private final TertiaryKeyRotationWindowedCount mWindowedCount;
- private final int mMaximumRotationsPerWindow;
-
- /**
- * A new instance.
- *
- * @param tracker Tracks how many times each application has backed up.
- * @param windowedCount Tracks how many rotations have happened in the last 24 hours.
- * @param maximumRotationsPerWindow The maximum number of key rotations allowed per 24 hours.
- */
- @VisibleForTesting
- TertiaryKeyRotationScheduler(
- TertiaryKeyRotationTracker tracker,
- TertiaryKeyRotationWindowedCount windowedCount,
- int maximumRotationsPerWindow) {
- mTracker = tracker;
- mWindowedCount = windowedCount;
- mMaximumRotationsPerWindow = maximumRotationsPerWindow;
- }
-
- /**
- * Returns {@code true} if the app with {@code packageName} is due having its key rotated.
- *
- * <p>This ought to be queried before backing up an app, to determine whether to do an
- * incremental backup or a full backup. (A full backup forces key rotation.)
- */
- public boolean isKeyRotationDue(String packageName) {
- if (mWindowedCount.getCount() >= mMaximumRotationsPerWindow) {
- return false;
- }
- return mTracker.isKeyRotationDue(packageName);
- }
-
- /**
- * Records that a backup happened for the app with the given {@code packageName}.
- *
- * <p>Each backup brings the app closer to the point at which a key rotation is due.
- */
- public void recordBackup(String packageName) {
- mTracker.recordBackup(packageName);
- }
-
- /**
- * Records a key rotation happened for the app with the given {@code packageName}.
- *
- * <p>This resets the countdown until the next key rotation is due.
- */
- public void recordKeyRotation(String packageName) {
- mTracker.resetCountdown(packageName);
- mWindowedCount.record();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
deleted file mode 100644
index 1a281e7..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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.keys;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Locale;
-
-/**
- * Tracks when a tertiary key rotation is due.
- *
- * <p>After a certain number of incremental backups, the device schedules a full backup, which will
- * generate a new encryption key, effecting a key rotation. We should do this on a regular basis so
- * that if a key does become compromised it has limited value to the attacker.
- *
- * <p>No additional synchronization of this class is provided. Only one instance should be used at
- * any time. This should be fine as there should be no parallelism in backups.
- */
-public class TertiaryKeyRotationTracker {
- private static final int MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION = 31;
- private static final String SHARED_PREFERENCES_NAME = "tertiary_key_rotation_tracker";
-
- private static final String TAG = "TertiaryKeyRotationTracker";
- private static final boolean DEBUG = false;
-
- /**
- * A new instance, using {@code context} to commit data to disk via {@link SharedPreferences}.
- */
- public static TertiaryKeyRotationTracker getInstance(Context context) {
- return new TertiaryKeyRotationTracker(
- context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE),
- MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION);
- }
-
- private final SharedPreferences mSharedPreferences;
- private final int mMaxBackupsTillRotation;
-
- /**
- * New instance, storing data in {@code sharedPreferences} and initializing backup countdown to
- * {@code maxBackupsTillRotation}.
- */
- @VisibleForTesting
- TertiaryKeyRotationTracker(SharedPreferences sharedPreferences, int maxBackupsTillRotation) {
- checkArgument(
- maxBackupsTillRotation >= 0,
- String.format(
- Locale.US,
- "maxBackupsTillRotation should be non-negative but was %d",
- maxBackupsTillRotation));
- mSharedPreferences = sharedPreferences;
- mMaxBackupsTillRotation = maxBackupsTillRotation;
- }
-
- /**
- * Returns {@code true} if the given app is due having its key rotated.
- *
- * @param packageName The package name of the app.
- */
- public boolean isKeyRotationDue(String packageName) {
- return getBackupsSinceRotation(packageName) >= mMaxBackupsTillRotation;
- }
-
- /**
- * Records that an incremental backup has occurred. Each incremental backup brings the app
- * closer to the time when its key should be rotated.
- *
- * @param packageName The package name of the app for which the backup occurred.
- */
- public void recordBackup(String packageName) {
- int backupsSinceRotation = getBackupsSinceRotation(packageName) + 1;
- mSharedPreferences.edit().putInt(packageName, backupsSinceRotation).apply();
- if (DEBUG) {
- Slog.d(
- TAG,
- String.format(
- Locale.US,
- "Incremental backup for %s. %d backups until key rotation.",
- packageName,
- Math.max(
- 0,
- mMaxBackupsTillRotation
- - backupsSinceRotation)));
- }
- }
-
- /**
- * Resets the rotation delay for the given app. Should be invoked after a key rotation.
- *
- * @param packageName Package name of the app whose key has rotated.
- */
- public void resetCountdown(String packageName) {
- mSharedPreferences.edit().putInt(packageName, 0).apply();
- }
-
- /** Marks all enrolled packages for key rotation. */
- public void markAllForRotation() {
- SharedPreferences.Editor editor = mSharedPreferences.edit();
- for (String packageName : mSharedPreferences.getAll().keySet()) {
- editor.putInt(packageName, mMaxBackupsTillRotation);
- }
- editor.apply();
- }
-
- private int getBackupsSinceRotation(String packageName) {
- return mSharedPreferences.getInt(packageName, 0);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java
deleted file mode 100644
index b90343a..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.keys;
-
-import android.content.Context;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.time.Clock;
-import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Tracks (and commits to disk) how many key rotations have happened in the last 24 hours. This
- * allows us to limit (and therefore stagger) the number of key rotations in a given period of time.
- *
- * <p>Note to engineers thinking of replacing the below with fancier algorithms and data structures:
- * we expect the total size of this count at any time to be below however many rotations we allow in
- * the window, which is going to be in single digits. Any changes that mean we write to disk more
- * frequently, that the code is no longer resistant to clock changes, or that the code is more
- * difficult to understand are almost certainly not worthwhile.
- */
-public class TertiaryKeyRotationWindowedCount {
- private static final String TAG = "TertiaryKeyRotCount";
-
- private static final int WINDOW_IN_HOURS = 24;
- private static final String LOG_FILE_NAME = "tertiary_key_rotation_windowed_count";
-
- private final Clock mClock;
- private final File mFile;
- private ArrayList<Long> mEvents;
-
- /** Returns a new instance, persisting state to the files dir of {@code context}. */
- public static TertiaryKeyRotationWindowedCount getInstance(Context context) {
- File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
- return new TertiaryKeyRotationWindowedCount(logFile, Clock.systemDefaultZone());
- }
-
- /** A new instance, committing state to {@code file}, and reading time from {@code clock}. */
- @VisibleForTesting
- TertiaryKeyRotationWindowedCount(File file, Clock clock) {
- mFile = file;
- mClock = clock;
- mEvents = new ArrayList<>();
- try {
- loadFromFile();
- } catch (IOException e) {
- Slog.e(TAG, "Error reading " + LOG_FILE_NAME, e);
- }
- }
-
- /** Records a key rotation at the current time. */
- public void record() {
- mEvents.add(mClock.millis());
- compact();
- try {
- saveToFile();
- } catch (IOException e) {
- Slog.e(TAG, "Error saving " + LOG_FILE_NAME, e);
- }
- }
-
- /** Returns the number of key rotation that have been recorded in the window. */
- public int getCount() {
- compact();
- return mEvents.size();
- }
-
- private void compact() {
- long minimumTimestamp = getMinimumTimestamp();
- long now = mClock.millis();
- ArrayList<Long> compacted = new ArrayList<>();
- for (long event : mEvents) {
- if (event >= minimumTimestamp && event <= now) {
- compacted.add(event);
- }
- }
- mEvents = compacted;
- }
-
- private long getMinimumTimestamp() {
- return mClock.millis() - TimeUnit.HOURS.toMillis(WINDOW_IN_HOURS) + 1;
- }
-
- private void loadFromFile() throws IOException {
- if (!mFile.exists()) {
- return;
- }
- try (FileInputStream fis = new FileInputStream(mFile);
- DataInputStream dis = new DataInputStream(fis)) {
- while (true) {
- mEvents.add(dis.readLong());
- }
- } catch (EOFException eof) {
- // expected
- }
- }
-
- private void saveToFile() throws IOException {
- // File size is maximum number of key rotations in window multiplied by 8 bytes, which is
- // why
- // we just overwrite it each time. We expect it will always be less than 100 bytes in size.
- try (FileOutputStream fos = new FileOutputStream(mFile);
- DataOutputStream dos = new DataOutputStream(fos)) {
- for (long event : mEvents) {
- dos.writeLong(event);
- }
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java
deleted file mode 100644
index 01444bf..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * 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.keys;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import android.content.Context;
-import android.util.ArrayMap;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-import com.android.server.backup.encryption.storage.BackupEncryptionDb;
-import com.android.server.backup.encryption.storage.TertiaryKey;
-import com.android.server.backup.encryption.storage.TertiaryKeysTable;
-
-import com.google.protobuf.nano.CodedOutputByteBufferNano;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Map;
-import java.util.Optional;
-
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-
-/**
- * Stores backup package keys. Each application package has its own {@link SecretKey}, which is used
- * to encrypt the backup data. These keys are then wrapped by a master backup key, and stored in
- * their wrapped form on disk and on the backup server.
- *
- * <p>For now this code only implements writing to disk. Once the backup server is ready, it will be
- * extended to sync the keys there, also.
- */
-public class TertiaryKeyStore {
-
- private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
- private final BackupEncryptionDb mDatabase;
-
- /**
- * Creates an instance, using {@code secondaryKey} to wrap tertiary keys, and storing them in
- * the database.
- */
- public static TertiaryKeyStore newInstance(
- Context context, RecoverableKeyStoreSecondaryKey secondaryKey) {
- return new TertiaryKeyStore(secondaryKey, BackupEncryptionDb.newInstance(context));
- }
-
- private TertiaryKeyStore(
- RecoverableKeyStoreSecondaryKey secondaryKey, BackupEncryptionDb database) {
- mSecondaryKey = secondaryKey;
- mDatabase = database;
- }
-
- /**
- * Saves the given key.
- *
- * @param applicationName The package name of the application for which this key will be used to
- * encrypt data. e.g., "com.example.app".
- * @param key The key.
- * @throws InvalidKeyException if the backup key is not capable of wrapping.
- * @throws IOException if there is an issue writing to the database.
- */
- public void save(String applicationName, SecretKey key)
- throws IOException, InvalidKeyException, IllegalBlockSizeException,
- NoSuchPaddingException, NoSuchAlgorithmException {
- checkApplicationName(applicationName);
-
- byte[] keyBytes = getEncodedKey(KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), key));
-
- long pk;
- try {
- pk =
- mDatabase
- .getTertiaryKeysTable()
- .addKey(
- new TertiaryKey(
- mSecondaryKey.getAlias(), applicationName, keyBytes));
- } finally {
- mDatabase.close();
- }
-
- if (pk == -1) {
- throw new IOException("Failed to commit to db");
- }
- }
-
- /**
- * Tries to load a key for the given application.
- *
- * @param applicationName The package name of the application, e.g. "com.example.app".
- * @return The key if it is exists, {@link Optional#empty()} ()} otherwise.
- * @throws InvalidKeyException if the backup key is not good for unwrapping.
- * @throws IOException if there is a problem loading the key from the database.
- */
- public Optional<SecretKey> load(String applicationName)
- throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
- NoSuchAlgorithmException, NoSuchPaddingException {
- checkApplicationName(applicationName);
-
- Optional<TertiaryKey> keyFromDb;
- try {
- keyFromDb =
- mDatabase
- .getTertiaryKeysTable()
- .getKey(mSecondaryKey.getAlias(), applicationName);
- } finally {
- mDatabase.close();
- }
-
- if (!keyFromDb.isPresent()) {
- return Optional.empty();
- }
-
- WrappedKeyProto.WrappedKey wrappedKey =
- WrappedKeyProto.WrappedKey.parseFrom(keyFromDb.get().getWrappedKeyBytes());
- return Optional.of(KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
- }
-
- /**
- * Loads keys for all applications.
- *
- * @return All of the keys in a map keyed by package name.
- * @throws IOException if there is an issue loading from the database.
- * @throws InvalidKeyException if the backup key is not an appropriate key for unwrapping.
- */
- public Map<String, SecretKey> getAll()
- throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
- NoSuchAlgorithmException, NoSuchPaddingException {
- Map<String, TertiaryKey> tertiaries;
- try {
- tertiaries = mDatabase.getTertiaryKeysTable().getAllKeys(mSecondaryKey.getAlias());
- } finally {
- mDatabase.close();
- }
-
- Map<String, SecretKey> unwrappedKeys = new ArrayMap<>();
- for (String applicationName : tertiaries.keySet()) {
- WrappedKeyProto.WrappedKey wrappedKey =
- WrappedKeyProto.WrappedKey.parseFrom(
- tertiaries.get(applicationName).getWrappedKeyBytes());
- unwrappedKeys.put(
- applicationName, KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
- }
-
- return unwrappedKeys;
- }
-
- /**
- * Adds all wrapped keys to the database.
- *
- * @throws IOException if an error occurred adding a wrapped key.
- */
- public void putAll(Map<String, WrappedKeyProto.WrappedKey> wrappedKeysByApplicationName)
- throws IOException {
- TertiaryKeysTable tertiaryKeysTable = mDatabase.getTertiaryKeysTable();
- try {
-
- for (String applicationName : wrappedKeysByApplicationName.keySet()) {
- byte[] keyBytes = getEncodedKey(wrappedKeysByApplicationName.get(applicationName));
- long primaryKey =
- tertiaryKeysTable.addKey(
- new TertiaryKey(
- mSecondaryKey.getAlias(), applicationName, keyBytes));
-
- if (primaryKey == -1) {
- throw new IOException("Failed to commit to db");
- }
- }
-
- } finally {
- mDatabase.close();
- }
- }
-
- private static void checkApplicationName(String applicationName) {
- checkArgument(!applicationName.isEmpty(), "applicationName must not be empty string.");
- checkArgument(!applicationName.contains("/"), "applicationName must not contain slash.");
- }
-
- private byte[] getEncodedKey(WrappedKeyProto.WrappedKey key) throws IOException {
- byte[] buffer = new byte[key.getSerializedSize()];
- CodedOutputByteBufferNano out = CodedOutputByteBufferNano.newInstance(buffer);
- key.writeTo(out);
- return buffer;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java
deleted file mode 100644
index 56e1c05..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.kv;
-
-import static com.android.internal.util.Preconditions.checkState;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ChunkHasher;
-import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
-import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Builds a key value backup set from plaintext chunks. Computes a digest over the sorted SHA-256
- * hashes of the chunks.
- */
-public class DecryptedChunkKvOutput implements DecryptedChunkOutput {
- @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
-
- private final ChunkHasher mChunkHasher;
- private final List<KeyValuePairProto.KeyValuePair> mUnsortedPairs = new ArrayList<>();
- private final List<ChunkHash> mUnsortedHashes = new ArrayList<>();
- private boolean mClosed;
-
- /** Constructs a new instance which computers the digest using the given hasher. */
- public DecryptedChunkKvOutput(ChunkHasher chunkHasher) {
- mChunkHasher = chunkHasher;
- }
-
- @Override
- public DecryptedChunkOutput open() {
- // As we don't have any resources there is nothing to open.
- return this;
- }
-
- @Override
- public void processChunk(byte[] plaintextBuffer, int length)
- throws IOException, InvalidKeyException {
- checkState(!mClosed, "Cannot process chunk after close()");
- KeyValuePairProto.KeyValuePair kvPair = new KeyValuePairProto.KeyValuePair();
- KeyValuePairProto.KeyValuePair.mergeFrom(kvPair, plaintextBuffer, 0, length);
- mUnsortedPairs.add(kvPair);
- // TODO(b/71492289): Update ChunkHasher to accept offset and length so we don't have to copy
- // the buffer into a smaller array.
- mUnsortedHashes.add(mChunkHasher.computeHash(Arrays.copyOf(plaintextBuffer, length)));
- }
-
- @Override
- public void close() {
- // As we don't have any resources there is nothing to close.
- mClosed = true;
- }
-
- @Override
- public byte[] getDigest() throws NoSuchAlgorithmException {
- checkState(mClosed, "Must close() before getDigest()");
- MessageDigest digest = getMessageDigest();
- Collections.sort(mUnsortedHashes);
- for (ChunkHash hash : mUnsortedHashes) {
- digest.update(hash.getHash());
- }
- return digest.digest();
- }
-
- private static MessageDigest getMessageDigest() throws NoSuchAlgorithmException {
- return MessageDigest.getInstance(DIGEST_ALGORITHM);
- }
-
- /**
- * Returns the key value pairs from the backup, sorted lexicographically by key.
- *
- * <p>You must call {@link #close} first.
- */
- public List<KeyValuePairProto.KeyValuePair> getPairs() {
- checkState(mClosed, "Must close() before getPairs()");
- Collections.sort(
- mUnsortedPairs,
- new Comparator<KeyValuePairProto.KeyValuePair>() {
- @Override
- public int compare(
- KeyValuePairProto.KeyValuePair o1, KeyValuePairProto.KeyValuePair o2) {
- return o1.key.compareTo(o2.key);
- }
- });
- return mUnsortedPairs;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java
deleted file mode 100644
index 217304c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.kv;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
-
-/**
- * Builds a {@link KeyValueListingProto.KeyValueListing}, which is a nano proto and so has no
- * builder.
- */
-public class KeyValueListingBuilder {
- private final List<KeyValueListingProto.KeyValueEntry> mEntries = new ArrayList<>();
-
- /** Adds a new pair entry to the listing. */
- public KeyValueListingBuilder addPair(String key, ChunkHash hash) {
- checkArgument(key.length() != 0, "Key must have non-zero length");
- Objects.requireNonNull(hash, "Hash must not be null");
-
- KeyValueListingProto.KeyValueEntry entry = new KeyValueListingProto.KeyValueEntry();
- entry.key = key;
- entry.hash = hash.getHash();
- mEntries.add(entry);
-
- return this;
- }
-
- /** Adds all pairs contained in a map, where the map is from key to hash. */
- public KeyValueListingBuilder addAll(Map<String, ChunkHash> map) {
- for (Entry<String, ChunkHash> entry : map.entrySet()) {
- addPair(entry.getKey(), entry.getValue());
- }
-
- return this;
- }
-
- /** Returns a new listing containing all the pairs added so far. */
- public KeyValueListingProto.KeyValueListing build() {
- if (mEntries.size() == 0) {
- return emptyListing();
- }
-
- KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
- listing.entries = new KeyValueListingProto.KeyValueEntry[mEntries.size()];
- mEntries.toArray(listing.entries);
- return listing;
- }
-
- /** Returns a new listing which does not contain any pairs. */
- public static KeyValueListingProto.KeyValueListing emptyListing() {
- KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
- listing.entries = KeyValueListingProto.KeyValueEntry.emptyArray();
- return listing;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDb.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDb.java
deleted file mode 100644
index 9f6c03a..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDb.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import android.content.Context;
-
-/**
- * Backup encryption SQLite database. All instances are threadsafe.
- *
- * <p>The database is automatically opened when accessing one of the tables. After the caller is
- * done they must call {@link #close()}.
- */
-public class BackupEncryptionDb {
- private final BackupEncryptionDbHelper mHelper;
-
- /** A new instance, using the storage defined by {@code context}. */
- public static BackupEncryptionDb newInstance(Context context) {
- BackupEncryptionDbHelper helper = new BackupEncryptionDbHelper(context);
- helper.setWriteAheadLoggingEnabled(true);
- return new BackupEncryptionDb(helper);
- }
-
- private BackupEncryptionDb(BackupEncryptionDbHelper helper) {
- mHelper = helper;
- }
-
- public TertiaryKeysTable getTertiaryKeysTable() {
- return new TertiaryKeysTable(mHelper);
- }
-
- /** Deletes the database. */
- public void clear() throws EncryptionDbException {
- mHelper.resetDatabase();
- }
-
- /**
- * Closes the database if it is open.
- *
- * <p>After calling this, the caller may access one of the tables again which will automatically
- * reopen the database.
- */
- public void close() {
- mHelper.close();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDbContract.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDbContract.java
deleted file mode 100644
index 5e8a8d9..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDbContract.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import android.provider.BaseColumns;
-
-/** Contract for the backup encryption database. Describes tables present. */
-class BackupEncryptionDbContract {
- /**
- * Table containing tertiary keys belonging to the user. Tertiary keys are wrapped by a
- * secondary key, which never leaves {@code AndroidKeyStore} (a provider for {@link
- * java.security.KeyStore}). Each application has a tertiary key, which is used to encrypt the
- * backup data.
- */
- static class TertiaryKeysEntry implements BaseColumns {
- static final String TABLE_NAME = "tertiary_keys";
-
- /** Alias of the secondary key used to wrap the tertiary key. */
- static final String COLUMN_NAME_SECONDARY_KEY_ALIAS = "secondary_key_alias";
-
- /** Name of the package to which the tertiary key belongs. */
- static final String COLUMN_NAME_PACKAGE_NAME = "package_name";
-
- /** Encrypted bytes of the tertiary key. */
- static final String COLUMN_NAME_WRAPPED_KEY_BYTES = "wrapped_key_bytes";
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDbHelper.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDbHelper.java
deleted file mode 100644
index c706342..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/BackupEncryptionDbHelper.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import static com.android.server.backup.encryption.storage.BackupEncryptionDbContract.TertiaryKeysEntry;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteOpenHelper;
-
-/** Helper for creating an instance of the backup encryption database. */
-class BackupEncryptionDbHelper extends SQLiteOpenHelper {
- private static final int DATABASE_VERSION = 1;
- static final String DATABASE_NAME = "backupencryption.db";
-
- private static final String SQL_CREATE_TERTIARY_KEYS_ENTRY =
- "CREATE TABLE "
- + TertiaryKeysEntry.TABLE_NAME
- + " ( "
- + TertiaryKeysEntry._ID
- + " INTEGER PRIMARY KEY,"
- + TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
- + " TEXT,"
- + TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
- + " TEXT,"
- + TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
- + " BLOB,"
- + "UNIQUE("
- + TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
- + ","
- + TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
- + "))";
-
- private static final String SQL_DROP_TERTIARY_KEYS_ENTRY =
- "DROP TABLE IF EXISTS " + TertiaryKeysEntry.TABLE_NAME;
-
- BackupEncryptionDbHelper(Context context) {
- super(context, DATABASE_NAME, /*factory=*/ null, DATABASE_VERSION);
- }
-
- public void resetDatabase() throws EncryptionDbException {
- SQLiteDatabase db = getWritableDatabaseSafe();
- db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
- onCreate(db);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL(SQL_CREATE_TERTIARY_KEYS_ENTRY);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
- onCreate(db);
- }
-
- @Override
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
- onCreate(db);
- }
-
- /**
- * Calls {@link #getWritableDatabase()}, but catches the unchecked {@link SQLiteException} and
- * rethrows {@link EncryptionDbException}.
- */
- public SQLiteDatabase getWritableDatabaseSafe() throws EncryptionDbException {
- try {
- return super.getWritableDatabase();
- } catch (SQLiteException e) {
- throw new EncryptionDbException(e);
- }
- }
-
- /**
- * Calls {@link #getReadableDatabase()}, but catches the unchecked {@link SQLiteException} and
- * rethrows {@link EncryptionDbException}.
- */
- public SQLiteDatabase getReadableDatabaseSafe() throws EncryptionDbException {
- try {
- return super.getReadableDatabase();
- } catch (SQLiteException e) {
- throw new EncryptionDbException(e);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/EncryptionDbException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/EncryptionDbException.java
deleted file mode 100644
index 82f7dea..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/EncryptionDbException.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import java.io.IOException;
-
-/** Thrown when there is a problem reading or writing the encryption database. */
-public class EncryptionDbException extends IOException {
- public EncryptionDbException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/TertiaryKey.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/TertiaryKey.java
deleted file mode 100644
index 39a2c6e..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/TertiaryKey.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-/** Wrapped bytes of a tertiary key. */
-public class TertiaryKey {
- private final String mSecondaryKeyAlias;
- private final String mPackageName;
- private final byte[] mWrappedKeyBytes;
-
- /**
- * Creates a new instance.
- *
- * @param secondaryKeyAlias Alias of the secondary used to wrap the key.
- * @param packageName The package name of the app to which the key belongs.
- * @param wrappedKeyBytes The wrapped key bytes.
- */
- public TertiaryKey(String secondaryKeyAlias, String packageName, byte[] wrappedKeyBytes) {
- mSecondaryKeyAlias = secondaryKeyAlias;
- mPackageName = packageName;
- mWrappedKeyBytes = wrappedKeyBytes;
- }
-
- /** Returns the alias of the secondary key used to wrap this tertiary key. */
- public String getSecondaryKeyAlias() {
- return mSecondaryKeyAlias;
- }
-
- /** Returns the package name of the application this key relates to. */
- public String getPackageName() {
- return mPackageName;
- }
-
- /** Returns the wrapped bytes of the key. */
- public byte[] getWrappedKeyBytes() {
- return mWrappedKeyBytes;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/TertiaryKeysTable.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/TertiaryKeysTable.java
deleted file mode 100644
index d8d40c4..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/storage/TertiaryKeysTable.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import static com.android.server.backup.encryption.storage.BackupEncryptionDbContract.TertiaryKeysEntry;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.ArrayMap;
-
-import java.util.Collections;
-import java.util.Map;
-import java.util.Optional;
-
-/** Database table for storing and retrieving tertiary keys. */
-public class TertiaryKeysTable {
- private final BackupEncryptionDbHelper mHelper;
-
- TertiaryKeysTable(BackupEncryptionDbHelper helper) {
- mHelper = helper;
- }
-
- /**
- * Adds the {@code tertiaryKey} to the database.
- *
- * @return The primary key of the inserted row if successful, -1 otherwise.
- */
- public long addKey(TertiaryKey tertiaryKey) throws EncryptionDbException {
- SQLiteDatabase db = mHelper.getWritableDatabaseSafe();
- ContentValues values = new ContentValues();
- values.put(
- TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
- tertiaryKey.getSecondaryKeyAlias());
- values.put(TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME, tertiaryKey.getPackageName());
- values.put(
- TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES, tertiaryKey.getWrappedKeyBytes());
- return db.replace(TertiaryKeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
- }
-
- /** Gets the key wrapped by {@code secondaryKeyAlias} for app with {@code packageName}. */
- public Optional<TertiaryKey> getKey(String secondaryKeyAlias, String packageName)
- throws EncryptionDbException {
- SQLiteDatabase db = mHelper.getReadableDatabaseSafe();
- String[] projection = {
- TertiaryKeysEntry._ID,
- TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
- TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME,
- TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
- };
- String selection =
- TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
- + " = ? AND "
- + TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
- + " = ?";
- String[] selectionArguments = {secondaryKeyAlias, packageName};
-
- try (Cursor cursor =
- db.query(
- TertiaryKeysEntry.TABLE_NAME,
- projection,
- selection,
- selectionArguments,
- /*groupBy=*/ null,
- /*having=*/ null,
- /*orderBy=*/ null)) {
- int count = cursor.getCount();
- if (count == 0) {
- return Optional.empty();
- }
-
- cursor.moveToFirst();
- byte[] wrappedKeyBytes =
- cursor.getBlob(
- cursor.getColumnIndexOrThrow(
- TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES));
- return Optional.of(new TertiaryKey(secondaryKeyAlias, packageName, wrappedKeyBytes));
- }
- }
-
- /** Returns all keys wrapped with {@code tertiaryKeyAlias} as an unmodifiable map. */
- public Map<String, TertiaryKey> getAllKeys(String secondaryKeyAlias)
- throws EncryptionDbException {
- SQLiteDatabase db = mHelper.getReadableDatabaseSafe();
- String[] projection = {
- TertiaryKeysEntry._ID,
- TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
- TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME,
- TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
- };
- String selection = TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS + " = ?";
- String[] selectionArguments = {secondaryKeyAlias};
-
- Map<String, TertiaryKey> keysByPackageName = new ArrayMap<>();
- try (Cursor cursor =
- db.query(
- TertiaryKeysEntry.TABLE_NAME,
- projection,
- selection,
- selectionArguments,
- /*groupBy=*/ null,
- /*having=*/ null,
- /*orderBy=*/ null)) {
- while (cursor.moveToNext()) {
- String packageName =
- cursor.getString(
- cursor.getColumnIndexOrThrow(
- TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME));
- byte[] wrappedKeyBytes =
- cursor.getBlob(
- cursor.getColumnIndexOrThrow(
- TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES));
- keysByPackageName.put(
- packageName,
- new TertiaryKey(secondaryKeyAlias, packageName, wrappedKeyBytes));
- }
- }
- return Collections.unmodifiableMap(keysByPackageName);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java
deleted file mode 100644
index 2e8a61f..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 when the server's active secondary key does not exist in the user's recoverable
- * keychain. This means the backup data cannot be decrypted, and should be wiped.
- */
-public class ActiveSecondaryNotInKeychainException extends Exception {
- public ActiveSecondaryNotInKeychainException(String message) {
- super(message);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupEncrypter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupEncrypter.java
deleted file mode 100644
index 95d0d97..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupEncrypter.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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 java.util.Collections.unmodifiableList;
-
-import android.annotation.Nullable;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-import javax.crypto.SecretKey;
-
-/** Task which reads data from some source, splits it into chunks and encrypts new chunks. */
-public interface BackupEncrypter {
- /** The algorithm which we use to compute the digest of the backup file plaintext. */
- String MESSAGE_DIGEST_ALGORITHM = "SHA-256";
-
- /**
- * Splits the backup input into encrypted chunks and encrypts new chunks.
- *
- * @param secretKey Key used to encrypt backup.
- * @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
- * full backup. Should be {@code null} for a key-value backup.
- * @param existingChunks Set of the SHA-256 Macs of chunks the server already has.
- * @return a result containing an array of new encrypted chunks to upload, and an ordered
- * listing of the chunks in the backup file.
- * @throws IOException if a problem occurs reading from the backup data.
- * @throws GeneralSecurityException if there is a problem encrypting the data.
- */
- Result backup(
- SecretKey secretKey,
- @Nullable byte[] fingerprintMixerSalt,
- Set<ChunkHash> existingChunks)
- throws IOException, GeneralSecurityException;
-
- /**
- * The result of an incremental backup. Contains new encrypted chunks to upload, and an ordered
- * list of the chunks in the backup file.
- */
- class Result {
- private final List<ChunkHash> mAllChunks;
- private final List<EncryptedChunk> mNewChunks;
- private final byte[] mDigest;
-
- public Result(List<ChunkHash> allChunks, List<EncryptedChunk> newChunks, byte[] digest) {
- mAllChunks = unmodifiableList(new ArrayList<>(allChunks));
- mDigest = digest;
- mNewChunks = unmodifiableList(new ArrayList<>(newChunks));
- }
-
- /**
- * Returns an unmodifiable list of the hashes of all the chunks in the backup, in the order
- * they appear in the plaintext.
- */
- public List<ChunkHash> getAllChunks() {
- return mAllChunks;
- }
-
- /** Returns an unmodifiable list of the new chunks in the backup. */
- public List<EncryptedChunk> getNewChunks() {
- return mNewChunks;
- }
-
- /** Returns the message digest of the backup. */
- public byte[] getDigest() {
- return mDigest;
- }
- }
-}
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
deleted file mode 100644
index 9bf148d..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
+++ /dev/null
@@ -1,378 +0,0 @@
-/*
- * 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/BackupStreamEncrypter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupStreamEncrypter.java
deleted file mode 100644
index 45798d3..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupStreamEncrypter.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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 com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ChunkEncryptor;
-import com.android.server.backup.encryption.chunking.ChunkHasher;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker;
-import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
-import com.android.server.backup.encryption.chunking.cdc.IsChunkBreakpoint;
-import com.android.server.backup.encryption.chunking.cdc.RabinFingerprint64;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.crypto.SecretKey;
-
-/**
- * Splits backup data into variable-sized chunks using content-defined chunking, then encrypts the
- * chunks. Given a hash of the SHA-256s of existing chunks, performs an incremental backup (i.e.,
- * only encrypts new chunks).
- */
-public class BackupStreamEncrypter implements BackupEncrypter {
- private static final String TAG = "BackupStreamEncryptor";
-
- private final InputStream mData;
- private final int mMinChunkSizeBytes;
- private final int mMaxChunkSizeBytes;
- private final int mAverageChunkSizeBytes;
-
- /**
- * A new instance over the given distribution of chunk sizes.
- *
- * @param data The data to be backed up.
- * @param minChunkSizeBytes The minimum chunk size. No chunk will be smaller than this.
- * @param maxChunkSizeBytes The maximum chunk size. No chunk will be larger than this.
- * @param averageChunkSizeBytes The average chunk size. The mean size of chunks will be roughly
- * this (with a few tens of bytes of overhead for the initialization vector and message
- * authentication code).
- */
- public BackupStreamEncrypter(
- InputStream data,
- int minChunkSizeBytes,
- int maxChunkSizeBytes,
- int averageChunkSizeBytes) {
- this.mData = data;
- this.mMinChunkSizeBytes = minChunkSizeBytes;
- this.mMaxChunkSizeBytes = maxChunkSizeBytes;
- this.mAverageChunkSizeBytes = averageChunkSizeBytes;
- }
-
- @Override
- public Result backup(
- SecretKey secretKey, byte[] fingerprintMixerSalt, Set<ChunkHash> existingChunks)
- throws IOException, GeneralSecurityException {
- MessageDigest messageDigest =
- MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
- RabinFingerprint64 rabinFingerprint64 = new RabinFingerprint64();
- FingerprintMixer fingerprintMixer = new FingerprintMixer(secretKey, fingerprintMixerSalt);
- IsChunkBreakpoint isChunkBreakpoint =
- new IsChunkBreakpoint(mAverageChunkSizeBytes - mMinChunkSizeBytes);
- ContentDefinedChunker chunker =
- new ContentDefinedChunker(
- mMinChunkSizeBytes,
- mMaxChunkSizeBytes,
- rabinFingerprint64,
- fingerprintMixer,
- isChunkBreakpoint);
- ChunkHasher chunkHasher = new ChunkHasher(secretKey);
- ChunkEncryptor encryptor = new ChunkEncryptor(secretKey, new SecureRandom());
- Set<ChunkHash> includedChunks = new HashSet<>();
- // New chunks will be added only once to this list, even if they occur multiple times.
- List<EncryptedChunk> newChunks = new ArrayList<>();
- // All chunks (including multiple occurrences) will be added to the chunkListing.
- List<ChunkHash> chunkListing = new ArrayList<>();
-
- includedChunks.addAll(existingChunks);
-
- chunker.chunkify(
- mData,
- chunk -> {
- messageDigest.update(chunk);
- ChunkHash key = chunkHasher.computeHash(chunk);
-
- if (!includedChunks.contains(key)) {
- newChunks.add(encryptor.encrypt(key, chunk));
- includedChunks.add(key);
- }
- chunkListing.add(key);
- });
-
- Slog.i(
- TAG,
- String.format(
- "Chunks: %d total, %d unique, %d new",
- chunkListing.size(), new HashSet<>(chunkListing).size(), newChunks.size()));
- return new Result(
- Collections.unmodifiableList(chunkListing),
- Collections.unmodifiableList(newChunks),
- messageDigest.digest());
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ClearCryptoStateTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ClearCryptoStateTask.java
deleted file mode 100644
index 8f35db6..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ClearCryptoStateTask.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.content.Context;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.storage.BackupEncryptionDb;
-import com.android.server.backup.encryption.storage.EncryptionDbException;
-
-import java.io.IOException;
-
-/**
- * Task to clear local crypto state.
- *
- * <p>Needs to run whenever the user changes their backup account.
- */
-public class ClearCryptoStateTask {
- private static final String TAG = "ClearCryptoStateTask";
-
- private final Context mContext;
- private final CryptoSettings mCryptoSettings;
-
- /**
- * A new instance.
- *
- * @param context for finding local storage.
- * @param cryptoSettings to clear
- */
- public ClearCryptoStateTask(Context context, CryptoSettings cryptoSettings) {
- mContext = context;
- mCryptoSettings = cryptoSettings;
- }
-
- /** Deletes all local state for backup (not restore). */
- public void run() {
- Slog.d(TAG, "Clearing local crypto state.");
- try {
- BackupEncryptionDb.newInstance(mContext).clear();
- } catch (EncryptionDbException e) {
- Slog.e(TAG, "Error clearing encryption database", e);
- }
- mCryptoSettings.clearAllSettingsForBackup();
- try {
- ProtoStore.createChunkListingStore(mContext).deleteAllProtos();
- } catch (IOException e) {
- Slog.e(TAG, "Error clearing chunk listing store", e);
- }
- try {
- ProtoStore.createKeyValueListingStore(mContext).deleteAllProtos();
- } catch (IOException e) {
- Slog.e(TAG, "Error clearing key-value store", e);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
deleted file mode 100644
index f67f100..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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 java.io.Closeable;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-/**
- * Accepts the plaintext bytes of decrypted chunks and writes them to some output. Also keeps track
- * of the message digest of the chunks.
- */
-public interface DecryptedChunkOutput extends Closeable {
- /**
- * Opens whatever output the implementation chooses, ready to process chunks.
- *
- * @return {@code this}, to allow use with try-with-resources
- */
- DecryptedChunkOutput open() throws IOException, NoSuchAlgorithmException;
-
- /**
- * Writes the plaintext bytes of chunk to whatever output the implementation chooses. Also
- * updates the digest with the chunk.
- *
- * <p>You must call {@link #open()} before this method, and you may not call it after calling
- * {@link Closeable#close()}.
- *
- * @param plaintextBuffer An array containing the bytes of the plaintext of the chunk, starting
- * at index 0.
- * @param length The length in bytes of the plaintext contained in {@code plaintextBuffer}.
- */
- void processChunk(byte[] plaintextBuffer, int length)
- throws IOException, InvalidKeyException, NoSuchAlgorithmException;
-
- /**
- * Returns the message digest of all the chunks processed by {@link #processChunk}.
- *
- * <p>You must call {@link Closeable#close()} before calling this method.
- */
- byte[] getDigest() throws NoSuchAlgorithmException;
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedBackupTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedBackupTask.java
deleted file mode 100644
index ef13f23..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedBackupTask.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * 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.annotation.Nullable;
-import android.annotation.TargetApi;
-import android.os.Build.VERSION_CODES;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.BackupFileBuilder;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-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;
-
-/**
- * Task which reads encrypted chunks from a {@link BackupEncrypter}, builds a backup file and
- * uploads it to the server.
- */
-@TargetApi(VERSION_CODES.P)
-public class EncryptedBackupTask {
- 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 TAG = "EncryptedBackupTask";
-
- private final CryptoBackupServer mCryptoBackupServer;
- private final SecureRandom mSecureRandom;
- private final String mPackageName;
- private final ByteArrayOutputStream mBackupDataOutput;
- private final BackupEncrypter mBackupEncrypter;
- private final AtomicBoolean mCancelled;
-
- /** Creates a new instance which reads data from the given input stream. */
- public EncryptedBackupTask(
- CryptoBackupServer cryptoBackupServer,
- SecureRandom secureRandom,
- String packageName,
- BackupEncrypter backupEncrypter) {
- mCryptoBackupServer = cryptoBackupServer;
- mSecureRandom = secureRandom;
- mPackageName = packageName;
- mBackupEncrypter = backupEncrypter;
-
- mBackupDataOutput = new ByteArrayOutputStream();
- mCancelled = new AtomicBoolean(false);
- }
-
- /**
- * Creates a non-incremental backup file and uploads it to the server.
- *
- * @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
- * full backup. May be {@code null} for a key-value backup.
- */
- public ChunksMetadataProto.ChunkListing performNonIncrementalBackup(
- SecretKey tertiaryKey,
- WrappedKeyProto.WrappedKey wrappedTertiaryKey,
- @Nullable byte[] fingerprintMixerSalt)
- throws IOException, GeneralSecurityException {
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- performBackup(
- tertiaryKey,
- fingerprintMixerSalt,
- BackupFileBuilder.createForNonIncremental(mBackupDataOutput),
- new HashSet<>());
-
- throwIfCancelled();
-
- newChunkListing.documentId =
- mCryptoBackupServer.uploadNonIncrementalBackup(
- mPackageName, mBackupDataOutput.toByteArray(), wrappedTertiaryKey);
-
- return newChunkListing;
- }
-
- /** Creates an incremental backup file and uploads it to the server. */
- public ChunksMetadataProto.ChunkListing performIncrementalBackup(
- SecretKey tertiaryKey,
- WrappedKeyProto.WrappedKey wrappedTertiaryKey,
- ChunksMetadataProto.ChunkListing oldChunkListing)
- throws IOException, GeneralSecurityException {
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- performBackup(
- tertiaryKey,
- oldChunkListing.fingerprintMixerSalt,
- BackupFileBuilder.createForIncremental(mBackupDataOutput, oldChunkListing),
- getChunkHashes(oldChunkListing));
-
- throwIfCancelled();
-
- String oldDocumentId = oldChunkListing.documentId;
- Slog.v(TAG, "Old doc id: " + oldDocumentId);
-
- newChunkListing.documentId =
- mCryptoBackupServer.uploadIncrementalBackup(
- mPackageName,
- oldDocumentId,
- mBackupDataOutput.toByteArray(),
- wrappedTertiaryKey);
- return newChunkListing;
- }
-
- /**
- * Signals to the task that the backup has been cancelled. If the upload has not yet started
- * then the task will not upload any data to the server or save the new chunk listing.
- */
- public void cancel() {
- mCancelled.getAndSet(true);
- }
-
- private void throwIfCancelled() {
- if (mCancelled.get()) {
- throw new CancellationException("EncryptedBackupTask was cancelled");
- }
- }
-
- private ChunksMetadataProto.ChunkListing performBackup(
- SecretKey tertiaryKey,
- @Nullable byte[] fingerprintMixerSalt,
- BackupFileBuilder backupFileBuilder,
- Set<ChunkHash> existingChunkHashes)
- throws IOException, GeneralSecurityException {
- BackupEncrypter.Result result =
- mBackupEncrypter.backup(tertiaryKey, fingerprintMixerSalt, existingChunkHashes);
- backupFileBuilder.writeChunks(result.getAllChunks(), buildChunkMap(result.getNewChunks()));
-
- ChunksMetadataProto.ChunkOrdering chunkOrdering =
- backupFileBuilder.getNewChunkOrdering(result.getDigest());
- backupFileBuilder.finish(buildMetadata(tertiaryKey, chunkOrdering));
-
- return backupFileBuilder.getNewChunkListing(fingerprintMixerSalt);
- }
-
- /** Returns a set containing the hashes of every chunk in the given listing. */
- private static Set<ChunkHash> getChunkHashes(ChunksMetadataProto.ChunkListing chunkListing) {
- Set<ChunkHash> hashes = new HashSet<>();
- for (ChunksMetadataProto.Chunk chunk : chunkListing.chunks) {
- hashes.add(new ChunkHash(chunk.hash));
- }
- return hashes;
- }
-
- /** Returns a map from chunk hash to chunk containing every chunk in the given list. */
- private static Map<ChunkHash, EncryptedChunk> buildChunkMap(List<EncryptedChunk> chunks) {
- Map<ChunkHash, EncryptedChunk> chunkMap = new HashMap<>();
- for (EncryptedChunk chunk : chunks) {
- chunkMap.put(chunk.key(), chunk);
- }
- return chunkMap;
- }
-
- private ChunksMetadataProto.ChunksMetadata buildMetadata(
- SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
- throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
- InvalidAlgorithmParameterException, NoSuchAlgorithmException,
- ShortBufferException, NoSuchPaddingException {
- ChunksMetadataProto.ChunksMetadata metaData = new ChunksMetadataProto.ChunksMetadata();
- metaData.cipherType = ChunksMetadataProto.AES_256_GCM;
- metaData.checksumType = ChunksMetadataProto.SHA_256;
- metaData.chunkOrdering = encryptChunkOrdering(tertiaryKey, chunkOrdering);
- return metaData;
- }
-
- private byte[] encryptChunkOrdering(
- SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
- throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
- NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, ShortBufferException {
- Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-
- byte[] nonce = generateNonce();
-
- cipher.init(
- Cipher.ENCRYPT_MODE,
- tertiaryKey,
- new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));
-
- byte[] orderingBytes = ChunksMetadataProto.ChunkOrdering.toByteArray(chunkOrdering);
- // We prepend the nonce to the ordering.
- byte[] output =
- Arrays.copyOf(
- nonce,
- GCM_NONCE_LENGTH_BYTES + orderingBytes.length + GCM_TAG_LENGTH_BYTES);
-
- cipher.doFinal(
- orderingBytes,
- /*inputOffset=*/ 0,
- /*inputLen=*/ orderingBytes.length,
- output,
- /*outputOffset=*/ GCM_NONCE_LENGTH_BYTES);
-
- return output;
- }
-
- private byte[] generateNonce() {
- byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
- mSecureRandom.nextBytes(nonce);
- return nonce;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java
deleted file mode 100644
index 71588f6..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * 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.internal.util.Preconditions.checkState;
-
-import android.annotation.Nullable;
-import android.app.backup.BackupTransport;
-import android.content.Context;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.FullBackupDataProcessor;
-import com.android.server.backup.encryption.StreamUtils;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-import java.security.SecureRandom;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-/**
- * Accepts backup data from a {@link InputStream} and passes it to the encrypted full data backup
- * path.
- */
-public class EncryptedFullBackupDataProcessor implements FullBackupDataProcessor {
-
- private static final String TAG = "EncryptedFullBackupDP";
-
- private final Context mContext;
- private final ExecutorService mExecutorService;
- private final CryptoBackupServer mCryptoBackupServer;
- private final SecureRandom mSecureRandom;
- private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
- private final String mPackageName;
-
- @Nullable private InputStream mInputStream;
- @Nullable private PipedOutputStream mOutputStream;
- @Nullable private EncryptedFullBackupTask mBackupTask;
- @Nullable private Future<Void> mBackupTaskFuture;
- @Nullable private FullBackupCallbacks mFullBackupCallbacks;
-
- public EncryptedFullBackupDataProcessor(
- Context context,
- ExecutorService executorService,
- CryptoBackupServer cryptoBackupServer,
- SecureRandom secureRandom,
- RecoverableKeyStoreSecondaryKey secondaryKey,
- String packageName) {
- mContext = Objects.requireNonNull(context);
- mExecutorService = Objects.requireNonNull(executorService);
- mCryptoBackupServer = Objects.requireNonNull(cryptoBackupServer);
- mSecureRandom = Objects.requireNonNull(secureRandom);
- mSecondaryKey = Objects.requireNonNull(secondaryKey);
- mPackageName = Objects.requireNonNull(packageName);
- }
-
- @Override
- public boolean initiate(InputStream inputStream) throws IOException {
- checkState(mBackupTask == null, "initiate() twice");
-
- this.mInputStream = inputStream;
- mOutputStream = new PipedOutputStream();
-
- mBackupTask =
- EncryptedFullBackupTask.newInstance(
- mContext,
- mCryptoBackupServer,
- mSecureRandom,
- mSecondaryKey,
- mPackageName,
- new PipedInputStream(mOutputStream));
-
- return true;
- }
-
- @Override
- public void start() {
- checkState(mBackupTask != null, "start() before initiate()");
- mBackupTaskFuture = mExecutorService.submit(mBackupTask);
- }
-
- @Override
- public int pushData(int numBytes) {
- checkState(
- mBackupTaskFuture != null && mInputStream != null && mOutputStream != null,
- "pushData() before start()");
-
- // If the upload has failed then stop without pushing any more bytes.
- if (mBackupTaskFuture.isDone()) {
- Optional<Exception> exception = getTaskException();
- Slog.e(TAG, "Encrypted upload failed", exception.orElse(null));
- if (exception.isPresent()) {
- reportNetworkFailureIfNecessary(exception.get());
-
- if (exception.get().getCause() instanceof SizeQuotaExceededException) {
- return BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
- }
- }
-
- return BackupTransport.TRANSPORT_ERROR;
- }
-
- try {
- StreamUtils.copyStream(mInputStream, mOutputStream, numBytes);
- } catch (IOException e) {
- Slog.e(TAG, "IOException when processing backup", e);
- return BackupTransport.TRANSPORT_ERROR;
- }
-
- return BackupTransport.TRANSPORT_OK;
- }
-
- @Override
- public void cancel() {
- checkState(mBackupTaskFuture != null && mBackupTask != null, "cancel() before start()");
- mBackupTask.cancel();
- closeStreams();
- }
-
- @Override
- public int finish() {
- checkState(mBackupTaskFuture != null, "finish() before start()");
-
- // getTaskException() waits for the task to finish. We must close the streams first, which
- // causes the task to finish, otherwise it will block forever.
- closeStreams();
- Optional<Exception> exception = getTaskException();
-
- if (exception.isPresent()) {
- Slog.e(TAG, "Exception during encrypted full backup", exception.get());
- reportNetworkFailureIfNecessary(exception.get());
-
- if (exception.get().getCause() instanceof SizeQuotaExceededException) {
- return BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
- }
- return BackupTransport.TRANSPORT_ERROR;
-
- } else {
- if (mFullBackupCallbacks != null) {
- mFullBackupCallbacks.onSuccess();
- }
-
- return BackupTransport.TRANSPORT_OK;
- }
- }
-
- private void closeStreams() {
- StreamUtils.closeQuietly(mInputStream);
- StreamUtils.closeQuietly(mOutputStream);
- }
-
- @Override
- public void handleCheckSizeRejectionZeroBytes() {
- cancel();
- }
-
- @Override
- public void handleCheckSizeRejectionQuotaExceeded() {
- cancel();
- }
-
- @Override
- public void handleSendBytesQuotaExceeded() {
- cancel();
- }
-
- @Override
- public void attachCallbacks(FullBackupCallbacks fullBackupCallbacks) {
- this.mFullBackupCallbacks = fullBackupCallbacks;
- }
-
- private void reportNetworkFailureIfNecessary(Exception exception) {
- if (!(exception.getCause() instanceof SizeQuotaExceededException)
- && mFullBackupCallbacks != null) {
- mFullBackupCallbacks.onTransferFailed();
- }
- }
-
- private Optional<Exception> getTaskException() {
- if (mBackupTaskFuture != null) {
- try {
- mBackupTaskFuture.get();
- } catch (InterruptedException | ExecutionException e) {
- return Optional.of(e);
- }
- }
- return Optional.empty();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java
deleted file mode 100644
index a938d71..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.content.Context;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.StreamUtils;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.concurrent.Callable;
-
-import javax.crypto.SecretKey;
-
-/**
- * Task which reads a stream of plaintext full backup data, chunks it, encrypts it and uploads it to
- * the server.
- *
- * <p>Once the backup completes or fails, closes the input stream.
- */
-public class EncryptedFullBackupTask implements Callable<Void> {
- private static final String TAG = "EncryptedFullBackupTask";
-
- private static final int MIN_CHUNK_SIZE_BYTES = 2 * 1024;
- private static final int MAX_CHUNK_SIZE_BYTES = 64 * 1024;
- private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * 1024;
-
- // TODO(b/69350270): Remove this hard-coded salt and related logic once we feel confident that
- // incremental backup has happened at least once for all existing packages/users since we moved
- // to
- // using a randomly generated salt.
- //
- // The hard-coded fingerprint mixer salt was used for a short time period before replaced by one
- // that is randomly generated on initial non-incremental backup and stored in ChunkListing to be
- // reused for succeeding incremental backups. If an old ChunkListing does not have a
- // fingerprint_mixer_salt, we assume that it was last backed up before a randomly generated salt
- // is used so we use the hardcoded salt and set ChunkListing#fingerprint_mixer_salt to this
- // value.
- // Eventually all backup ChunkListings will have this field set and then we can remove the
- // default
- // value in the code.
- static final byte[] DEFAULT_FINGERPRINT_MIXER_SALT =
- Arrays.copyOf(new byte[] {20, 23}, FingerprintMixer.SALT_LENGTH_BYTES);
-
- private final ProtoStore<ChunkListing> mChunkListingStore;
- private final TertiaryKeyManager mTertiaryKeyManager;
- private final InputStream mInputStream;
- private final EncryptedBackupTask mTask;
- private final String mPackageName;
- private final SecureRandom mSecureRandom;
-
- /** Creates a new instance with the default min, max and average chunk sizes. */
- public static EncryptedFullBackupTask newInstance(
- Context context,
- CryptoBackupServer cryptoBackupServer,
- SecureRandom secureRandom,
- RecoverableKeyStoreSecondaryKey secondaryKey,
- String packageName,
- InputStream inputStream)
- throws IOException {
- EncryptedBackupTask encryptedBackupTask =
- new EncryptedBackupTask(
- cryptoBackupServer,
- secureRandom,
- packageName,
- new BackupStreamEncrypter(
- inputStream,
- MIN_CHUNK_SIZE_BYTES,
- MAX_CHUNK_SIZE_BYTES,
- AVERAGE_CHUNK_SIZE_BYTES));
- TertiaryKeyManager tertiaryKeyManager =
- new TertiaryKeyManager(
- context,
- secureRandom,
- TertiaryKeyRotationScheduler.getInstance(context),
- secondaryKey,
- packageName);
-
- return new EncryptedFullBackupTask(
- ProtoStore.createChunkListingStore(context),
- tertiaryKeyManager,
- encryptedBackupTask,
- inputStream,
- packageName,
- new SecureRandom());
- }
-
- @VisibleForTesting
- EncryptedFullBackupTask(
- ProtoStore<ChunkListing> chunkListingStore,
- TertiaryKeyManager tertiaryKeyManager,
- EncryptedBackupTask task,
- InputStream inputStream,
- String packageName,
- SecureRandom secureRandom) {
- mChunkListingStore = chunkListingStore;
- mTertiaryKeyManager = tertiaryKeyManager;
- mInputStream = inputStream;
- mTask = task;
- mPackageName = packageName;
- mSecureRandom = secureRandom;
- }
-
- @Override
- public Void call() throws Exception {
- try {
- Optional<ChunkListing> maybeOldChunkListing =
- mChunkListingStore.loadProto(mPackageName);
-
- if (maybeOldChunkListing.isPresent()) {
- Slog.i(TAG, "Found previous chunk listing for " + mPackageName);
- }
-
- // If the key has been rotated then we must re-encrypt all of the backup data.
- if (mTertiaryKeyManager.wasKeyRotated()) {
- Slog.i(
- TAG,
- "Key was rotated or newly generated for "
- + mPackageName
- + ", so performing a full backup.");
- maybeOldChunkListing = Optional.empty();
- mChunkListingStore.deleteProto(mPackageName);
- }
-
- SecretKey tertiaryKey = mTertiaryKeyManager.getKey();
- WrappedKeyProto.WrappedKey wrappedTertiaryKey = mTertiaryKeyManager.getWrappedKey();
-
- ChunkListing newChunkListing;
- if (!maybeOldChunkListing.isPresent()) {
- byte[] fingerprintMixerSalt = new byte[FingerprintMixer.SALT_LENGTH_BYTES];
- mSecureRandom.nextBytes(fingerprintMixerSalt);
- newChunkListing =
- mTask.performNonIncrementalBackup(
- tertiaryKey, wrappedTertiaryKey, fingerprintMixerSalt);
- } else {
- ChunkListing oldChunkListing = maybeOldChunkListing.get();
-
- if (oldChunkListing.fingerprintMixerSalt == null
- || oldChunkListing.fingerprintMixerSalt.length == 0) {
- oldChunkListing.fingerprintMixerSalt = DEFAULT_FINGERPRINT_MIXER_SALT;
- }
-
- newChunkListing =
- mTask.performIncrementalBackup(
- tertiaryKey, wrappedTertiaryKey, oldChunkListing);
- }
-
- mChunkListingStore.saveProto(mPackageName, newChunkListing);
- Slog.v(TAG, "Saved chunk listing for " + mPackageName);
- } catch (IOException e) {
- Slog.e(TAG, "Storage exception, wiping state");
- mChunkListingStore.deleteProto(mPackageName);
- throw e;
- } finally {
- StreamUtils.closeQuietly(mInputStream);
- }
-
- return null;
- }
-
- /**
- * Signals to the task that the backup has been cancelled. If the upload has not yet started
- * then the task will not upload any data to the server or save the new chunk listing.
- *
- * <p>You must then terminate the input stream.
- */
- public void cancel() {
- mTask.cancel();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java
deleted file mode 100644
index 04381af..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.internal.util.Preconditions.checkArgument;
-
-import android.annotation.Nullable;
-import android.content.Context;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.FullRestoreDataProcessor;
-import com.android.server.backup.encryption.FullRestoreDownloader;
-import com.android.server.backup.encryption.StreamUtils;
-import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.ShortBufferException;
-
-/** Downloads the encrypted backup file, decrypts it and passes the data to backup manager. */
-public class EncryptedFullRestoreTask implements FullRestoreDataProcessor {
- private static final String DEFAULT_TEMPORARY_FOLDER = "encrypted_restore_temp";
- private static final String ENCRYPTED_FILE_NAME = "encrypted_restore";
- private static final String DECRYPTED_FILE_NAME = "decrypted_restore";
-
- private final FullRestoreToFileTask mFullRestoreToFileTask;
- private final BackupFileDecryptorTask mBackupFileDecryptorTask;
- private final File mEncryptedFile;
- private final File mDecryptedFile;
- @Nullable private InputStream mDecryptedFileInputStream;
-
- /**
- * Creates a new task which stores temporary files in the files directory.
- *
- * @param fullRestoreDownloader which will download the backup file
- * @param tertiaryKey which the backup file is encrypted with
- */
- public static EncryptedFullRestoreTask newInstance(
- Context context, FullRestoreDownloader fullRestoreDownloader, SecretKey tertiaryKey)
- throws NoSuchAlgorithmException, NoSuchPaddingException {
- File temporaryFolder = new File(context.getFilesDir(), DEFAULT_TEMPORARY_FOLDER);
- temporaryFolder.mkdirs();
- return new EncryptedFullRestoreTask(
- temporaryFolder, fullRestoreDownloader, new BackupFileDecryptorTask(tertiaryKey));
- }
-
- @VisibleForTesting
- EncryptedFullRestoreTask(
- File temporaryFolder,
- FullRestoreDownloader fullRestoreDownloader,
- BackupFileDecryptorTask backupFileDecryptorTask) {
- checkArgument(temporaryFolder.isDirectory(), "Temporary folder must be existing directory");
-
- mEncryptedFile = new File(temporaryFolder, ENCRYPTED_FILE_NAME);
- mDecryptedFile = new File(temporaryFolder, DECRYPTED_FILE_NAME);
-
- mFullRestoreToFileTask = new FullRestoreToFileTask(fullRestoreDownloader);
- mBackupFileDecryptorTask = backupFileDecryptorTask;
- }
-
- /**
- * Reads the next decrypted bytes into the given buffer.
- *
- * <p>During the first call this method will download the backup file from the server, decrypt
- * it and save it to disk. It will then read the bytes from the file on disk.
- *
- * <p>Once this method has read all the bytes of the file, the caller must call {@link #finish}
- * to clean up.
- *
- * @return the number of bytes read, or {@code -1} on reaching the end of the file
- */
- @Override
- public int readNextChunk(byte[] buffer) throws IOException {
- if (mDecryptedFileInputStream == null) {
- try {
- mDecryptedFileInputStream = downloadAndDecryptBackup();
- } catch (BadPaddingException
- | InvalidKeyException
- | NoSuchAlgorithmException
- | IllegalBlockSizeException
- | ShortBufferException
- | EncryptedRestoreException
- | InvalidAlgorithmParameterException e) {
- throw new IOException("Encryption issue", e);
- }
- }
-
- return mDecryptedFileInputStream.read(buffer);
- }
-
- private InputStream downloadAndDecryptBackup()
- throws IOException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
- IllegalBlockSizeException, ShortBufferException, EncryptedRestoreException,
- InvalidAlgorithmParameterException {
- mFullRestoreToFileTask.restoreToFile(mEncryptedFile);
- mBackupFileDecryptorTask.decryptFile(
- mEncryptedFile, new DecryptedChunkFileOutput(mDecryptedFile));
- mEncryptedFile.delete();
- return new BufferedInputStream(new FileInputStream(mDecryptedFile));
- }
-
- /** Cleans up temporary files. */
- @Override
- public void finish(FullRestoreDownloader.FinishType unusedFinishType) {
- // The download is finished and log sent during RestoreToFileTask#restoreToFile(), so we
- // don't need to do either of those things here.
-
- StreamUtils.closeQuietly(mDecryptedFileInputStream);
- mEncryptedFile.delete();
- mDecryptedFile.delete();
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java
deleted file mode 100644
index 619438c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * 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.annotation.Nullable;
-import android.app.backup.BackupDataInput;
-import android.content.Context;
-import android.os.ParcelFileDescriptor;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.LockScreenRequiredException;
-import android.util.Pair;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
-
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import java.security.SecureRandom;
-import java.security.UnrecoverableKeyException;
-import java.util.Optional;
-
-// TODO(b/141975695): Create a base class for EncryptedKvBackupTask and EncryptedFullBackupTask.
-/** Performs encrypted key value backup, handling rotating the tertiary key as necessary. */
-public class EncryptedKvBackupTask {
- private static final String TAG = "EncryptedKvBackupTask";
-
- private final TertiaryKeyManager mTertiaryKeyManager;
- private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
- private final ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
- private final ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
- private final KvBackupEncrypter mKvBackupEncrypter;
- private final EncryptedBackupTask mEncryptedBackupTask;
- private final String mPackageName;
-
- /** Constructs new instances of {@link EncryptedKvBackupTask}. */
- public static class EncryptedKvBackupTaskFactory {
- /**
- * Creates a new instance.
- *
- * <p>Either initializes encrypted backup or loads an existing secondary key as necessary.
- *
- * @param cryptoSettings to load secondary key state from
- * @param fileDescriptor to read the backup data from
- */
- public EncryptedKvBackupTask newInstance(
- Context context,
- SecureRandom secureRandom,
- CryptoBackupServer cryptoBackupServer,
- CryptoSettings cryptoSettings,
- RecoverableKeyStoreSecondaryKeyManager
- .RecoverableKeyStoreSecondaryKeyManagerProvider
- recoverableSecondaryKeyManagerProvider,
- ParcelFileDescriptor fileDescriptor,
- String packageName)
- throws IOException, UnrecoverableKeyException, LockScreenRequiredException,
- InternalRecoveryServiceException, InvalidKeyException {
- RecoverableKeyStoreSecondaryKey secondaryKey =
- new InitializeRecoverableSecondaryKeyTask(
- context,
- cryptoSettings,
- recoverableSecondaryKeyManagerProvider.get(),
- cryptoBackupServer)
- .run();
- KvBackupEncrypter backupEncrypter =
- new KvBackupEncrypter(new BackupDataInput(fileDescriptor.getFileDescriptor()));
- TertiaryKeyManager tertiaryKeyManager =
- new TertiaryKeyManager(
- context,
- secureRandom,
- TertiaryKeyRotationScheduler.getInstance(context),
- secondaryKey,
- packageName);
-
- return new EncryptedKvBackupTask(
- tertiaryKeyManager,
- ProtoStore.createKeyValueListingStore(context),
- secondaryKey,
- ProtoStore.createChunkListingStore(context),
- backupEncrypter,
- new EncryptedBackupTask(
- cryptoBackupServer, secureRandom, packageName, backupEncrypter),
- packageName);
- }
- }
-
- @VisibleForTesting
- EncryptedKvBackupTask(
- TertiaryKeyManager tertiaryKeyManager,
- ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore,
- RecoverableKeyStoreSecondaryKey secondaryKey,
- ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore,
- KvBackupEncrypter kvBackupEncrypter,
- EncryptedBackupTask encryptedBackupTask,
- String packageName) {
- mTertiaryKeyManager = tertiaryKeyManager;
- mSecondaryKey = secondaryKey;
- mKeyValueListingStore = keyValueListingStore;
- mChunkListingStore = chunkListingStore;
- mKvBackupEncrypter = kvBackupEncrypter;
- mEncryptedBackupTask = encryptedBackupTask;
- mPackageName = packageName;
- }
-
- /**
- * Reads backup data from the file descriptor provided in the construtor, encrypts it and
- * uploads it to the server.
- *
- * <p>The {@code incremental} flag indicates if the backup data provided is incremental or a
- * complete set. Incremental backup is not possible if no previous crypto state exists, or the
- * tertiary key must be rotated in the next backup. If the caller requests incremental backup
- * but it is not possible, then the backup will not start and this method will throw {@link
- * NonIncrementalBackupRequiredException}.
- *
- * <p>TODO(b/70704456): Update return code to indicate that we require non-incremental backup.
- *
- * @param incremental {@code true} if the data provided is a diff from the previous backup,
- * {@code false} if it is a complete set
- * @throws NonIncrementalBackupRequiredException if the caller provides an incremental backup but the task
- * requires non-incremental backup
- */
- public void performBackup(boolean incremental)
- throws GeneralSecurityException, IOException, NoSuchMethodException,
- InstantiationException, IllegalAccessException, InvocationTargetException,
- NonIncrementalBackupRequiredException {
- if (mTertiaryKeyManager.wasKeyRotated()) {
- Slog.d(TAG, "Tertiary key is new so clearing package state.");
- deleteListings(mPackageName);
- }
-
- Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
- oldListings = getListingsAndEnsureConsistency(mPackageName);
-
- if (oldListings.isPresent() && !incremental) {
- Slog.d(
- TAG,
- "Non-incremental backup requested but incremental state existed, clearing it");
- deleteListings(mPackageName);
- oldListings = Optional.empty();
- }
-
- if (!oldListings.isPresent() && incremental) {
- // If we don't have any state then we require a non-incremental backup, but this backup
- // is incremental.
- throw new NonIncrementalBackupRequiredException();
- }
-
- if (oldListings.isPresent()) {
- mKvBackupEncrypter.setOldKeyValueListing(oldListings.get().first);
- }
-
- ChunksMetadataProto.ChunkListing newChunkListing;
- if (oldListings.isPresent()) {
- Slog.v(TAG, "Old listings existed, performing incremental backup");
- newChunkListing =
- mEncryptedBackupTask.performIncrementalBackup(
- mTertiaryKeyManager.getKey(),
- mTertiaryKeyManager.getWrappedKey(),
- oldListings.get().second);
- } else {
- Slog.v(TAG, "Old listings did not exist, performing non-incremental backup");
- // kv backups don't use this salt because they don't involve content-defined chunking.
- byte[] fingerprintMixerSalt = null;
- newChunkListing =
- mEncryptedBackupTask.performNonIncrementalBackup(
- mTertiaryKeyManager.getKey(),
- mTertiaryKeyManager.getWrappedKey(),
- fingerprintMixerSalt);
- }
-
- Slog.v(TAG, "Backup and upload succeeded, saving new listings");
- saveListings(mPackageName, mKvBackupEncrypter.getNewKeyValueListing(), newChunkListing);
- }
-
- private Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
- getListingsAndEnsureConsistency(String packageName)
- throws IOException, InvocationTargetException, NoSuchMethodException,
- InstantiationException, IllegalAccessException {
- Optional<KeyValueListingProto.KeyValueListing> keyValueListing =
- mKeyValueListingStore.loadProto(packageName);
- Optional<ChunksMetadataProto.ChunkListing> chunkListing =
- mChunkListingStore.loadProto(packageName);
-
- // Normally either both protos exist or neither exist, but we correct this just in case.
- boolean bothPresent = keyValueListing.isPresent() && chunkListing.isPresent();
- if (!bothPresent) {
- Slog.d(
- TAG,
- "Both listing were not present, clearing state, key value="
- + keyValueListing.isPresent()
- + ", chunk="
- + chunkListing.isPresent());
- deleteListings(packageName);
- return Optional.empty();
- }
-
- return Optional.of(Pair.create(keyValueListing.get(), chunkListing.get()));
- }
-
- private void saveListings(
- String packageName,
- KeyValueListingProto.KeyValueListing keyValueListing,
- ChunksMetadataProto.ChunkListing chunkListing) {
- try {
- mKeyValueListingStore.saveProto(packageName, keyValueListing);
- mChunkListingStore.saveProto(packageName, chunkListing);
- } catch (IOException e) {
- // If a problem occurred while saving either listing then they may be inconsistent, so
- // delete
- // both.
- Slog.w(TAG, "Unable to save listings, deleting both for consistency", e);
- deleteListings(packageName);
- }
- }
-
- private void deleteListings(String packageName) {
- mKeyValueListingStore.deleteProto(packageName);
- mChunkListingStore.deleteProto(packageName);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java
deleted file mode 100644
index 12b4459..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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.internal.util.Preconditions.checkArgument;
-
-import android.app.backup.BackupDataOutput;
-import android.content.Context;
-import android.os.ParcelFileDescriptor;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.FullRestoreDownloader;
-import com.android.server.backup.encryption.chunking.ChunkHasher;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.backup.encryption.keys.RestoreKeyFetcher;
-import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
-import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.io.File;
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.KeyException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.ShortBufferException;
-
-/**
- * Performs a key value restore by downloading the backup set, decrypting it and writing it to the
- * file provided by backup manager.
- */
-public class EncryptedKvRestoreTask {
- private static final String ENCRYPTED_FILE_NAME = "encrypted_kv";
-
- private final File mTemporaryFolder;
- private final ChunkHasher mChunkHasher;
- private final FullRestoreToFileTask mFullRestoreToFileTask;
- private final BackupFileDecryptorTask mBackupFileDecryptorTask;
-
- /** Constructs new instances of the task. */
- public static class EncryptedKvRestoreTaskFactory {
- /**
- * Constructs a new instance.
- *
- * <p>Fetches the appropriate secondary key and uses this to unwrap the tertiary key. Stores
- * temporary files in {@link Context#getFilesDir()}.
- */
- public EncryptedKvRestoreTask newInstance(
- Context context,
- RecoverableKeyStoreSecondaryKeyManager
- .RecoverableKeyStoreSecondaryKeyManagerProvider
- recoverableSecondaryKeyManagerProvider,
- FullRestoreDownloader fullRestoreDownloader,
- String secondaryKeyAlias,
- WrappedKeyProto.WrappedKey wrappedTertiaryKey)
- throws EncryptedRestoreException, NoSuchAlgorithmException, NoSuchPaddingException,
- KeyException, InvalidAlgorithmParameterException {
- SecretKey tertiaryKey =
- RestoreKeyFetcher.unwrapTertiaryKey(
- recoverableSecondaryKeyManagerProvider,
- secondaryKeyAlias,
- wrappedTertiaryKey);
-
- return new EncryptedKvRestoreTask(
- context.getFilesDir(),
- new ChunkHasher(tertiaryKey),
- new FullRestoreToFileTask(fullRestoreDownloader),
- new BackupFileDecryptorTask(tertiaryKey));
- }
- }
-
- @VisibleForTesting
- EncryptedKvRestoreTask(
- File temporaryFolder,
- ChunkHasher chunkHasher,
- FullRestoreToFileTask fullRestoreToFileTask,
- BackupFileDecryptorTask backupFileDecryptorTask) {
- checkArgument(
- temporaryFolder.isDirectory(), "Temporary folder must be an existing directory");
-
- mTemporaryFolder = temporaryFolder;
- mChunkHasher = chunkHasher;
- mFullRestoreToFileTask = fullRestoreToFileTask;
- mBackupFileDecryptorTask = backupFileDecryptorTask;
- }
-
- /**
- * Runs the restore, writing the pairs in lexicographical order to the given file descriptor.
- *
- * <p>This will block for the duration of the restore.
- *
- * @throws EncryptedRestoreException if there is a problem decrypting or verifying the backup
- */
- public void getRestoreData(ParcelFileDescriptor output)
- throws IOException, EncryptedRestoreException, BadPaddingException,
- InvalidAlgorithmParameterException, NoSuchAlgorithmException,
- IllegalBlockSizeException, ShortBufferException, InvalidKeyException {
- File encryptedFile = new File(mTemporaryFolder, ENCRYPTED_FILE_NAME);
- try {
- downloadDecryptAndWriteBackup(encryptedFile, output);
- } finally {
- encryptedFile.delete();
- }
- }
-
- private void downloadDecryptAndWriteBackup(File encryptedFile, ParcelFileDescriptor output)
- throws EncryptedRestoreException, IOException, BadPaddingException, InvalidKeyException,
- NoSuchAlgorithmException, IllegalBlockSizeException, ShortBufferException,
- InvalidAlgorithmParameterException {
- mFullRestoreToFileTask.restoreToFile(encryptedFile);
- DecryptedChunkKvOutput decryptedChunkKvOutput = new DecryptedChunkKvOutput(mChunkHasher);
- mBackupFileDecryptorTask.decryptFile(encryptedFile, decryptedChunkKvOutput);
-
- BackupDataOutput backupDataOutput = new BackupDataOutput(output.getFileDescriptor());
- for (KeyValuePairProto.KeyValuePair pair : decryptedChunkKvOutput.getPairs()) {
- backupDataOutput.writeEntityHeader(pair.key, pair.value.length);
- backupDataOutput.writeEntityData(pair.value, pair.value.length);
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedRestoreException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedRestoreException.java
deleted file mode 100644
index 487c0d9..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedRestoreException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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;
-
-/** Wraps any exception related to encryption which occurs during restore. */
-public class EncryptedRestoreException extends Exception {
- public EncryptedRestoreException(String message) {
- super(message);
- }
-
- public EncryptedRestoreException(Throwable cause) {
- super(cause);
- }
-
- public EncryptedRestoreException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/FullRestoreToFileTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/FullRestoreToFileTask.java
deleted file mode 100644
index 82f83f9..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/FullRestoreToFileTask.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.internal.util.Preconditions.checkArgument;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.backup.encryption.FullRestoreDownloader;
-import com.android.server.backup.encryption.FullRestoreDownloader.FinishType;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-/**
- * Reads a stream from a {@link FullRestoreDownloader} and writes it to a file for consumption by
- * {@link BackupFileDecryptorTask}.
- */
-public class FullRestoreToFileTask {
- /**
- * Maximum number of bytes which the framework can request from the full restore data stream in
- * one call to {@link BackupTransport#getNextFullRestoreDataChunk}.
- */
- public static final int MAX_BYTES_FULL_RESTORE_CHUNK = 1024 * 32;
-
- /** Returned when the end of a backup stream has been reached. */
- private static final int END_OF_STREAM = -1;
-
- private final FullRestoreDownloader mFullRestoreDownloader;
- private final int mBufferSize;
-
- /**
- * Constructs a new instance which reads from the given package wrapper, using a buffer of size
- * {@link #MAX_BYTES_FULL_RESTORE_CHUNK}.
- */
- public FullRestoreToFileTask(FullRestoreDownloader fullRestoreDownloader) {
- this(fullRestoreDownloader, MAX_BYTES_FULL_RESTORE_CHUNK);
- }
-
- @VisibleForTesting
- FullRestoreToFileTask(FullRestoreDownloader fullRestoreDownloader, int bufferSize) {
- checkArgument(bufferSize > 0, "Buffer must have positive size");
-
- this.mFullRestoreDownloader = fullRestoreDownloader;
- this.mBufferSize = bufferSize;
- }
-
- /**
- * Downloads the backup file from the server and writes it to the given file.
- *
- * <p>At the end of the download (success or failure), closes the connection and sends a
- * Clearcut log.
- */
- public void restoreToFile(File targetFile) throws IOException {
- try (BufferedOutputStream outputStream =
- new BufferedOutputStream(new FileOutputStream(targetFile))) {
- byte[] buffer = new byte[mBufferSize];
- int bytesRead = mFullRestoreDownloader.readNextChunk(buffer);
- while (bytesRead != END_OF_STREAM) {
- outputStream.write(buffer, /* off=*/ 0, bytesRead);
- bytesRead = mFullRestoreDownloader.readNextChunk(buffer);
- }
-
- outputStream.flush();
-
- mFullRestoreDownloader.finish(FinishType.FINISHED);
- } catch (IOException e) {
- mFullRestoreDownloader.finish(FinishType.TRANSFER_FAILURE);
- throw e;
- }
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTask.java
deleted file mode 100644
index d436554..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTask.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.content.Context;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.LockScreenRequiredException;
-import android.security.keystore.recovery.RecoveryController;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-
-import java.security.InvalidKeyException;
-import java.security.UnrecoverableKeyException;
-import java.util.Collections;
-import java.util.Optional;
-
-/**
- * Initializes the device for encrypted backup, through generating a secondary key, and setting its
- * alias in the settings.
- *
- * <p>If the device is already initialized, this is a no-op.
- */
-public class InitializeRecoverableSecondaryKeyTask {
- private static final String TAG = "InitializeRecoverableSecondaryKeyTask";
-
- private final Context mContext;
- private final CryptoSettings mCryptoSettings;
- private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
- private final CryptoBackupServer mBackupServer;
-
- /**
- * A new instance.
- *
- * @param cryptoSettings Settings to store the active key alias.
- * @param secondaryKeyManager Key manager to generate the new active secondary key.
- * @param backupServer Server with which to sync the active key alias.
- */
- public InitializeRecoverableSecondaryKeyTask(
- Context context,
- CryptoSettings cryptoSettings,
- RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
- CryptoBackupServer backupServer) {
- mContext = context;
- mCryptoSettings = cryptoSettings;
- mSecondaryKeyManager = secondaryKeyManager;
- mBackupServer = backupServer;
- }
-
- /**
- * Initializes the device for encrypted backup, by generating a recoverable secondary key, then
- * sending that alias to the backup server and saving it in local settings.
- *
- * <p>If there is already an active secondary key then does nothing. If the active secondary key
- * is destroyed then throws {@link InvalidKeyException}.
- *
- * <p>If a key rotation is pending and able to finish (i.e., the new key has synced with the
- * remote trusted hardware module), then it completes the rotation before returning the key.
- *
- * @return The active secondary key.
- * @throws InvalidKeyException if the secondary key is in a bad state.
- */
- public RecoverableKeyStoreSecondaryKey run()
- throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
- InternalRecoveryServiceException {
- // Complete any pending key rotations
- new RotateSecondaryKeyTask(
- mContext,
- mSecondaryKeyManager,
- mBackupServer,
- mCryptoSettings,
- RecoveryController.getInstance(mContext))
- .run();
-
- return runInternal();
- }
-
- private RecoverableKeyStoreSecondaryKey runInternal()
- throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
- InternalRecoveryServiceException {
- Optional<RecoverableKeyStoreSecondaryKey> maybeActiveKey = loadFromSetting();
-
- if (maybeActiveKey.isPresent()) {
- assertKeyNotDestroyed(maybeActiveKey.get());
- Slog.d(TAG, "Secondary key already initialized: " + maybeActiveKey.get().getAlias());
- return maybeActiveKey.get();
- }
-
- Slog.v(TAG, "Initializing for crypto: generating a secondary key.");
- RecoverableKeyStoreSecondaryKey key = mSecondaryKeyManager.generate();
-
- String alias = key.getAlias();
- Slog.i(TAG, "Generated new secondary key " + alias);
-
- // No tertiary keys yet as we are creating a brand new secondary (without rotation).
- mBackupServer.setActiveSecondaryKeyAlias(alias, /*tertiaryKeys=*/ Collections.emptyMap());
- Slog.v(TAG, "Successfully synced %s " + alias + " with server.");
-
- mCryptoSettings.initializeWithKeyAlias(alias);
- Slog.v(TAG, "Successfully saved " + alias + " as active secondary to disk.");
-
- return key;
- }
-
- private void assertKeyNotDestroyed(RecoverableKeyStoreSecondaryKey key)
- throws InvalidKeyException {
- if (key.getStatus(mContext) == RecoverableKeyStoreSecondaryKey.Status.DESTROYED) {
- throw new InvalidKeyException("Key destroyed: " + key.getAlias());
- }
- }
-
- private Optional<RecoverableKeyStoreSecondaryKey> loadFromSetting()
- throws InvalidKeyException, UnrecoverableKeyException,
- InternalRecoveryServiceException {
-
- // TODO: b/141856950.
- if (!mCryptoSettings.getIsInitialized()) {
- return Optional.empty();
- }
-
- Optional<String> maybeAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
- if (!maybeAlias.isPresent()) {
- throw new InvalidKeyException(
- "Settings said crypto was initialized, but there was no active secondary"
- + " alias");
- }
-
- String alias = maybeAlias.get();
-
- Optional<RecoverableKeyStoreSecondaryKey> key;
- key = mSecondaryKeyManager.get(alias);
-
- if (!key.isPresent()) {
- throw new InvalidKeyException(
- "Initialized with key but it was not in key store: " + alias);
- }
-
- return key;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/KvBackupEncrypter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/KvBackupEncrypter.java
deleted file mode 100644
index d20cd4c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/KvBackupEncrypter.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.internal.util.Preconditions.checkState;
-
-import android.annotation.Nullable;
-import android.app.backup.BackupDataInput;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ChunkEncryptor;
-import com.android.server.backup.encryption.chunking.ChunkHasher;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
-import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.SecretKey;
-
-/**
- * Reads key value backup data from an input, converts each pair into a chunk and encrypts the
- * chunks.
- *
- * <p>The caller should pass in the key value listing from the previous backup, if there is one.
- * This class emits chunks for both existing and new pairs, using the provided listing to
- * determine the hashes of pairs that already exist. During the backup it computes the new listing,
- * which the caller should store on disk and pass in at the start of the next backup.
- *
- * <p>Also computes the message digest, which is {@code SHA-256(chunk hashes sorted
- * lexicographically)}.
- */
-public class KvBackupEncrypter implements BackupEncrypter {
- private final BackupDataInput mBackupDataInput;
-
- private KeyValueListingProto.KeyValueListing mOldKeyValueListing;
- @Nullable private KeyValueListingBuilder mNewKeyValueListing;
-
- /**
- * Constructs a new instance which reads data from the given input.
- *
- * <p>By default this performs non-incremental backup, call {@link #setOldKeyValueListing} to
- * perform incremental backup.
- */
- public KvBackupEncrypter(BackupDataInput backupDataInput) {
- mBackupDataInput = backupDataInput;
- mOldKeyValueListing = KeyValueListingBuilder.emptyListing();
- }
-
- /** Sets the old listing to perform incremental backup against. */
- public void setOldKeyValueListing(KeyValueListingProto.KeyValueListing oldKeyValueListing) {
- mOldKeyValueListing = oldKeyValueListing;
- }
-
- @Override
- public Result backup(
- SecretKey secretKey,
- @Nullable byte[] unusedFingerprintMixerSalt,
- Set<ChunkHash> unusedExistingChunks)
- throws IOException, GeneralSecurityException {
- ChunkHasher chunkHasher = new ChunkHasher(secretKey);
- ChunkEncryptor chunkEncryptor = new ChunkEncryptor(secretKey, new SecureRandom());
- mNewKeyValueListing = new KeyValueListingBuilder();
- List<ChunkHash> allChunks = new ArrayList<>();
- List<EncryptedChunk> newChunks = new ArrayList<>();
-
- Map<String, ChunkHash> existingChunksToReuse = buildPairMap(mOldKeyValueListing);
-
- while (mBackupDataInput.readNextHeader()) {
- String key = mBackupDataInput.getKey();
- Optional<byte[]> value = readEntireValue(mBackupDataInput);
-
- // As this pair exists in the new backup, we don't need to add it from the previous
- // backup.
- existingChunksToReuse.remove(key);
-
- // If the value is not present then this key has been deleted.
- if (value.isPresent()) {
- EncryptedChunk newChunk =
- createEncryptedChunk(chunkHasher, chunkEncryptor, key, value.get());
- allChunks.add(newChunk.key());
- newChunks.add(newChunk);
- mNewKeyValueListing.addPair(key, newChunk.key());
- }
- }
-
- allChunks.addAll(existingChunksToReuse.values());
-
- mNewKeyValueListing.addAll(existingChunksToReuse);
-
- return new Result(allChunks, newChunks, createMessageDigest(allChunks));
- }
-
- /**
- * Returns a listing containing the pairs in the new backup.
- *
- * <p>You must call {@link #backup} first.
- */
- public KeyValueListingProto.KeyValueListing getNewKeyValueListing() {
- checkState(mNewKeyValueListing != null, "Must call backup() first");
- return mNewKeyValueListing.build();
- }
-
- private static Map<String, ChunkHash> buildPairMap(
- KeyValueListingProto.KeyValueListing listing) {
- Map<String, ChunkHash> map = new HashMap<>();
- for (KeyValueListingProto.KeyValueEntry entry : listing.entries) {
- map.put(entry.key, new ChunkHash(entry.hash));
- }
- return map;
- }
-
- private EncryptedChunk createEncryptedChunk(
- ChunkHasher chunkHasher, ChunkEncryptor chunkEncryptor, String key, byte[] value)
- throws InvalidKeyException, IllegalBlockSizeException {
- KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
- pair.key = key;
- pair.value = Arrays.copyOf(value, value.length);
-
- byte[] plaintext = KeyValuePairProto.KeyValuePair.toByteArray(pair);
- return chunkEncryptor.encrypt(chunkHasher.computeHash(plaintext), plaintext);
- }
-
- private static byte[] createMessageDigest(List<ChunkHash> allChunks)
- throws NoSuchAlgorithmException {
- MessageDigest messageDigest =
- MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
- // TODO:b/141531271 Extract sorted chunks code to utility class
- List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
- Collections.sort(sortedChunks);
- for (ChunkHash hash : sortedChunks) {
- messageDigest.update(hash.getHash());
- }
- return messageDigest.digest();
- }
-
- private static Optional<byte[]> readEntireValue(BackupDataInput input) throws IOException {
- // A negative data size indicates that this key should be deleted.
- if (input.getDataSize() < 0) {
- return Optional.empty();
- }
-
- byte[] value = new byte[input.getDataSize()];
- int bytesRead = 0;
- while (bytesRead < value.length) {
- bytesRead += input.readEntityData(value, bytesRead, value.length - bytesRead);
- }
- return Optional.of(value);
- }
-}
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
deleted file mode 100644
index 78c370b..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 1e4f43b..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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/NoActiveSecondaryKeyException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java
deleted file mode 100644
index 72e8a89..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 attempting to rotate key when there is no current active secondary key set
- * locally. This means the device needs to re-initialize, asking the backup server what the active
- * secondary key is.
- */
-public class NoActiveSecondaryKeyException extends Exception {
- public NoActiveSecondaryKeyException(String message) {
- super(message);
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java
deleted file mode 100644
index a3eda7d..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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;
-
-// TODO(141840878): Update documentation.
-/**
- * Exception thrown when the framework provides an incremental backup but the transport requires a
- * non-incremental backup.
- */
-public class NonIncrementalBackupRequiredException extends Exception {}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java
deleted file mode 100644
index e5e2c1c..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * 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 android.os.Build.VERSION_CODES.P;
-
-import android.content.Context;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.RecoveryController;
-import android.util.Slog;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.KeyWrapUtils;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyStore;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-
-/**
- * Finishes a rotation for a {@link
- * com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey}.
- */
-public class RotateSecondaryKeyTask {
- private static final String TAG = "RotateSecondaryKeyTask";
-
- private final Context mContext;
- private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
- private final CryptoBackupServer mBackupServer;
- private final CryptoSettings mCryptoSettings;
- private final RecoveryController mRecoveryController;
-
- /**
- * A new instance.
- *
- * @param secondaryKeyManager For loading the currently active and next secondary key.
- * @param backupServer For loading and storing tertiary keys and for setting active secondary
- * key.
- * @param cryptoSettings For checking the stored aliases for the next and active key.
- * @param recoveryController For communicating with the Framework apis.
- */
- public RotateSecondaryKeyTask(
- Context context,
- RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
- CryptoBackupServer backupServer,
- CryptoSettings cryptoSettings,
- RecoveryController recoveryController) {
- mContext = context;
- mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager);
- mCryptoSettings = Objects.requireNonNull(cryptoSettings);
- mBackupServer = Objects.requireNonNull(backupServer);
- mRecoveryController = Objects.requireNonNull(recoveryController);
- }
-
- /** Runs the task. */
- public void run() {
- // Never run more than one of these at the same time.
- synchronized (RotateSecondaryKeyTask.class) {
- runInternal();
- }
- }
-
- private void runInternal() {
- Optional<RecoverableKeyStoreSecondaryKey> maybeNextKey;
- try {
- maybeNextKey = getNextKey();
- } catch (Exception e) {
- Slog.e(TAG, "Error checking for next key", e);
- return;
- }
-
- if (!maybeNextKey.isPresent()) {
- Slog.d(TAG, "No secondary key rotation task pending. Exiting.");
- return;
- }
-
- RecoverableKeyStoreSecondaryKey nextKey = maybeNextKey.get();
- boolean isReady;
- try {
- isReady = isSecondaryKeyRotationReady(nextKey);
- } catch (InternalRecoveryServiceException e) {
- Slog.e(TAG, "Error encountered checking whether next secondary key is synced", e);
- return;
- }
-
- if (!isReady) {
- return;
- }
-
- try {
- rotateToKey(nextKey);
- } catch (Exception e) {
- Slog.e(TAG, "Error trying to rotate to new secondary key", e);
- }
- }
-
- private Optional<RecoverableKeyStoreSecondaryKey> getNextKey()
- throws InternalRecoveryServiceException, UnrecoverableKeyException {
- Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
- if (!maybeNextAlias.isPresent()) {
- return Optional.empty();
- }
- return mSecondaryKeyManager.get(maybeNextAlias.get());
- }
-
- private boolean isSecondaryKeyRotationReady(RecoverableKeyStoreSecondaryKey nextKey)
- throws InternalRecoveryServiceException {
- String nextAlias = nextKey.getAlias();
- Slog.i(TAG, "Key rotation to " + nextAlias + " is pending. Checking key sync status.");
- int status = mRecoveryController.getRecoveryStatus(nextAlias);
-
- if (status == RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE) {
- Slog.e(
- TAG,
- "Permanent failure to sync " + nextAlias + ". Cannot possibly rotate to it.");
- mCryptoSettings.removeNextSecondaryKeyAlias();
- return false;
- }
-
- if (status == RecoveryController.RECOVERY_STATUS_SYNCED) {
- Slog.i(TAG, "Secondary key " + nextAlias + " has now synced! Commencing rotation.");
- } else {
- Slog.i(TAG, "Sync still pending for " + nextAlias);
- }
- return status == RecoveryController.RECOVERY_STATUS_SYNCED;
- }
-
- /**
- * @throws ActiveSecondaryNotInKeychainException if the currently active secondary key is not in
- * the keychain.
- * @throws IOException if there is an IO issue communicating with the server or loading from
- * disk.
- * @throws NoActiveSecondaryKeyException if there is no active key set.
- * @throws IllegalBlockSizeException if there is an issue decrypting a tertiary key.
- * @throws InvalidKeyException if any of the secondary keys cannot be used for wrapping or
- * unwrapping tertiary keys.
- */
- private void rotateToKey(RecoverableKeyStoreSecondaryKey newSecondaryKey)
- throws ActiveSecondaryNotInKeychainException, IOException,
- NoActiveSecondaryKeyException, IllegalBlockSizeException, InvalidKeyException,
- InternalRecoveryServiceException, UnrecoverableKeyException,
- InvalidAlgorithmParameterException, NoSuchAlgorithmException,
- NoSuchPaddingException {
- RecoverableKeyStoreSecondaryKey activeSecondaryKey = getActiveSecondaryKey();
- String activeSecondaryKeyAlias = activeSecondaryKey.getAlias();
- String newSecondaryKeyAlias = newSecondaryKey.getAlias();
- if (newSecondaryKeyAlias.equals(activeSecondaryKeyAlias)) {
- Slog.i(TAG, activeSecondaryKeyAlias + " was already the active alias.");
- return;
- }
-
- TertiaryKeyStore tertiaryKeyStore =
- TertiaryKeyStore.newInstance(mContext, activeSecondaryKey);
- Map<String, SecretKey> tertiaryKeys = tertiaryKeyStore.getAll();
-
- if (tertiaryKeys.isEmpty()) {
- Slog.i(
- TAG,
- "No tertiary keys for " + activeSecondaryKeyAlias + ". No need to rewrap. ");
- mBackupServer.setActiveSecondaryKeyAlias(
- newSecondaryKeyAlias, /*tertiaryKeys=*/ Collections.emptyMap());
- } else {
- Map<String, WrappedKeyProto.WrappedKey> rewrappedTertiaryKeys =
- rewrapAll(newSecondaryKey, tertiaryKeys);
- TertiaryKeyStore.newInstance(mContext, newSecondaryKey).putAll(rewrappedTertiaryKeys);
- Slog.i(
- TAG,
- "Successfully rewrapped " + rewrappedTertiaryKeys.size() + " tertiary keys");
- mBackupServer.setActiveSecondaryKeyAlias(newSecondaryKeyAlias, rewrappedTertiaryKeys);
- Slog.i(
- TAG,
- "Successfully uploaded new set of tertiary keys to "
- + newSecondaryKeyAlias
- + " alias");
- }
-
- mCryptoSettings.setActiveSecondaryKeyAlias(newSecondaryKeyAlias);
- mCryptoSettings.removeNextSecondaryKeyAlias();
- try {
- mRecoveryController.removeKey(activeSecondaryKeyAlias);
- } catch (InternalRecoveryServiceException e) {
- Slog.e(TAG, "Error removing old secondary key from RecoverableKeyStoreLoader", e);
- }
- }
-
- private RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
- throws NoActiveSecondaryKeyException, ActiveSecondaryNotInKeychainException,
- InternalRecoveryServiceException, UnrecoverableKeyException {
-
- Optional<String> activeSecondaryAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
-
- if (!activeSecondaryAlias.isPresent()) {
- Slog.i(
- TAG,
- "Was asked to rotate secondary key, but local config did not have a secondary "
- + "key alias set.");
- throw new NoActiveSecondaryKeyException("No local active secondary key set.");
- }
-
- String activeSecondaryKeyAlias = activeSecondaryAlias.get();
- Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
- mSecondaryKeyManager.get(activeSecondaryKeyAlias);
-
- if (!secondaryKey.isPresent()) {
- throw new ActiveSecondaryNotInKeychainException(
- String.format(
- Locale.US,
- "Had local active recoverable key alias of %s but key was not in"
- + " user's keychain.",
- activeSecondaryKeyAlias));
- }
-
- return secondaryKey.get();
- }
-
- /**
- * Rewraps all the tertiary keys.
- *
- * @param newSecondaryKey The secondary key with which to rewrap the tertiaries.
- * @param tertiaryKeys The tertiary keys, by package name.
- * @return The newly wrapped tertiary keys, by package name.
- * @throws InvalidKeyException if any key is unusable.
- * @throws IllegalBlockSizeException if could not decrypt.
- */
- private Map<String, WrappedKeyProto.WrappedKey> rewrapAll(
- RecoverableKeyStoreSecondaryKey newSecondaryKey, Map<String, SecretKey> tertiaryKeys)
- throws InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException,
- NoSuchAlgorithmException {
- Map<String, WrappedKeyProto.WrappedKey> wrappedKeys = new HashMap<>();
-
- for (String packageName : tertiaryKeys.keySet()) {
- SecretKey tertiaryKey = tertiaryKeys.get(packageName);
- wrappedKeys.put(
- packageName, KeyWrapUtils.wrap(newSecondaryKey.getSecretKey(), tertiaryKey));
- }
-
- return wrappedKeys;
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java
deleted file mode 100644
index 515db86..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 aa backup has exceeded the space allowed for that user */
-public class SizeQuotaExceededException extends RuntimeException {
- public SizeQuotaExceededException() {
- super("Backup size quota exceeded.");
- }
-}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java
deleted file mode 100644
index 81169e2..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.LockScreenRequiredException;
-import android.util.Slog;
-
-import com.android.internal.util.Preconditions;
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-
-import java.security.UnrecoverableKeyException;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Starts rotating to a new secondary key. Cannot complete until the screen is unlocked and the new
- * key is synced.
- */
-public class StartSecondaryKeyRotationTask {
- private static final String TAG = "BE-StSecondaryKeyRotTsk";
-
- private final CryptoSettings mCryptoSettings;
- private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
-
- public StartSecondaryKeyRotationTask(
- CryptoSettings cryptoSettings,
- RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager) {
- mCryptoSettings = Objects.requireNonNull(cryptoSettings);
- mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager);
- }
-
- /** Begin the key rotation */
- public void run() {
- Slog.i(TAG, "Attempting to initiate a secondary key rotation.");
-
- Optional<String> maybeCurrentAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
- if (!maybeCurrentAlias.isPresent()) {
- Slog.w(TAG, "No active current alias. Cannot trigger a secondary rotation.");
- return;
- }
- String currentAlias = maybeCurrentAlias.get();
-
- Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
- if (maybeNextAlias.isPresent()) {
- String nextAlias = maybeNextAlias.get();
- if (nextAlias.equals(currentAlias)) {
- // Shouldn't be possible, but guard against accidentally deleting the active key.
- Slog.e(TAG, "Was already trying to rotate to what is already the active key.");
- } else {
- Slog.w(TAG, "Was already rotating to another key. Cancelling that.");
- try {
- mSecondaryKeyManager.remove(nextAlias);
- } catch (Exception e) {
- Slog.wtf(TAG, "Could not remove old key", e);
- }
- }
- mCryptoSettings.removeNextSecondaryKeyAlias();
- }
-
- RecoverableKeyStoreSecondaryKey newSecondaryKey;
- try {
- newSecondaryKey = mSecondaryKeyManager.generate();
- } catch (LockScreenRequiredException e) {
- Slog.e(TAG, "No lock screen is set - cannot generate a new key to rotate to.", e);
- return;
- } catch (InternalRecoveryServiceException e) {
- Slog.e(TAG, "Internal error in Recovery Controller, failed to rotate key.", e);
- return;
- } catch (UnrecoverableKeyException e) {
- Slog.e(TAG, "Failed to get key after generating, failed to rotate", e);
- return;
- }
-
- String alias = newSecondaryKey.getAlias();
- Slog.i(TAG, "Generated a new secondary key with alias '" + alias + "'.");
- try {
- mCryptoSettings.setNextSecondaryAlias(alias);
- Slog.i(TAG, "Successfully set '" + alias + "' as next key to rotate to");
- } catch (IllegalArgumentException e) {
- Slog.e(TAG, "Unexpected error setting next alias", e);
- try {
- mSecondaryKeyManager.remove(alias);
- } catch (Exception err) {
- Slog.wtf(TAG, "Failed to remove generated key after encountering error", err);
- }
- }
- }
-}
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
deleted file mode 100644
index 9a97e38..0000000
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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-integration/Android.bp b/packages/BackupEncryption/test/robolectric-integration/Android.bp
deleted file mode 100644
index c842e42..0000000
--- a/packages/BackupEncryption/test/robolectric-integration/Android.bp
+++ /dev/null
@@ -1,43 +0,0 @@
-// 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 {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_robolectric_test {
- name: "BackupEncryptionRoboIntegTests",
- srcs: [
- "src/**/*.java",
- ],
- java_resource_dirs: ["config"],
- libs: [
- "backup-encryption-protos",
- "platform-test-annotations",
- "testng",
- "truth-prebuilt",
- "BackupEncryptionRoboTests",
- ],
- static_libs: [
- "androidx.test.core",
- "androidx.test.runner",
- "androidx.test.rules",
- ],
- instrumentation_for: "BackupEncryption",
-}
diff --git a/packages/BackupEncryption/test/robolectric-integration/AndroidManifest.xml b/packages/BackupEncryption/test/robolectric-integration/AndroidManifest.xml
deleted file mode 100644
index c3930cc..0000000
--- a/packages/BackupEncryption/test/robolectric-integration/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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"
- coreApp="true"
- package="com.android.server.backup.encryption.robointeg">
-
- <application/>
-
-</manifest>
diff --git a/packages/BackupEncryption/test/robolectric-integration/config/robolectric.properties b/packages/BackupEncryption/test/robolectric-integration/config/robolectric.properties
deleted file mode 100644
index 26fceb3..0000000
--- a/packages/BackupEncryption/test/robolectric-integration/config/robolectric.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# 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.
-#
-
-sdk=NEWEST_SDK
diff --git a/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java b/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java
deleted file mode 100644
index a432d91..0000000
--- a/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java
+++ /dev/null
@@ -1,321 +0,0 @@
-/*
- * 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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.os.ParcelFileDescriptor;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.RecoveryController;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.KeyWrapUtils;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-import com.android.server.backup.encryption.tasks.EncryptedFullBackupTask;
-import com.android.server.backup.encryption.tasks.EncryptedFullRestoreTask;
-import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
-import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
-import com.android.server.testing.shadows.DataEntity;
-import com.android.server.testing.shadows.ShadowBackupDataInput;
-import com.android.server.testing.shadows.ShadowBackupDataOutput;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Optional;
-import java.util.Map;
-import java.util.Set;
-
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-
-@Config(
- shadows = {
- ShadowBackupDataInput.class,
- ShadowBackupDataOutput.class,
- ShadowRecoveryController.class
- })
-@RunWith(RobolectricTestRunner.class)
-public class RoundTripTest {
- private static final DataEntity[] KEY_VALUE_DATA = {
- new DataEntity("test_key_1", "test_value_1"),
- new DataEntity("test_key_2", "test_value_2"),
- new DataEntity("test_key_3", "test_value_3")
- };
-
- /** Amount of data we want to round trip in this test */
- private static final int TEST_DATA_SIZE = 1024 * 1024; // 1MB
-
- /** Buffer size used when reading data from the restore task */
- private static final int READ_BUFFER_SIZE = 1024; // 1024 byte buffer.
-
- /** Key parameters used for the secondary encryption key */
- private static final String KEY_ALGORITHM = "AES";
-
- private static final int KEY_SIZE_BITS = 256;
-
- /** Package name for our test package */
- private static final String TEST_PACKAGE_NAME = "com.android.backup.test";
-
- /** The name we use to refer to our secondary key */
- private static final String TEST_KEY_ALIAS = "test/backup/KEY_ALIAS";
-
- /** Original data used for comparison after round trip */
- private final byte[] mOriginalData = new byte[TEST_DATA_SIZE];
-
- /** App context, used to store the key data and chunk listings */
- private Context mContext;
-
- /** The secondary key we're using for the test */
- private RecoverableKeyStoreSecondaryKey mSecondaryKey;
-
- /** Source of random material which is considered non-predictable in its' generation */
- private final SecureRandom mSecureRandom = new SecureRandom();
-
- private RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider
- mSecondaryKeyManagerProvider;
- private DummyServer mDummyServer;
- private RecoveryController mRecoveryController;
-
- @Mock private ParcelFileDescriptor mParcelFileDescriptor;
-
- @Before
- public void setUp() throws NoSuchAlgorithmException, InternalRecoveryServiceException {
- MockitoAnnotations.initMocks(this);
-
- ShadowBackupDataInput.reset();
- ShadowBackupDataOutput.reset();
-
- mContext = ApplicationProvider.getApplicationContext();
- mSecondaryKey = new RecoverableKeyStoreSecondaryKey(TEST_KEY_ALIAS, generateAesKey());
- mDummyServer = new DummyServer();
- mSecondaryKeyManagerProvider =
- () ->
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(mContext), mSecureRandom);
-
- fillBuffer(mOriginalData);
- }
-
- @Test
- public void testFull_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
- byte[] backupData = performFullBackup(mOriginalData);
- assertThat(backupData).isNotEqualTo(mOriginalData);
- byte[] restoredData = performFullRestore(backupData);
- assertThat(restoredData).isEqualTo(mOriginalData);
- }
-
- @Test
- public void testKeyValue_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
- byte[] backupData = performNonIncrementalKeyValueBackup(KEY_VALUE_DATA);
-
- // Get the secondary key used to do backup.
- Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
- mSecondaryKeyManagerProvider.get().get(mDummyServer.mSecondaryKeyAlias);
- assertThat(secondaryKey.isPresent()).isTrue();
-
- Set<DataEntity> restoredData = performKeyValueRestore(backupData, secondaryKey.get());
-
- assertThat(restoredData).containsExactly(KEY_VALUE_DATA).inOrder();
- }
-
- /** Perform a key/value backup and return the backed-up representation of the data */
- private byte[] performNonIncrementalKeyValueBackup(DataEntity[] backupData)
- throws Exception {
- // Populate test key/value data.
- for (DataEntity entity : backupData) {
- ShadowBackupDataInput.addEntity(entity);
- }
-
- EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
- new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
- EncryptedKvBackupTask backupTask =
- backupTaskFactory.newInstance(
- mContext,
- mSecureRandom,
- mDummyServer,
- CryptoSettings.getInstance(mContext),
- mSecondaryKeyManagerProvider,
- mParcelFileDescriptor,
- TEST_PACKAGE_NAME);
-
- backupTask.performBackup(/* incremental */ false);
-
- return mDummyServer.mStoredData;
- }
-
- /** Perform a full backup and return the backed-up representation of the data */
- private byte[] performFullBackup(byte[] backupData) throws Exception {
- DummyServer dummyServer = new DummyServer();
- EncryptedFullBackupTask backupTask =
- EncryptedFullBackupTask.newInstance(
- mContext,
- dummyServer,
- mSecureRandom,
- mSecondaryKey,
- TEST_PACKAGE_NAME,
- new ByteArrayInputStream(backupData));
- backupTask.call();
- return dummyServer.mStoredData;
- }
-
- private Set<DataEntity> performKeyValueRestore(
- byte[] backupData, RecoverableKeyStoreSecondaryKey secondaryKey) throws Exception {
- EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
- new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
- EncryptedKvRestoreTask restoreTask =
- restoreTaskFactory.newInstance(
- mContext,
- mSecondaryKeyManagerProvider,
- new FakeFullRestoreDownloader(backupData),
- secondaryKey.getAlias(),
- KeyWrapUtils.wrap(
- secondaryKey.getSecretKey(), getTertiaryKey(secondaryKey)));
- restoreTask.getRestoreData(mParcelFileDescriptor);
- return ShadowBackupDataOutput.getEntities();
- }
-
- /** Perform a full restore and return the bytes obtained from the restore process */
- private byte[] performFullRestore(byte[] backupData)
- throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- IllegalBlockSizeException {
- ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
-
- EncryptedFullRestoreTask restoreTask =
- EncryptedFullRestoreTask.newInstance(
- mContext,
- new FakeFullRestoreDownloader(backupData),
- getTertiaryKey(mSecondaryKey));
-
- byte[] buffer = new byte[READ_BUFFER_SIZE];
- int bytesRead = restoreTask.readNextChunk(buffer);
- while (bytesRead != -1) {
- decryptedOutput.write(buffer, 0, bytesRead);
- bytesRead = restoreTask.readNextChunk(buffer);
- }
-
- return decryptedOutput.toByteArray();
- }
-
- /** Get the tertiary key for our test package from the key manager */
- private SecretKey getTertiaryKey(RecoverableKeyStoreSecondaryKey secondaryKey)
- throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
- NoSuchAlgorithmException, IOException, NoSuchPaddingException,
- InvalidKeyException {
- TertiaryKeyManager tertiaryKeyManager =
- new TertiaryKeyManager(
- mContext,
- mSecureRandom,
- TertiaryKeyRotationScheduler.getInstance(mContext),
- secondaryKey,
- TEST_PACKAGE_NAME);
- return tertiaryKeyManager.getKey();
- }
-
- /** Fill a buffer with data in a predictable way */
- private void fillBuffer(byte[] buffer) {
- byte loopingCounter = 0;
- for (int i = 0; i < buffer.length; i++) {
- buffer[i] = loopingCounter;
- loopingCounter++;
- }
- }
-
- /** Generate a new, random, AES key */
- public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- keyGenerator.init(KEY_SIZE_BITS);
- return keyGenerator.generateKey();
- }
-
- /**
- * Dummy backup data endpoint. This stores the data so we can use it in subsequent test steps.
- */
- private static class DummyServer implements CryptoBackupServer {
- private static final String DUMMY_DOC_ID = "DummyDoc";
-
- byte[] mStoredData = null;
- String mSecondaryKeyAlias;
-
- @Override
- public String uploadIncrementalBackup(
- String packageName,
- String oldDocId,
- byte[] diffScript,
- WrappedKeyProto.WrappedKey tertiaryKey) {
- throw new RuntimeException("Not Implemented");
- }
-
- @Override
- public String uploadNonIncrementalBackup(
- String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
- assertThat(packageName).isEqualTo(TEST_PACKAGE_NAME);
- mStoredData = data;
- return DUMMY_DOC_ID;
- }
-
- @Override
- public void setActiveSecondaryKeyAlias(
- String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
- mSecondaryKeyAlias = keyAlias;
- }
- }
-
- /** Fake package wrapper which returns data from a byte array. */
- private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
- private final ByteArrayInputStream mData;
-
- FakeFullRestoreDownloader(byte[] data) {
- // We override all methods of the superclass, so it does not require any collaborators.
- super();
- mData = new ByteArrayInputStream(data);
- }
-
- @Override
- public int readNextChunk(byte[] buffer) throws IOException {
- return mData.read(buffer);
- }
-
- @Override
- public void finish(FinishType finishType) {
- // Do nothing.
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/Android.bp b/packages/BackupEncryption/test/robolectric/Android.bp
deleted file mode 100644
index 7665d8f..0000000
--- a/packages/BackupEncryption/test/robolectric/Android.bp
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_robolectric_test {
- name: "BackupEncryptionRoboTests",
- srcs: [
- "src/**/*.java",
-// ":FrameworksServicesRoboShadows",
- ],
- java_resource_dirs: ["config"],
- libs: [
- "backup-encryption-protos",
- "platform-test-annotations",
- "testng",
- "truth-prebuilt",
- ],
- static_libs: [
- "androidx.test.core",
- "androidx.test.runner",
- "androidx.test.rules",
- ],
- instrumentation_for: "BackupEncryption",
-}
-
-filegroup {
- name: "BackupEncryptionRoboShadows",
- srcs: ["src/com/android/server/testing/shadows/**/*.java"],
-}
diff --git a/packages/BackupEncryption/test/robolectric/AndroidManifest.xml b/packages/BackupEncryption/test/robolectric/AndroidManifest.xml
deleted file mode 100644
index ae5cdd9..0000000
--- a/packages/BackupEncryption/test/robolectric/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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"
- coreApp="true"
- package="com.android.server.backup.encryption.robotests">
-
- <application/>
-
-</manifest>
diff --git a/packages/BackupEncryption/test/robolectric/config/robolectric.properties b/packages/BackupEncryption/test/robolectric/config/robolectric.properties
deleted file mode 100644
index 26fceb3..0000000
--- a/packages/BackupEncryption/test/robolectric/config/robolectric.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# 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.
-#
-
-sdk=NEWEST_SDK
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java
deleted file mode 100644
index 979b3d5..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * 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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.app.Application;
-import android.security.keystore.recovery.RecoveryController;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.Optional;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = ShadowRecoveryController.class)
-public class CryptoSettingsTest {
-
- private static final String TEST_KEY_ALIAS =
- "com.android.server.backup.encryption/keystore/08120c326b928ff34c73b9c58581da63";
-
- private CryptoSettings mCryptoSettings;
- private Application mApplication;
-
- @Before
- public void setUp() {
- ShadowRecoveryController.reset();
-
- mApplication = ApplicationProvider.getApplicationContext();
- mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
- }
-
- @Test
- public void getActiveSecondaryAlias_isInitiallyAbsent() {
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse();
- }
-
- @Test
- public void getActiveSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
- mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS);
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
- }
-
- @Test
- public void getNextSecondaryAlias_isInitiallyAbsent() {
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
- }
-
- @Test
- public void getNextSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
- mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
- }
-
- @Test
- public void isInitialized_isInitiallyFalse() {
- assertThat(mCryptoSettings.getIsInitialized()).isFalse();
- }
-
- @Test
- public void setActiveSecondaryAlias_throwsIfKeyIsNotInRecoveryController() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS));
- }
-
- @Test
- public void setNextSecondaryAlias_inRecoveryController_setsAlias() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
-
- mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
-
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
- }
-
- @Test
- public void setNextSecondaryAlias_throwsIfKeyIsNotInRecoveryController() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS));
- }
-
- @Test
- public void removeNextSecondaryAlias_removesIt() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
- mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
-
- mCryptoSettings.removeNextSecondaryKeyAlias();
-
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
- }
-
- @Test
- public void initializeWithKeyAlias_setsAsInitialized() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
- mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
- assertThat(mCryptoSettings.getIsInitialized()).isTrue();
- }
-
- @Test
- public void initializeWithKeyAlias_setsActiveAlias() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
- mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
- }
-
- @Test
- public void initializeWithKeyAlias_throwsIfKeyIsNotInRecoveryController() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS));
- }
-
- @Test
- public void initializeWithKeyAlias_throwsIfAlreadyInitialized() throws Exception {
- setAliasIsInRecoveryController(TEST_KEY_ALIAS);
- mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
-
- assertThrows(
- IllegalStateException.class,
- () -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS));
- }
-
- @Test
- public void getSecondaryLastRotated_returnsEmptyInitially() {
- assertThat(mCryptoSettings.getSecondaryLastRotated()).isEqualTo(Optional.empty());
- }
-
- @Test
- public void getSecondaryLastRotated_returnsTimestampAfterItIsSet() {
- long timestamp = 1000001;
-
- mCryptoSettings.setSecondaryLastRotated(timestamp);
-
- assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(timestamp);
- }
-
- @Test
- public void getAncestralSecondaryKeyVersion_notSet_returnsOptionalAbsent() {
- assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().isPresent()).isFalse();
- }
-
- @Test
- public void getAncestralSecondaryKeyVersion_isSet_returnsSetValue() {
- String secondaryKeyVersion = "some_secondary_key";
- mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion);
-
- assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get())
- .isEqualTo(secondaryKeyVersion);
- }
-
- @Test
- public void getAncestralSecondaryKeyVersion_isSetMultipleTimes_returnsLastSetValue() {
- String secondaryKeyVersion1 = "some_secondary_key";
- String secondaryKeyVersion2 = "another_secondary_key";
- mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion1);
- mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion2);
-
- assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get())
- .isEqualTo(secondaryKeyVersion2);
- }
-
- @Test
- public void clearAllSettingsForBackup_clearsStateForBackup() throws Exception {
- String key1 = "key1";
- String key2 = "key2";
- String ancestralKey = "ancestral_key";
- setAliasIsInRecoveryController(key1);
- setAliasIsInRecoveryController(key2);
- mCryptoSettings.setActiveSecondaryKeyAlias(key1);
- mCryptoSettings.setNextSecondaryAlias(key2);
- mCryptoSettings.setSecondaryLastRotated(100001);
- mCryptoSettings.setAncestralSecondaryKeyVersion(ancestralKey);
-
- mCryptoSettings.clearAllSettingsForBackup();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse();
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
- assertThat(mCryptoSettings.getSecondaryLastRotated().isPresent()).isFalse();
- assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get()).isEqualTo(ancestralKey);
- }
-
- private void setAliasIsInRecoveryController(String alias) throws Exception {
- RecoveryController recoveryController = RecoveryController.getInstance(mApplication);
- recoveryController.generateKey(alias);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/StreamUtilsTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/StreamUtilsTest.java
deleted file mode 100644
index a95e87e..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/StreamUtilsTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-@RunWith(RobolectricTestRunner.class)
-public class StreamUtilsTest {
- private static final int SOURCE_DATA_SIZE = 64;
-
- private byte[] mSourceData;
-
- private InputStream mSource;
- private ByteArrayOutputStream mDestination;
-
- @Before
- public void setUp() {
- mSourceData = new byte[SOURCE_DATA_SIZE];
- for (byte i = 0; i < SOURCE_DATA_SIZE; i++) {
- mSourceData[i] = i;
- }
- mSource = new ByteArrayInputStream(mSourceData);
- mDestination = new ByteArrayOutputStream();
- }
-
- @Test
- public void copyStream_copiesAllBytesIfAsked() throws IOException {
- StreamUtils.copyStream(mSource, mDestination, mSourceData.length);
- assertOutputHasBytes(mSourceData.length);
- }
-
- @Test
- public void copyStream_stopsShortIfAsked() throws IOException {
- StreamUtils.copyStream(mSource, mDestination, mSourceData.length - 10);
- assertOutputHasBytes(mSourceData.length - 10);
- }
-
- @Test
- public void copyStream_stopsShortIfAskedToCopyMoreThanAvailable() throws IOException {
- StreamUtils.copyStream(mSource, mDestination, mSourceData.length + 10);
- assertOutputHasBytes(mSourceData.length);
- }
-
- private void assertOutputHasBytes(int count) {
- byte[] output = mDestination.toByteArray();
- assertThat(output.length).isEqualTo(count);
- for (int i = 0; i < count; i++) {
- assertThat(output[i]).isEqualTo(mSourceData[i]);
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/ChunkHashTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/ChunkHashTest.java
deleted file mode 100644
index c12464c..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/ChunkHashTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunk;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ChunkHashTest {
- private static final int HASH_LENGTH_BYTES = 256 / 8;
- private static final byte[] TEST_HASH_1 = Arrays.copyOf(new byte[] {1}, HASH_LENGTH_BYTES);
- private static final byte[] TEST_HASH_2 = Arrays.copyOf(new byte[] {2}, HASH_LENGTH_BYTES);
-
- @Test
- public void testGetHash_returnsHash() {
- ChunkHash chunkHash = new ChunkHash(TEST_HASH_1);
-
- byte[] hash = chunkHash.getHash();
-
- assertThat(hash).asList().containsExactlyElementsIn(Bytes.asList(TEST_HASH_1)).inOrder();
- }
-
- @Test
- public void testEquals() {
- ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
- ChunkHash equalChunkHash1 = new ChunkHash(TEST_HASH_1);
- ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
-
- assertThat(chunkHash1).isEqualTo(equalChunkHash1);
- assertThat(chunkHash1).isNotEqualTo(chunkHash2);
- }
-
- @Test
- public void testHashCode() {
- ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
- ChunkHash equalChunkHash1 = new ChunkHash(TEST_HASH_1);
- ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
-
- int hash1 = chunkHash1.hashCode();
- int equalHash1 = equalChunkHash1.hashCode();
- int hash2 = chunkHash2.hashCode();
-
- assertThat(hash1).isEqualTo(equalHash1);
- assertThat(hash1).isNotEqualTo(hash2);
- }
-
- @Test
- public void testCompareTo_whenEqual_returnsZero() {
- ChunkHash chunkHash = new ChunkHash(TEST_HASH_1);
- ChunkHash equalChunkHash = new ChunkHash(TEST_HASH_1);
-
- int result = chunkHash.compareTo(equalChunkHash);
-
- assertThat(result).isEqualTo(0);
- }
-
- @Test
- public void testCompareTo_whenArgumentGreater_returnsNegative() {
- ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
- ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
-
- int result = chunkHash1.compareTo(chunkHash2);
-
- assertThat(result).isLessThan(0);
- }
-
- @Test
- public void testCompareTo_whenArgumentSmaller_returnsPositive() {
- ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
- ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
-
- int result = chunkHash2.compareTo(chunkHash1);
-
- assertThat(result).isGreaterThan(0);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/ChunkListingMapTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/ChunkListingMapTest.java
deleted file mode 100644
index c5f78c2..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/ChunkListingMapTest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.chunk;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import com.google.common.base.Charsets;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ChunkListingMapTest {
- private static final ChunkHash CHUNK_A_HASH = getHash("CHUNK_A");
- private static final ChunkHash CHUNK_B_HASH = getHash("CHUNK_B");
- private static final ChunkHash CHUNK_C_HASH = getHash("CHUNK_C");
-
- private static final int CHUNK_A_LENGTH = 256;
- private static final int CHUNK_B_LENGTH = 1024;
- private static final int CHUNK_C_LENGTH = 4055;
-
- private static final int CHUNK_A_START = 0;
- private static final int CHUNK_B_START = CHUNK_A_START + CHUNK_A_LENGTH;
- private static final int CHUNK_C_START = CHUNK_B_START + CHUNK_B_LENGTH;
-
- private ChunkListingMap mChunkListingMap;
-
- @Before
- public void setUp() {
- mChunkListingMap = createFromFixture();
- }
-
- @Test
- public void hasChunk_isTrueForExistingChunks() {
- assertThat(mChunkListingMap.hasChunk(CHUNK_A_HASH)).isTrue();
- assertThat(mChunkListingMap.hasChunk(CHUNK_B_HASH)).isTrue();
- assertThat(mChunkListingMap.hasChunk(CHUNK_C_HASH)).isTrue();
- }
-
- @Test
- public void hasChunk_isFalseForNonexistentChunks() {
- assertThat(mChunkListingMap.hasChunk(getHash("CHUNK_D"))).isFalse();
- assertThat(mChunkListingMap.hasChunk(getHash(""))).isFalse();
- }
-
- @Test
- public void getChunkListing_hasCorrectLengths() {
- assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getLength())
- .isEqualTo(CHUNK_A_LENGTH);
- assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getLength())
- .isEqualTo(CHUNK_B_LENGTH);
- assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getLength())
- .isEqualTo(CHUNK_C_LENGTH);
- }
-
- @Test
- public void getChunkListing_hasCorrectStarts() {
- assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getStart())
- .isEqualTo(CHUNK_A_START);
- assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getStart())
- .isEqualTo(CHUNK_B_START);
- assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getStart())
- .isEqualTo(CHUNK_C_START);
- }
-
- @Test
- public void getChunkListing_isNullForNonExistentChunks() {
- assertThat(mChunkListingMap.getChunkEntry(getHash("Hey"))).isNull();
- }
-
- private static ChunkListingMap createFromFixture() {
- ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
- chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
- chunkListing.chunks[0] = newChunk(CHUNK_A_HASH.getHash(), CHUNK_A_LENGTH);
- chunkListing.chunks[1] = newChunk(CHUNK_B_HASH.getHash(), CHUNK_B_LENGTH);
- chunkListing.chunks[2] = newChunk(CHUNK_C_HASH.getHash(), CHUNK_C_LENGTH);
- return ChunkListingMap.fromProto(chunkListing);
- }
-
- private static ChunkHash getHash(String name) {
- return new ChunkHash(
- Arrays.copyOf(name.getBytes(Charsets.UTF_8), ChunkHash.HASH_LENGTH_BYTES));
- }
-
- public static ChunksMetadataProto.Chunk newChunk(byte[] hash, int length) {
- ChunksMetadataProto.Chunk newChunk = new ChunksMetadataProto.Chunk();
- newChunk.hash = Arrays.copyOf(hash, hash.length);
- newChunk.length = length;
- return newChunk;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java
deleted file mode 100644
index c6b29b7b..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunk;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class EncryptedChunkOrderingTest {
- private static final byte[] TEST_BYTE_ARRAY_1 = new byte[] {1, 2, 3, 4, 5};
- private static final byte[] TEST_BYTE_ARRAY_2 = new byte[] {5, 4, 3, 2, 1};
-
- @Test
- public void testEncryptedChunkOrdering_returnsValue() {
- EncryptedChunkOrdering encryptedChunkOrdering =
- EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
-
- byte[] bytes = encryptedChunkOrdering.encryptedChunkOrdering();
-
- assertThat(bytes)
- .asList()
- .containsExactlyElementsIn(Bytes.asList(TEST_BYTE_ARRAY_1))
- .inOrder();
- }
-
- @Test
- public void testEquals() {
- EncryptedChunkOrdering chunkOrdering1 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
- EncryptedChunkOrdering equalChunkOrdering1 =
- EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
- EncryptedChunkOrdering chunkOrdering2 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_2);
-
- assertThat(chunkOrdering1).isEqualTo(equalChunkOrdering1);
- assertThat(chunkOrdering1).isNotEqualTo(chunkOrdering2);
- }
-
- @Test
- public void testHashCode() {
- EncryptedChunkOrdering chunkOrdering1 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
- EncryptedChunkOrdering equalChunkOrdering1 =
- EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
- EncryptedChunkOrdering chunkOrdering2 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_2);
-
- int hash1 = chunkOrdering1.hashCode();
- int equalHash1 = equalChunkOrdering1.hashCode();
- int hash2 = chunkOrdering2.hashCode();
-
- assertThat(hash1).isEqualTo(equalHash1);
- assertThat(hash1).isNotEqualTo(hash2);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/BackupFileBuilderTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/BackupFileBuilderTest.java
deleted file mode 100644
index 590938e..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/BackupFileBuilderTest.java
+++ /dev/null
@@ -1,614 +0,0 @@
-/*
- * 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.server.backup.encryption.protos.nano.ChunksMetadataProto.AES_256_GCM;
-import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
-import static com.android.server.backup.testing.CryptoTestUtils.newChunk;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static junit.framework.Assert.fail;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.testng.Assert.assertThrows;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.testing.DiffScriptProcessor;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.io.Files;
-import com.google.common.primitives.Bytes;
-import com.google.common.primitives.Longs;
-
-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.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class BackupFileBuilderTest {
- private static final String TEST_DATA_1 =
- "I'm already there or close to [T7-9/executive level] in terms of big-picture vision";
- private static final String TEST_DATA_2 =
- "I was known for Real Games and should have been brought in for advice";
- private static final String TEST_DATA_3 =
- "Pride is rooted in the delusional belief held by all humans in an unchanging self";
-
- private static final byte[] TEST_FINGERPRINT_MIXER_SALT =
- Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
-
- private static final ChunkHash TEST_HASH_1 =
- new ChunkHash(Arrays.copyOf(new byte[] {0}, EncryptedChunk.KEY_LENGTH_BYTES));
- private static final ChunkHash TEST_HASH_2 =
- new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
- private static final ChunkHash TEST_HASH_3 =
- new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
-
- private static final byte[] TEST_NONCE =
- Arrays.copyOf(new byte[] {3}, EncryptedChunk.NONCE_LENGTH_BYTES);
-
- private static final EncryptedChunk TEST_CHUNK_1 =
- EncryptedChunk.create(TEST_HASH_1, TEST_NONCE, TEST_DATA_1.getBytes(UTF_8));
- private static final EncryptedChunk TEST_CHUNK_2 =
- EncryptedChunk.create(TEST_HASH_2, TEST_NONCE, TEST_DATA_2.getBytes(UTF_8));
- private static final EncryptedChunk TEST_CHUNK_3 =
- EncryptedChunk.create(TEST_HASH_3, TEST_NONCE, TEST_DATA_3.getBytes(UTF_8));
-
- private static final byte[] TEST_CHECKSUM = {1, 2, 3, 4, 5, 6};
-
- @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-
- private File mOldFile;
- private ChunksMetadataProto.ChunkListing mOldChunkListing;
- private EncryptedChunkEncoder mEncryptedChunkEncoder;
-
- @Before
- public void setUp() {
- mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
- }
-
- @Test
- public void writeChunks_nonIncremental_writesCorrectRawData() throws Exception {
- ByteArrayOutputStream output = new ByteArrayOutputStream();
- BackupFileBuilder backupFileBuilder = BackupFileBuilder.createForNonIncremental(output);
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
- getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
-
- byte[] actual = output.toByteArray();
- byte[] expected =
- Bytes.concat(
- TEST_CHUNK_1.nonce(),
- TEST_CHUNK_1.encryptedBytes(),
- TEST_CHUNK_2.nonce(),
- TEST_CHUNK_2.encryptedBytes());
- assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
- }
-
- @Test
- public void writeChunks_nonIncrementalWithDuplicates_writesEachChunkOnlyOnce()
- throws Exception {
- ByteArrayOutputStream output = new ByteArrayOutputStream();
- BackupFileBuilder backupFileBuilder = BackupFileBuilder.createForNonIncremental(output);
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_1),
- getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
-
- byte[] actual = output.toByteArray();
- byte[] expected =
- Bytes.concat(
- TEST_CHUNK_1.nonce(),
- TEST_CHUNK_1.encryptedBytes(),
- TEST_CHUNK_2.nonce(),
- TEST_CHUNK_2.encryptedBytes());
- assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
- }
-
- @Test
- public void writeChunks_incremental_writesParsableDiffScript() throws Exception {
- // We will insert chunk 2 in between chunks 1 and 3.
- setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
- ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
- getNewChunkMap(TEST_HASH_2));
- backupFileBuilder.finish(getTestMetadata());
-
- byte[] actual =
- stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
- byte[] expected =
- Bytes.concat(
- TEST_CHUNK_1.nonce(),
- TEST_CHUNK_1.encryptedBytes(),
- TEST_CHUNK_2.nonce(),
- TEST_CHUNK_2.encryptedBytes(),
- TEST_CHUNK_3.nonce(),
- TEST_CHUNK_3.encryptedBytes());
- assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
- }
-
- @Test
- public void writeChunks_incrementalWithDuplicates_writesEachChunkOnlyOnce() throws Exception {
- // We will insert chunk 2 twice in between chunks 1 and 3.
- setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
- ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_2, TEST_HASH_3),
- getNewChunkMap(TEST_HASH_2));
- backupFileBuilder.finish(getTestMetadata());
-
- byte[] actual =
- stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
- byte[] expected =
- Bytes.concat(
- TEST_CHUNK_1.nonce(),
- TEST_CHUNK_1.encryptedBytes(),
- TEST_CHUNK_2.nonce(),
- TEST_CHUNK_2.encryptedBytes(),
- TEST_CHUNK_3.nonce(),
- TEST_CHUNK_3.encryptedBytes());
- assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
- }
-
- @Test
- public void writeChunks_writesChunksInOrderOfHash() throws Exception {
- setUpOldBackupWithChunks(ImmutableList.of());
- ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
-
- // Write chunks out of order.
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_2, TEST_HASH_1),
- getNewChunkMap(TEST_HASH_2, TEST_HASH_1));
- backupFileBuilder.finish(getTestMetadata());
-
- byte[] actual =
- stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
- byte[] expected =
- Bytes.concat(
- TEST_CHUNK_1.nonce(),
- TEST_CHUNK_1.encryptedBytes(),
- TEST_CHUNK_2.nonce(),
- TEST_CHUNK_2.encryptedBytes());
- assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
- }
-
- @Test
- public void writeChunks_alreadyFlushed_throwsException() throws Exception {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
- backupFileBuilder.finish(getTestMetadata());
-
- assertThrows(
- IllegalStateException.class,
- () -> backupFileBuilder.writeChunks(ImmutableList.of(), getNewChunkMap()));
- }
-
- @Test
- public void getNewChunkListing_hasChunksInOrderOfKey() throws Exception {
- // We will insert chunk 2 in between chunks 1 and 3.
- setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- // Write chunks out of order.
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2),
- getNewChunkMap(TEST_HASH_2));
- backupFileBuilder.finish(getTestMetadata());
-
- ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
- ChunksMetadataProto.ChunkListing actual =
- backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
- assertListingsEqual(actual, expected);
- }
-
- @Test
- public void getNewChunkListing_writeChunksInTwoBatches_returnsListingContainingAllChunks()
- throws Exception {
- // We will insert chunk 2 in between chunks 1 and 3.
- setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2), getNewChunkMap(TEST_HASH_2));
- backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_3), getNewChunkMap(TEST_HASH_2));
- backupFileBuilder.finish(getTestMetadata());
-
- ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
- ChunksMetadataProto.ChunkListing actual =
- backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
- assertListingsEqual(actual, expected);
- }
-
- @Test
- public void getNewChunkListing_writeDuplicateChunks_writesEachChunkOnlyOnce() throws Exception {
- // We will append [2][3][3][2] onto [1].
- setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1));
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
- getNewChunkMap(TEST_HASH_3, TEST_HASH_2));
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_3, TEST_HASH_2),
- getNewChunkMap(TEST_HASH_3, TEST_HASH_2));
- backupFileBuilder.finish(getTestMetadata());
-
- ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
- ChunksMetadataProto.ChunkListing actual =
- backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
- assertListingsEqual(actual, expected);
- }
-
- @Test
- public void getNewChunkListing_nonIncrementalWithNoSalt_doesNotThrowOnSerialisation() {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
-
- // Does not throw.
- ChunksMetadataProto.ChunkListing.toByteArray(newChunkListing);
- }
-
- @Test
- public void getNewChunkListing_incrementalWithNoSalt_doesNotThrowOnSerialisation()
- throws Exception {
-
- setUpOldBackupWithChunks(ImmutableList.of());
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
-
- // Does not throw.
- ChunksMetadataProto.ChunkListing.toByteArray(newChunkListing);
- }
-
- @Test
- public void getNewChunkListing_nonIncrementalWithNoSalt_hasEmptySalt() {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
-
- assertThat(newChunkListing.fingerprintMixerSalt).isEmpty();
- }
-
- @Test
- public void getNewChunkListing_incrementalWithNoSalt_hasEmptySalt() throws Exception {
- setUpOldBackupWithChunks(ImmutableList.of());
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
-
- assertThat(newChunkListing.fingerprintMixerSalt).isEmpty();
- }
-
- @Test
- public void getNewChunkListing_nonIncrementalWithSalt_hasGivenSalt() {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
-
- assertThat(newChunkListing.fingerprintMixerSalt).isEqualTo(TEST_FINGERPRINT_MIXER_SALT);
- }
-
- @Test
- public void getNewChunkListing_incrementalWithSalt_hasGivenSalt() throws Exception {
- setUpOldBackupWithChunks(ImmutableList.of());
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
-
- assertThat(newChunkListing.fingerprintMixerSalt).isEqualTo(TEST_FINGERPRINT_MIXER_SALT);
- }
-
- @Test
- public void getNewChunkListing_nonIncremental_hasCorrectCipherTypeAndChunkOrderingType() {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
-
- assertThat(newChunkListing.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
- assertThat(newChunkListing.chunkOrderingType)
- .isEqualTo(ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED);
- }
-
- @Test
- public void getNewChunkListing_incremental_hasCorrectCipherTypeAndChunkOrderingType()
- throws Exception {
- setUpOldBackupWithChunks(ImmutableList.of());
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), mOldChunkListing);
-
- ChunksMetadataProto.ChunkListing newChunkListing =
- backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
-
- assertThat(newChunkListing.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
- assertThat(newChunkListing.chunkOrderingType)
- .isEqualTo(ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED);
- }
-
- @Test
- public void getNewChunkOrdering_chunksHaveCorrectStartPositions() throws Exception {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
-
- // Write out of order by key to check that ordering is maintained.
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2),
- getNewChunkMap(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2));
- backupFileBuilder.finish(getTestMetadata());
-
- ChunksMetadataProto.ChunkOrdering actual =
- backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
- // The chunks are listed in the order they are written above, but the start positions are
- // determined by the order in the encrypted blob (which is lexicographical by key).
- int chunk1Start = 0;
- int chunk2Start =
- chunk1Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1);
- int chunk3Start =
- chunk2Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2);
-
- int[] expected = {chunk1Start, chunk3Start, chunk2Start};
- assertThat(actual.starts.length).isEqualTo(expected.length);
- for (int i = 0; i < actual.starts.length; i++) {
- assertThat(expected[i]).isEqualTo(actual.starts[i]);
- }
- }
-
- @Test
- public void getNewChunkOrdering_duplicateChunks_writesDuplicates() throws Exception {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
-
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_2),
- getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
- backupFileBuilder.writeChunks(
- ImmutableList.of(TEST_HASH_3, TEST_HASH_3), getNewChunkMap(TEST_HASH_3));
- backupFileBuilder.finish(getTestMetadata());
-
- ChunksMetadataProto.ChunkOrdering actual =
- backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
- int chunk1Start = 0;
- int chunk2Start =
- chunk1Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1);
- int chunk3Start =
- chunk2Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2);
-
- int[] expected = {chunk1Start, chunk2Start, chunk2Start, chunk3Start, chunk3Start};
- assertThat(actual.starts.length).isEqualTo(expected.length);
- for (int i = 0; i < actual.starts.length; i++) {
- assertThat(expected[i]).isEqualTo(actual.starts[i]);
- }
- }
-
- @Test
- public void getNewChunkOrdering_returnsOrderingWithChecksum() throws Exception {
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
-
- backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_1), getNewChunkMap(TEST_HASH_1));
- backupFileBuilder.finish(getTestMetadata());
-
- ChunksMetadataProto.ChunkOrdering actual =
- backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
- assertThat(actual.checksum).isEqualTo(TEST_CHECKSUM);
- }
-
- @Test
- public void finish_writesMetadata() throws Exception {
- ByteArrayOutputStream output = new ByteArrayOutputStream();
- BackupFileBuilder builder = BackupFileBuilder.createForNonIncremental(output);
- ChunksMetadataProto.ChunksMetadata expectedMetadata = getTestMetadata();
-
- builder.finish(expectedMetadata);
-
- // The output is [metadata]+[long giving size of metadata].
- byte[] metadataBytes =
- Arrays.copyOfRange(output.toByteArray(), 0, output.size() - Long.BYTES);
- ChunksMetadataProto.ChunksMetadata actualMetadata =
- ChunksMetadataProto.ChunksMetadata.parseFrom(metadataBytes);
- assertThat(actualMetadata.checksumType).isEqualTo(ChunksMetadataProto.SHA_256);
- assertThat(actualMetadata.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
- }
-
- @Test
- public void finish_writesMetadataPosition() throws Exception {
- ByteArrayOutputStream output = new ByteArrayOutputStream();
- BackupFileBuilder builder = BackupFileBuilder.createForNonIncremental(output);
-
- builder.writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
- getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
- builder.writeChunks(ImmutableList.of(TEST_HASH_3), getNewChunkMap(TEST_HASH_3));
- builder.finish(getTestMetadata());
-
- long expectedPosition =
- (long) mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1)
- + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2)
- + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_3);
- long actualPosition =
- Longs.fromByteArray(
- Arrays.copyOfRange(
- output.toByteArray(), output.size() - Long.BYTES, output.size()));
- assertThat(actualPosition).isEqualTo(expectedPosition);
- }
-
- @Test
- public void finish_flushesOutputStream() throws Exception {
- OutputStream diffOutputStream = mock(OutputStream.class);
- BackupFileBuilder backupFileBuilder =
- BackupFileBuilder.createForIncremental(
- diffOutputStream, new ChunksMetadataProto.ChunkListing());
-
- backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_1), getNewChunkMap(TEST_HASH_1));
- diffOutputStream.flush();
-
- verify(diffOutputStream).flush();
- }
-
- private void setUpOldBackupWithChunks(List<EncryptedChunk> chunks) throws Exception {
- mOldFile = mTemporaryFolder.newFile();
- ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
- chunkListing.fingerprintMixerSalt =
- Arrays.copyOf(TEST_FINGERPRINT_MIXER_SALT, TEST_FINGERPRINT_MIXER_SALT.length);
- chunkListing.cipherType = AES_256_GCM;
- chunkListing.chunkOrderingType = CHUNK_ORDERING_TYPE_UNSPECIFIED;
-
- List<ChunksMetadataProto.Chunk> knownChunks = new ArrayList<>();
- try (FileOutputStream outputStream = new FileOutputStream(mOldFile)) {
- for (EncryptedChunk chunk : chunks) {
- // Chunks are encoded in the format [nonce]+[data].
- outputStream.write(chunk.nonce());
- outputStream.write(chunk.encryptedBytes());
-
- knownChunks.add(createChunkFor(chunk));
- }
-
- outputStream.flush();
- }
-
- chunkListing.chunks = knownChunks.toArray(new ChunksMetadataProto.Chunk[0]);
- mOldChunkListing = chunkListing;
- }
-
- private byte[] parseDiffScript(byte[] diffScript) throws Exception {
- File newFile = mTemporaryFolder.newFile();
- new DiffScriptProcessor(mOldFile, newFile).process(new ByteArrayInputStream(diffScript));
- return Files.toByteArray(newFile);
- }
-
- 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);
- }
- }
-
- private static ImmutableMap<ChunkHash, EncryptedChunk> getNewChunkMap(ChunkHash... hashes) {
- ImmutableMap.Builder<ChunkHash, EncryptedChunk> builder = ImmutableMap.builder();
- for (ChunkHash hash : hashes) {
- if (TEST_HASH_1.equals(hash)) {
- builder.put(TEST_HASH_1, TEST_CHUNK_1);
- } else if (TEST_HASH_2.equals(hash)) {
- builder.put(TEST_HASH_2, TEST_CHUNK_2);
- } else if (TEST_HASH_3.equals(hash)) {
- builder.put(TEST_HASH_3, TEST_CHUNK_3);
- } else {
- fail("Hash was not recognised: " + hash);
- }
- }
- return builder.build();
- }
-
- private static ChunksMetadataProto.ChunksMetadata getTestMetadata() {
- ChunksMetadataProto.ChunksMetadata metadata = new ChunksMetadataProto.ChunksMetadata();
- metadata.checksumType = ChunksMetadataProto.SHA_256;
- metadata.cipherType = AES_256_GCM;
- return metadata;
- }
-
- private static byte[] stripMetadataAndPositionFromOutput(byte[] output) {
- long metadataStart =
- Longs.fromByteArray(
- Arrays.copyOfRange(output, output.length - Long.BYTES, output.length));
- return Arrays.copyOfRange(output, 0, (int) metadataStart);
- }
-
- private ChunksMetadataProto.ChunkListing expectedChunkListing() {
- ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
- chunkListing.fingerprintMixerSalt =
- Arrays.copyOf(TEST_FINGERPRINT_MIXER_SALT, TEST_FINGERPRINT_MIXER_SALT.length);
- chunkListing.cipherType = AES_256_GCM;
- chunkListing.chunkOrderingType = CHUNK_ORDERING_TYPE_UNSPECIFIED;
- chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
- chunkListing.chunks[0] = createChunkFor(TEST_CHUNK_1);
- chunkListing.chunks[1] = createChunkFor(TEST_CHUNK_2);
- chunkListing.chunks[2] = createChunkFor(TEST_CHUNK_3);
- return chunkListing;
- }
-
- private ChunksMetadataProto.Chunk createChunkFor(EncryptedChunk encryptedChunk) {
- byte[] chunkHash = encryptedChunk.key().getHash();
- byte[] hashCopy = Arrays.copyOf(chunkHash, chunkHash.length);
- return newChunk(hashCopy, mEncryptedChunkEncoder.getEncodedLengthOfChunk(encryptedChunk));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ByteRangeTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ByteRangeTest.java
deleted file mode 100644
index 8df0826..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ByteRangeTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static org.junit.Assert.assertEquals;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-/** Tests for {@link ByteRange}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ByteRangeTest {
- @Test
- public void getLength_includesEnd() throws Exception {
- ByteRange byteRange = new ByteRange(5, 10);
-
- int length = byteRange.getLength();
-
- assertEquals(6, length);
- }
-
- @Test
- public void constructor_rejectsNegativeStart() {
- assertThrows(IllegalArgumentException.class, () -> new ByteRange(-1, 10));
- }
-
- @Test
- public void constructor_rejectsEndBeforeStart() {
- assertThrows(IllegalArgumentException.class, () -> new ByteRange(10, 9));
- }
-
- @Test
- public void extend_withZeroLength_throwsException() {
- ByteRange byteRange = new ByteRange(5, 10);
-
- assertThrows(IllegalArgumentException.class, () -> byteRange.extend(0));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ChunkEncryptorTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ChunkEncryptorTest.java
deleted file mode 100644
index 19e3b28..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ChunkEncryptorTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-import org.robolectric.RobolectricTestRunner;
-
-import java.security.SecureRandom;
-
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ChunkEncryptorTest {
- private static final String MAC_ALGORITHM = "HmacSHA256";
- 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 String CHUNK_PLAINTEXT =
- "A little Learning is a dang'rous Thing;\n"
- + "Drink deep, or taste not the Pierian Spring:\n"
- + "There shallow Draughts intoxicate the Brain,\n"
- + "And drinking largely sobers us again.";
- private static final byte[] PLAINTEXT_BYTES = CHUNK_PLAINTEXT.getBytes(UTF_8);
- private static final byte[] NONCE_1 = "0123456789abc".getBytes(UTF_8);
- private static final byte[] NONCE_2 = "123456789abcd".getBytes(UTF_8);
-
- private static final byte[][] NONCES = new byte[][] {NONCE_1, NONCE_2};
-
- @Mock private SecureRandom mSecureRandomMock;
- private SecretKey mSecretKey;
- private ChunkHash mPlaintextHash;
- private ChunkEncryptor mChunkEncryptor;
-
- @Before
- public void setUp() throws Exception {
- mSecretKey = generateAesKey();
- ChunkHasher chunkHasher = new ChunkHasher(mSecretKey);
- mPlaintextHash = chunkHasher.computeHash(PLAINTEXT_BYTES);
- mSecureRandomMock = mock(SecureRandom.class);
- mChunkEncryptor = new ChunkEncryptor(mSecretKey, mSecureRandomMock);
-
- // Return NONCE_1, then NONCE_2 for invocations of mSecureRandomMock.nextBytes().
- doAnswer(
- new Answer<Void>() {
- private int mInvocation = 0;
-
- @Override
- public Void answer(InvocationOnMock invocation) {
- byte[] nonceDestination = invocation.getArgument(0);
- System.arraycopy(
- NONCES[this.mInvocation],
- 0,
- nonceDestination,
- 0,
- GCM_NONCE_LENGTH_BYTES);
- this.mInvocation++;
- return null;
- }
- })
- .when(mSecureRandomMock)
- .nextBytes(any(byte[].class));
- }
-
- @Test
- public void encrypt_withHash_resultContainsHashAsKey() throws Exception {
- EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- assertThat(chunk.key()).isEqualTo(mPlaintextHash);
- }
-
- @Test
- public void encrypt_generatesHmacOfPlaintext() throws Exception {
- EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- byte[] generatedHash = chunk.key().getHash();
- Mac mac = Mac.getInstance(MAC_ALGORITHM);
- mac.init(mSecretKey);
- byte[] plaintextHmac = mac.doFinal(PLAINTEXT_BYTES);
- assertThat(generatedHash).isEqualTo(plaintextHmac);
- }
-
- @Test
- public void encrypt_whenInvokedAgain_generatesNewNonce() throws Exception {
- EncryptedChunk chunk1 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- EncryptedChunk chunk2 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- assertThat(chunk1.nonce()).isNotEqualTo(chunk2.nonce());
- }
-
- @Test
- public void encrypt_whenInvokedAgain_generatesNewCiphertext() throws Exception {
- EncryptedChunk chunk1 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- EncryptedChunk chunk2 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- assertThat(chunk1.encryptedBytes()).isNotEqualTo(chunk2.encryptedBytes());
- }
-
- @Test
- public void encrypt_generates12ByteNonce() throws Exception {
- EncryptedChunk encryptedChunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- byte[] nonce = encryptedChunk.nonce();
- assertThat(nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
- }
-
- @Test
- public void encrypt_decryptedResultCorrespondsToPlaintext() throws Exception {
- EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
-
- Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- cipher.init(
- Cipher.DECRYPT_MODE,
- mSecretKey,
- new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, chunk.nonce()));
- byte[] decrypted = cipher.doFinal(chunk.encryptedBytes());
- assertThat(decrypted).isEqualTo(PLAINTEXT_BYTES);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ChunkHasherTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ChunkHasherTest.java
deleted file mode 100644
index 72a927d..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ChunkHasherTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ChunkHasherTest {
- private static final String KEY_ALGORITHM = "AES";
- private static final String MAC_ALGORITHM = "HmacSHA256";
-
- private static final byte[] TEST_KEY = {100, 120};
- private static final byte[] TEST_DATA = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
-
- private SecretKey mSecretKey;
- private ChunkHasher mChunkHasher;
-
- @Before
- public void setUp() throws Exception {
- mSecretKey = new SecretKeySpec(TEST_KEY, KEY_ALGORITHM);
- mChunkHasher = new ChunkHasher(mSecretKey);
- }
-
- @Test
- public void computeHash_returnsHmacForData() throws Exception {
- ChunkHash chunkHash = mChunkHasher.computeHash(TEST_DATA);
-
- byte[] hash = chunkHash.getHash();
- Mac mac = Mac.getInstance(MAC_ALGORITHM);
- mac.init(mSecretKey);
- byte[] expectedHash = mac.doFinal(TEST_DATA);
- assertThat(hash).isEqualTo(expectedHash);
- }
-
- @Test
- public void computeHash_generates256BitHmac() throws Exception {
- int expectedLength = 256 / Byte.SIZE;
-
- byte[] hash = mChunkHasher.computeHash(TEST_DATA).getHash();
-
- assertThat(hash).hasLength(expectedLength);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java
deleted file mode 100644
index 823a63c..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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 org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
-
-import com.google.common.io.Files;
-import com.google.common.primitives.Bytes;
-
-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.FileNotFoundException;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class DecryptedChunkFileOutputTest {
- private static final byte[] TEST_CHUNK_1 = {1, 2, 3};
- private static final byte[] TEST_CHUNK_2 = {4, 5, 6, 7, 8, 9, 10};
- private static final int TEST_BUFFER_LENGTH =
- Math.max(TEST_CHUNK_1.length, TEST_CHUNK_2.length);
-
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- private File mOutputFile;
- private DecryptedChunkFileOutput mDecryptedChunkFileOutput;
-
- @Before
- public void setUp() throws Exception {
- mOutputFile = temporaryFolder.newFile();
- mDecryptedChunkFileOutput = new DecryptedChunkFileOutput(mOutputFile);
- }
-
- @Test
- public void open_returnsInstance() throws Exception {
- DecryptedChunkOutput result = mDecryptedChunkFileOutput.open();
- assertThat(result).isEqualTo(mDecryptedChunkFileOutput);
- }
-
- @Test
- public void open_nonExistentOutputFolder_throwsException() throws Exception {
- mDecryptedChunkFileOutput =
- new DecryptedChunkFileOutput(
- new File(temporaryFolder.newFolder(), "mOutput/directory"));
- assertThrows(FileNotFoundException.class, () -> mDecryptedChunkFileOutput.open());
- }
-
- @Test
- public void open_whenRunTwice_throwsException() throws Exception {
- mDecryptedChunkFileOutput.open();
- assertThrows(IllegalStateException.class, () -> mDecryptedChunkFileOutput.open());
- }
-
- @Test
- public void processChunk_beforeOpen_throwsException() throws Exception {
- assertThrows(IllegalStateException.class,
- () -> mDecryptedChunkFileOutput.processChunk(new byte[0], 0));
- }
-
- @Test
- public void processChunk_writesChunksToFile() throws Exception {
- processTestChunks();
-
- assertThat(Files.toByteArray(mOutputFile))
- .isEqualTo(Bytes.concat(TEST_CHUNK_1, TEST_CHUNK_2));
- }
-
- @Test
- public void getDigest_beforeClose_throws() throws Exception {
- mDecryptedChunkFileOutput.open();
- assertThrows(IllegalStateException.class, () -> mDecryptedChunkFileOutput.getDigest());
- }
-
- @Test
- public void getDigest_returnsCorrectDigest() throws Exception {
- processTestChunks();
-
- byte[] actualDigest = mDecryptedChunkFileOutput.getDigest();
-
- MessageDigest expectedDigest =
- MessageDigest.getInstance(DecryptedChunkFileOutput.DIGEST_ALGORITHM);
- expectedDigest.update(TEST_CHUNK_1);
- expectedDigest.update(TEST_CHUNK_2);
- assertThat(actualDigest).isEqualTo(expectedDigest.digest());
- }
-
- @Test
- public void getDigest_whenRunTwice_returnsIdenticalDigestBothTimes() throws Exception {
- processTestChunks();
-
- byte[] digest1 = mDecryptedChunkFileOutput.getDigest();
- byte[] digest2 = mDecryptedChunkFileOutput.getDigest();
-
- assertThat(digest1).isEqualTo(digest2);
- }
-
- private void processTestChunks() throws IOException {
- mDecryptedChunkFileOutput.open();
- mDecryptedChunkFileOutput.processChunk(Arrays.copyOf(TEST_CHUNK_1, TEST_BUFFER_LENGTH),
- TEST_CHUNK_1.length);
- mDecryptedChunkFileOutput.processChunk(Arrays.copyOf(TEST_CHUNK_2, TEST_BUFFER_LENGTH),
- TEST_CHUNK_2.length);
- mDecryptedChunkFileOutput.close();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/DiffScriptBackupWriterTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/DiffScriptBackupWriterTest.java
deleted file mode 100644
index 2af6f2b..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/DiffScriptBackupWriterTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.IOException;
-
-/** Tests for {@link DiffScriptBackupWriter}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class DiffScriptBackupWriterTest {
- private static final byte[] TEST_BYTES = {1, 2, 3, 4, 5, 6, 7, 8, 9};
-
- @Captor private ArgumentCaptor<Byte> mBytesCaptor;
- @Mock private SingleStreamDiffScriptWriter mDiffScriptWriter;
- private BackupWriter mBackupWriter;
-
- @Before
- public void setUp() {
- mDiffScriptWriter = mock(SingleStreamDiffScriptWriter.class);
- mBackupWriter = new DiffScriptBackupWriter(mDiffScriptWriter);
- mBytesCaptor = ArgumentCaptor.forClass(Byte.class);
- }
-
- @Test
- public void writeBytes_writesBytesToWriter() throws Exception {
- mBackupWriter.writeBytes(TEST_BYTES);
-
- verify(mDiffScriptWriter, atLeastOnce()).writeByte(mBytesCaptor.capture());
- assertThat(mBytesCaptor.getAllValues())
- .containsExactlyElementsIn(Bytes.asList(TEST_BYTES))
- .inOrder();
- }
-
- @Test
- public void writeChunk_writesChunkToWriter() throws Exception {
- mBackupWriter.writeChunk(0, 10);
-
- verify(mDiffScriptWriter).writeChunk(0, 10);
- }
-
- @Test
- public void getBytesWritten_returnsTotalSum() throws Exception {
- mBackupWriter.writeBytes(TEST_BYTES);
- mBackupWriter.writeBytes(TEST_BYTES);
- mBackupWriter.writeChunk(/*start=*/ 0, /*length=*/ 10);
-
- long bytesWritten = mBackupWriter.getBytesWritten();
-
- assertThat(bytesWritten).isEqualTo(2 * TEST_BYTES.length + 10);
- }
-
- @Test
- public void flush_flushesWriter() throws IOException {
- mBackupWriter.flush();
-
- verify(mDiffScriptWriter).flush();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/EncryptedChunkTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/EncryptedChunkTest.java
deleted file mode 100644
index 325b601..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/EncryptedChunkTest.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class EncryptedChunkTest {
- private static final byte[] CHUNK_HASH_1_BYTES =
- Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES);
- private static final byte[] NONCE_1 =
- Arrays.copyOf(new byte[] {2}, EncryptedChunk.NONCE_LENGTH_BYTES);
- private static final byte[] ENCRYPTED_BYTES_1 =
- Arrays.copyOf(new byte[] {3}, EncryptedChunk.KEY_LENGTH_BYTES);
-
- private static final byte[] CHUNK_HASH_2_BYTES =
- Arrays.copyOf(new byte[] {4}, ChunkHash.HASH_LENGTH_BYTES);
- private static final byte[] NONCE_2 =
- Arrays.copyOf(new byte[] {5}, EncryptedChunk.NONCE_LENGTH_BYTES);
- private static final byte[] ENCRYPTED_BYTES_2 =
- Arrays.copyOf(new byte[] {6}, EncryptedChunk.KEY_LENGTH_BYTES);
-
- @Test
- public void testCreate_withIncorrectLength_throwsException() {
- ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
- byte[] shortNonce = Arrays.copyOf(new byte[] {2}, EncryptedChunk.NONCE_LENGTH_BYTES - 1);
-
- assertThrows(
- IllegalArgumentException.class,
- () -> EncryptedChunk.create(chunkHash, shortNonce, ENCRYPTED_BYTES_1));
- }
-
- @Test
- public void testEncryptedBytes_forNewlyCreatedObject_returnsCorrectValue() {
- ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
- EncryptedChunk encryptedChunk =
- EncryptedChunk.create(chunkHash, NONCE_1, ENCRYPTED_BYTES_1);
-
- byte[] returnedBytes = encryptedChunk.encryptedBytes();
-
- assertThat(returnedBytes)
- .asList()
- .containsExactlyElementsIn(Bytes.asList(ENCRYPTED_BYTES_1))
- .inOrder();
- }
-
- @Test
- public void testKey_forNewlyCreatedObject_returnsCorrectValue() {
- ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
- EncryptedChunk encryptedChunk =
- EncryptedChunk.create(chunkHash, NONCE_1, ENCRYPTED_BYTES_1);
-
- ChunkHash returnedKey = encryptedChunk.key();
-
- assertThat(returnedKey).isEqualTo(chunkHash);
- }
-
- @Test
- public void testNonce_forNewlycreatedObject_returnCorrectValue() {
- ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
- EncryptedChunk encryptedChunk =
- EncryptedChunk.create(chunkHash, NONCE_1, ENCRYPTED_BYTES_1);
-
- byte[] returnedNonce = encryptedChunk.nonce();
-
- assertThat(returnedNonce).asList().containsExactlyElementsIn(Bytes.asList(NONCE_1));
- }
-
- @Test
- public void testEquals() {
- ChunkHash chunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
- ChunkHash equalChunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
- ChunkHash chunkHash2 = new ChunkHash(CHUNK_HASH_2_BYTES);
- EncryptedChunk encryptedChunk1 =
- EncryptedChunk.create(chunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
- EncryptedChunk equalEncryptedChunk1 =
- EncryptedChunk.create(equalChunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
- EncryptedChunk encryptedChunk2 =
- EncryptedChunk.create(chunkHash2, NONCE_2, ENCRYPTED_BYTES_2);
-
- assertThat(encryptedChunk1).isEqualTo(equalEncryptedChunk1);
- assertThat(encryptedChunk1).isNotEqualTo(encryptedChunk2);
- }
-
- @Test
- public void testHashCode() {
- ChunkHash chunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
- ChunkHash equalChunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
- ChunkHash chunkHash2 = new ChunkHash(CHUNK_HASH_2_BYTES);
- EncryptedChunk encryptedChunk1 =
- EncryptedChunk.create(chunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
- EncryptedChunk equalEncryptedChunk1 =
- EncryptedChunk.create(equalChunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
- EncryptedChunk encryptedChunk2 =
- EncryptedChunk.create(chunkHash2, NONCE_2, ENCRYPTED_BYTES_2);
-
- int hash1 = encryptedChunk1.hashCode();
- int equalHash1 = equalEncryptedChunk1.hashCode();
- int hash2 = encryptedChunk2.hashCode();
-
- assertThat(hash1).isEqualTo(equalHash1);
- assertThat(hash1).isNotEqualTo(hash2);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoderTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoderTest.java
deleted file mode 100644
index 7e1fded..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoderTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class InlineLengthsEncryptedChunkEncoderTest {
-
- private static final byte[] TEST_NONCE =
- Arrays.copyOf(new byte[] {1}, EncryptedChunk.NONCE_LENGTH_BYTES);
- private static final byte[] TEST_KEY_DATA =
- Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES);
- private static final byte[] TEST_DATA = {5, 4, 5, 7, 10, 12, 1, 2, 9};
-
- @Mock private BackupWriter mMockBackupWriter;
- private ChunkHash mTestKey;
- private EncryptedChunk mTestChunk;
- private EncryptedChunkEncoder mEncoder;
-
- @Before
- public void setUp() throws Exception {
- mMockBackupWriter = mock(BackupWriter.class);
- mTestKey = new ChunkHash(TEST_KEY_DATA);
- mTestChunk = EncryptedChunk.create(mTestKey, TEST_NONCE, TEST_DATA);
- mEncoder = new InlineLengthsEncryptedChunkEncoder();
- }
-
- @Test
- public void writeChunkToWriter_writesLengthThenNonceThenData() throws Exception {
- mEncoder.writeChunkToWriter(mMockBackupWriter, mTestChunk);
-
- InOrder inOrder = inOrder(mMockBackupWriter);
- inOrder.verify(mMockBackupWriter)
- .writeBytes(
- InlineLengthsEncryptedChunkEncoder.toByteArray(
- TEST_NONCE.length + TEST_DATA.length));
- inOrder.verify(mMockBackupWriter).writeBytes(TEST_NONCE);
- inOrder.verify(mMockBackupWriter).writeBytes(TEST_DATA);
- }
-
- @Test
- public void getEncodedLengthOfChunk_returnsSumOfNonceAndDataLengths() {
- int encodedLength = mEncoder.getEncodedLengthOfChunk(mTestChunk);
-
- assertThat(encodedLength).isEqualTo(Integer.BYTES + TEST_NONCE.length + TEST_DATA.length);
- }
-
- @Test
- public void getChunkOrderingType_returnsExplicitStartsType() {
- assertThat(mEncoder.getChunkOrderingType()).isEqualTo(ChunksMetadataProto.INLINE_LENGTHS);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoderTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoderTest.java
deleted file mode 100644
index 6f58ee1..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoderTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class LengthlessEncryptedChunkEncoderTest {
- private static final byte[] TEST_NONCE =
- Arrays.copyOf(new byte[] {1}, EncryptedChunk.NONCE_LENGTH_BYTES);
- private static final byte[] TEST_KEY_DATA =
- Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES);
- private static final byte[] TEST_DATA = {5, 4, 5, 7, 10, 12, 1, 2, 9};
-
- @Mock private BackupWriter mMockBackupWriter;
- private ChunkHash mTestKey;
- private EncryptedChunk mTestChunk;
- private EncryptedChunkEncoder mEncoder;
-
- @Before
- public void setUp() throws Exception {
- mMockBackupWriter = mock(BackupWriter.class);
- mTestKey = new ChunkHash(TEST_KEY_DATA);
- mTestChunk = EncryptedChunk.create(mTestKey, TEST_NONCE, TEST_DATA);
- mEncoder = new LengthlessEncryptedChunkEncoder();
- }
-
- @Test
- public void writeChunkToWriter_writesNonceThenData() throws Exception {
- mEncoder.writeChunkToWriter(mMockBackupWriter, mTestChunk);
-
- InOrder inOrder = inOrder(mMockBackupWriter);
- inOrder.verify(mMockBackupWriter).writeBytes(TEST_NONCE);
- inOrder.verify(mMockBackupWriter).writeBytes(TEST_DATA);
- }
-
- @Test
- public void getEncodedLengthOfChunk_returnsSumOfNonceAndDataLengths() {
- int encodedLength = mEncoder.getEncodedLengthOfChunk(mTestChunk);
-
- assertThat(encodedLength).isEqualTo(TEST_NONCE.length + TEST_DATA.length);
- }
-
- @Test
- public void getChunkOrderingType_returnsExplicitStartsType() {
- assertThat(mEncoder.getChunkOrderingType()).isEqualTo(ChunksMetadataProto.EXPLICIT_STARTS);
- }
-}
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
deleted file mode 100644
index d73c8e4..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * 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/chunking/RawBackupWriterTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/RawBackupWriterTest.java
deleted file mode 100644
index 966d3e2..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/RawBackupWriterTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayOutputStream;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class RawBackupWriterTest {
- private static final byte[] TEST_BYTES = {1, 2, 3, 4, 5, 6};
-
- private BackupWriter mWriter;
- private ByteArrayOutputStream mOutput;
-
- @Before
- public void setUp() {
- mOutput = new ByteArrayOutputStream();
- mWriter = new RawBackupWriter(mOutput);
- }
-
- @Test
- public void writeBytes_writesToOutputStream() throws Exception {
- mWriter.writeBytes(TEST_BYTES);
-
- assertThat(mOutput.toByteArray())
- .asList()
- .containsExactlyElementsIn(Bytes.asList(TEST_BYTES))
- .inOrder();
- }
-
- @Test
- public void writeChunk_throwsUnsupportedOperationException() throws Exception {
- assertThrows(UnsupportedOperationException.class, () -> mWriter.writeChunk(0, 0));
- }
-
- @Test
- public void getBytesWritten_returnsTotalSum() throws Exception {
- mWriter.writeBytes(TEST_BYTES);
- mWriter.writeBytes(TEST_BYTES);
-
- long bytesWritten = mWriter.getBytesWritten();
-
- assertThat(bytesWritten).isEqualTo(2 * TEST_BYTES.length);
- }
-
- @Test
- public void flush_flushesOutputStream() throws Exception {
- mOutput = mock(ByteArrayOutputStream.class);
- mWriter = new RawBackupWriter(mOutput);
-
- mWriter.flush();
-
- verify(mOutput).flush();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriterTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriterTest.java
deleted file mode 100644
index 73baf80..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriterTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.Locale;
-
-/** Tests for {@link SingleStreamDiffScriptWriter}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class SingleStreamDiffScriptWriterTest {
- private static final int MAX_CHUNK_SIZE_IN_BYTES = 256;
- /** By default this Locale does not use Arabic numbers for %d formatting. */
- private static final Locale HINDI = new Locale("hi", "IN");
-
- private Locale mDefaultLocale;
- private ByteArrayOutputStream mOutputStream;
- private SingleStreamDiffScriptWriter mDiffScriptWriter;
-
- @Before
- public void setUp() {
- mDefaultLocale = Locale.getDefault();
- mOutputStream = new ByteArrayOutputStream();
- mDiffScriptWriter =
- new SingleStreamDiffScriptWriter(mOutputStream, MAX_CHUNK_SIZE_IN_BYTES);
- }
-
- @After
- public void tearDown() {
- Locale.setDefault(mDefaultLocale);
- }
-
- @Test
- public void writeChunk_withNegativeStart_throwsException() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mDiffScriptWriter.writeChunk(-1, 50));
- }
-
- @Test
- public void writeChunk_withZeroLength_throwsException() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mDiffScriptWriter.writeChunk(0, 0));
- }
-
- @Test
- public void writeChunk_withExistingBytesInBuffer_writesBufferFirst()
- throws IOException {
- String testString = "abcd";
- writeStringAsBytesToWriter(testString, mDiffScriptWriter);
-
- mDiffScriptWriter.writeChunk(0, 20);
- mDiffScriptWriter.flush();
-
- // Expected format: length of abcd, newline, abcd, newline, chunk start - chunk end
- assertThat(mOutputStream.toString("UTF-8")).isEqualTo(
- String.format("%d\n%s\n%d-%d\n", testString.length(), testString, 0, 19));
- }
-
- @Test
- public void writeChunk_overlappingPreviousChunk_combinesChunks() throws IOException {
- mDiffScriptWriter.writeChunk(3, 4);
-
- mDiffScriptWriter.writeChunk(7, 5);
- mDiffScriptWriter.flush();
-
- assertThat(mOutputStream.toString("UTF-8")).isEqualTo(String.format("3-11\n"));
- }
-
- @Test
- public void writeChunk_formatsByteIndexesUsingArabicNumbers() throws Exception {
- Locale.setDefault(HINDI);
-
- mDiffScriptWriter.writeChunk(0, 12345);
- mDiffScriptWriter.flush();
-
- assertThat(mOutputStream.toString("UTF-8")).isEqualTo("0-12344\n");
- }
-
- @Test
- public void flush_flushesOutputStream() throws IOException {
- ByteArrayOutputStream mockOutputStream = mock(ByteArrayOutputStream.class);
- SingleStreamDiffScriptWriter diffScriptWriter =
- new SingleStreamDiffScriptWriter(mockOutputStream, MAX_CHUNK_SIZE_IN_BYTES);
-
- diffScriptWriter.flush();
-
- verify(mockOutputStream).flush();
- }
-
- private void writeStringAsBytesToWriter(String string, SingleStreamDiffScriptWriter writer)
- throws IOException {
- byte[] bytes = string.getBytes("UTF-8");
- for (int i = 0; i < bytes.length; i++) {
- writer.writeByte(bytes[i]);
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunkerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunkerTest.java
deleted file mode 100644
index 77b7347..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunkerTest.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Random;
-
-import javax.crypto.SecretKey;
-
-/** Tests for {@link ContentDefinedChunker}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ContentDefinedChunkerTest {
- private static final int WINDOW_SIZE_BYTES = 31;
- private static final int MIN_SIZE_BYTES = 40;
- private static final int MAX_SIZE_BYTES = 300;
- private static final String CHUNK_BOUNDARY = "<----------BOUNDARY----------->";
- private static final byte[] CHUNK_BOUNDARY_BYTES = CHUNK_BOUNDARY.getBytes(UTF_8);
- private static final String CHUNK_1 = "This is the first chunk";
- private static final String CHUNK_2 = "And this is the second chunk";
- private static final String CHUNK_3 = "And finally here is the third chunk";
- private static final String SMALL_CHUNK = "12345678";
-
- private FingerprintMixer mFingerprintMixer;
- private RabinFingerprint64 mRabinFingerprint64;
- private ContentDefinedChunker mChunker;
-
- /** Set up a {@link ContentDefinedChunker} and dependencies for use in the tests. */
- @Before
- public void setUp() throws Exception {
- SecretKey secretKey = generateAesKey();
- byte[] salt = new byte[FingerprintMixer.SALT_LENGTH_BYTES];
- Random random = new Random();
- random.nextBytes(salt);
- mFingerprintMixer = new FingerprintMixer(secretKey, salt);
-
- mRabinFingerprint64 = new RabinFingerprint64();
- long chunkBoundaryFingerprint = calculateFingerprint(CHUNK_BOUNDARY_BYTES);
- mChunker =
- new ContentDefinedChunker(
- MIN_SIZE_BYTES,
- MAX_SIZE_BYTES,
- mRabinFingerprint64,
- mFingerprintMixer,
- (fingerprint) -> fingerprint == chunkBoundaryFingerprint);
- }
-
- /**
- * Creating a {@link ContentDefinedChunker} with a minimum chunk size that is smaller than the
- * window size should throw an {@link IllegalArgumentException}.
- */
- @Test
- public void create_withMinChunkSizeSmallerThanWindowSize_throwsIllegalArgumentException() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- new ContentDefinedChunker(
- WINDOW_SIZE_BYTES - 1,
- MAX_SIZE_BYTES,
- mRabinFingerprint64,
- mFingerprintMixer,
- null));
- }
-
- /**
- * Creating a {@link ContentDefinedChunker} with a maximum chunk size that is smaller than the
- * minimum chunk size should throw an {@link IllegalArgumentException}.
- */
- @Test
- public void create_withMaxChunkSizeSmallerThanMinChunkSize_throwsIllegalArgumentException() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- new ContentDefinedChunker(
- MIN_SIZE_BYTES,
- MIN_SIZE_BYTES - 1,
- mRabinFingerprint64,
- mFingerprintMixer,
- null));
- }
-
- /**
- * {@link ContentDefinedChunker#chunkify(InputStream, Chunker.ChunkConsumer)} should split the
- * input stream across chunk boundaries by default.
- */
- @Test
- public void chunkify_withLargeChunks_splitsIntoChunksAcrossBoundaries() throws Exception {
- byte[] input =
- (CHUNK_1 + CHUNK_BOUNDARY + CHUNK_2 + CHUNK_BOUNDARY + CHUNK_3).getBytes(UTF_8);
- ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
- ArrayList<String> result = new ArrayList<>();
-
- mChunker.chunkify(inputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
-
- assertThat(result)
- .containsExactly(CHUNK_1 + CHUNK_BOUNDARY, CHUNK_2 + CHUNK_BOUNDARY, CHUNK_3)
- .inOrder();
- }
-
- /** Chunks should be combined across boundaries until they reach the minimum chunk size. */
- @Test
- public void chunkify_withSmallChunks_combinesChunksUntilMinSize() throws Exception {
- byte[] input =
- (SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2 + CHUNK_BOUNDARY + CHUNK_3).getBytes(UTF_8);
- ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
- ArrayList<String> result = new ArrayList<>();
-
- mChunker.chunkify(inputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
-
- assertThat(result)
- .containsExactly(SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2 + CHUNK_BOUNDARY, CHUNK_3)
- .inOrder();
- assertThat(result.get(0).length()).isAtLeast(MIN_SIZE_BYTES);
- }
-
- /** Chunks can not be larger than the maximum chunk size. */
- @Test
- public void chunkify_doesNotProduceChunksLargerThanMaxSize() throws Exception {
- byte[] largeInput = new byte[MAX_SIZE_BYTES * 10];
- Arrays.fill(largeInput, "a".getBytes(UTF_8)[0]);
- ByteArrayInputStream inputStream = new ByteArrayInputStream(largeInput);
- ArrayList<String> result = new ArrayList<>();
-
- mChunker.chunkify(inputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
-
- byte[] expectedChunkBytes = new byte[MAX_SIZE_BYTES];
- Arrays.fill(expectedChunkBytes, "a".getBytes(UTF_8)[0]);
- String expectedChunk = new String(expectedChunkBytes, UTF_8);
- assertThat(result)
- .containsExactly(
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk,
- expectedChunk)
- .inOrder();
- }
-
- /**
- * If the input stream signals zero availablility, {@link
- * ContentDefinedChunker#chunkify(InputStream, Chunker.ChunkConsumer)} should still work.
- */
- @Test
- public void chunkify_withInputStreamReturningZeroAvailability_returnsChunks() throws Exception {
- byte[] input = (SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2).getBytes(UTF_8);
- ZeroAvailabilityInputStream zeroAvailabilityInputStream =
- new ZeroAvailabilityInputStream(input);
- ArrayList<String> result = new ArrayList<>();
-
- mChunker.chunkify(
- zeroAvailabilityInputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
-
- assertThat(result).containsExactly(SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2).inOrder();
- }
-
- /**
- * {@link ContentDefinedChunker#chunkify(InputStream, Chunker.ChunkConsumer)} should rethrow any
- * exception thrown by its consumer.
- */
- @Test
- public void chunkify_whenConsumerThrowsException_rethrowsException() throws Exception {
- ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[] {1});
-
- assertThrows(
- GeneralSecurityException.class,
- () ->
- mChunker.chunkify(
- inputStream,
- (chunk) -> {
- throw new GeneralSecurityException();
- }));
- }
-
- private long calculateFingerprint(byte[] bytes) {
- long fingerprint = 0;
- for (byte inByte : bytes) {
- fingerprint =
- mRabinFingerprint64.computeFingerprint64(
- /*inChar=*/ inByte, /*outChar=*/ (byte) 0, fingerprint);
- }
- return mFingerprintMixer.mix(fingerprint);
- }
-
- private static class ZeroAvailabilityInputStream extends ByteArrayInputStream {
- ZeroAvailabilityInputStream(byte[] wrapped) {
- super(wrapped);
- }
-
- @Override
- public synchronized int available() {
- return 0;
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/FingerprintMixerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/FingerprintMixerTest.java
deleted file mode 100644
index 936b5dc..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/FingerprintMixerTest.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.util.HashSet;
-import java.util.Random;
-
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Tests for {@link FingerprintMixer}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class FingerprintMixerTest {
- private static final String KEY_ALGORITHM = "AES";
- private static final int SEED = 42;
- private static final int SALT_LENGTH_BYTES = 256 / 8;
- private static final int KEY_SIZE_BITS = 256;
-
- private Random mSeededRandom;
- private FingerprintMixer mFingerprintMixer;
-
- /** Set up a {@link FingerprintMixer} with deterministic key and salt generation. */
- @Before
- public void setUp() throws Exception {
- // Seed so that the tests are deterministic.
- mSeededRandom = new Random(SEED);
- mFingerprintMixer = new FingerprintMixer(randomKey(), randomSalt());
- }
-
- /**
- * Construcing a {@link FingerprintMixer} with a salt that is too small should throw an {@link
- * IllegalArgumentException}.
- */
- @Test
- public void create_withIncorrectSaltSize_throwsIllegalArgumentException() {
- byte[] tooSmallSalt = new byte[SALT_LENGTH_BYTES - 1];
-
- assertThrows(
- IllegalArgumentException.class,
- () -> new FingerprintMixer(randomKey(), tooSmallSalt));
- }
-
- /**
- * Constructing a {@link FingerprintMixer} with a secret key that can't be encoded should throw
- * an {@link InvalidKeyException}.
- */
- @Test
- public void create_withUnencodableSecretKey_throwsInvalidKeyException() {
- byte[] keyBytes = new byte[KEY_SIZE_BITS / 8];
- UnencodableSecretKeySpec keySpec =
- new UnencodableSecretKeySpec(keyBytes, 0, keyBytes.length, KEY_ALGORITHM);
-
- assertThrows(InvalidKeyException.class, () -> new FingerprintMixer(keySpec, randomSalt()));
- }
-
- /**
- * {@link FingerprintMixer#getAddend()} should not return the same addend for two different
- * keys.
- */
- @Test
- public void getAddend_withDifferentKey_returnsDifferentResult() throws Exception {
- int iterations = 100_000;
- HashSet<Long> returnedAddends = new HashSet<>();
- byte[] salt = randomSalt();
-
- for (int i = 0; i < iterations; i++) {
- FingerprintMixer fingerprintMixer = new FingerprintMixer(randomKey(), salt);
- long addend = fingerprintMixer.getAddend();
- returnedAddends.add(addend);
- }
-
- assertThat(returnedAddends).containsNoDuplicates();
- }
-
- /**
- * {@link FingerprintMixer#getMultiplicand()} should not return the same multiplicand for two
- * different keys.
- */
- @Test
- public void getMultiplicand_withDifferentKey_returnsDifferentResult() throws Exception {
- int iterations = 100_000;
- HashSet<Long> returnedMultiplicands = new HashSet<>();
- byte[] salt = randomSalt();
-
- for (int i = 0; i < iterations; i++) {
- FingerprintMixer fingerprintMixer = new FingerprintMixer(randomKey(), salt);
- long multiplicand = fingerprintMixer.getMultiplicand();
- returnedMultiplicands.add(multiplicand);
- }
-
- assertThat(returnedMultiplicands).containsNoDuplicates();
- }
-
- /** The multiplicant returned by {@link FingerprintMixer} should always be odd. */
- @Test
- public void getMultiplicand_isOdd() throws Exception {
- int iterations = 100_000;
-
- for (int i = 0; i < iterations; i++) {
- FingerprintMixer fingerprintMixer = new FingerprintMixer(randomKey(), randomSalt());
-
- long multiplicand = fingerprintMixer.getMultiplicand();
-
- assertThat(isOdd(multiplicand)).isTrue();
- }
- }
-
- /** {@link FingerprintMixer#mix(long)} should have a random distribution. */
- @Test
- public void mix_randomlyDistributesBits() throws Exception {
- int iterations = 100_000;
- float tolerance = 0.1f;
- int[] totals = new int[64];
-
- for (int i = 0; i < iterations; i++) {
- long n = mFingerprintMixer.mix(mSeededRandom.nextLong());
- for (int j = 0; j < 64; j++) {
- int bit = (int) (n >> j & 1);
- totals[j] += bit;
- }
- }
-
- for (int i = 0; i < 64; i++) {
- float mean = ((float) totals[i]) / iterations;
- float diff = Math.abs(mean - 0.5f);
- assertThat(diff).isLessThan(tolerance);
- }
- }
-
- /**
- * {@link FingerprintMixer#mix(long)} should always produce a number that's different from the
- * input.
- */
- @Test
- public void mix_doesNotProduceSameNumberAsInput() {
- int iterations = 100_000;
-
- for (int i = 0; i < iterations; i++) {
- assertThat(mFingerprintMixer.mix(i)).isNotEqualTo(i);
- }
- }
-
- private byte[] randomSalt() {
- byte[] salt = new byte[SALT_LENGTH_BYTES];
- mSeededRandom.nextBytes(salt);
- return salt;
- }
-
- /**
- * Not a secure way of generating keys. We want to deterministically generate the same keys for
- * each test run, though, to ensure the test is deterministic.
- */
- private SecretKey randomKey() {
- byte[] keyBytes = new byte[KEY_SIZE_BITS / 8];
- mSeededRandom.nextBytes(keyBytes);
- return new SecretKeySpec(keyBytes, 0, keyBytes.length, KEY_ALGORITHM);
- }
-
- private static boolean isOdd(long n) {
- return Math.abs(n % 2) == 1;
- }
-
- /**
- * Subclass of {@link SecretKeySpec} that does not provide an encoded version. As per its
- * contract in {@link Key}, that means {@code getEncoded()} always returns null.
- */
- private class UnencodableSecretKeySpec extends SecretKeySpec {
- UnencodableSecretKeySpec(byte[] key, int offset, int len, String algorithm) {
- super(key, offset, len, algorithm);
- }
-
- @Override
- public byte[] getEncoded() {
- return null;
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/HkdfTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/HkdfTest.java
deleted file mode 100644
index 5494374..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/HkdfTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-/** Tests for {@link Hkdf}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class HkdfTest {
- /** HKDF Test Case 1 IKM from RFC 5869 */
- private static final byte[] HKDF_CASE1_IKM = {
- 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
- 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
- 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
- 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
- 0x0b, 0x0b
- };
-
- /** HKDF Test Case 1 salt from RFC 5869 */
- private static final byte[] HKDF_CASE1_SALT = {
- 0x00, 0x01, 0x02, 0x03, 0x04,
- 0x05, 0x06, 0x07, 0x08, 0x09,
- 0x0a, 0x0b, 0x0c
- };
-
- /** HKDF Test Case 1 info from RFC 5869 */
- private static final byte[] HKDF_CASE1_INFO = {
- (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, (byte) 0xf4,
- (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, (byte) 0xf8, (byte) 0xf9
- };
-
- /** First 32 bytes of HKDF Test Case 1 OKM (output) from RFC 5869 */
- private static final byte[] HKDF_CASE1_OKM = {
- (byte) 0x3c, (byte) 0xb2, (byte) 0x5f, (byte) 0x25, (byte) 0xfa,
- (byte) 0xac, (byte) 0xd5, (byte) 0x7a, (byte) 0x90, (byte) 0x43,
- (byte) 0x4f, (byte) 0x64, (byte) 0xd0, (byte) 0x36, (byte) 0x2f,
- (byte) 0x2a, (byte) 0x2d, (byte) 0x2d, (byte) 0x0a, (byte) 0x90,
- (byte) 0xcf, (byte) 0x1a, (byte) 0x5a, (byte) 0x4c, (byte) 0x5d,
- (byte) 0xb0, (byte) 0x2d, (byte) 0x56, (byte) 0xec, (byte) 0xc4,
- (byte) 0xc5, (byte) 0xbf
- };
-
- /** Test the example from RFC 5869. */
- @Test
- public void hkdf_derivesKeyMaterial() throws Exception {
- byte[] result = Hkdf.hkdf(HKDF_CASE1_IKM, HKDF_CASE1_SALT, HKDF_CASE1_INFO);
-
- assertThat(result).isEqualTo(HKDF_CASE1_OKM);
- }
-
- /** Providing a key that is null should throw a {@link java.lang.NullPointerException}. */
- @Test
- public void hkdf_withNullKey_throwsNullPointerException() throws Exception {
- assertThrows(
- NullPointerException.class,
- () -> Hkdf.hkdf(null, HKDF_CASE1_SALT, HKDF_CASE1_INFO));
- }
-
- /** Providing a salt that is null should throw a {@link java.lang.NullPointerException}. */
- @Test
- public void hkdf_withNullSalt_throwsNullPointerException() throws Exception {
- assertThrows(
- NullPointerException.class, () -> Hkdf.hkdf(HKDF_CASE1_IKM, null, HKDF_CASE1_INFO));
- }
-
- /** Providing data that is null should throw a {@link java.lang.NullPointerException}. */
- @Test
- public void hkdf_withNullData_throwsNullPointerException() throws Exception {
- assertThrows(
- NullPointerException.class, () -> Hkdf.hkdf(HKDF_CASE1_IKM, HKDF_CASE1_SALT, null));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpointTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpointTest.java
deleted file mode 100644
index 277dc37..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpointTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Random;
-
-/** Tests for {@link IsChunkBreakpoint}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class IsChunkBreakpointTest {
- private static final int RANDOM_SEED = 42;
- private static final double TOLERANCE = 0.01;
- private static final int NUMBER_OF_TESTS = 10000;
- private static final int BITS_PER_LONG = 64;
-
- private Random mRandom;
-
- /** Make sure that tests are deterministic. */
- @Before
- public void setUp() {
- mRandom = new Random(RANDOM_SEED);
- }
-
- /**
- * Providing a negative average number of trials should throw an {@link
- * IllegalArgumentException}.
- */
- @Test
- public void create_withNegativeAverageNumberOfTrials_throwsIllegalArgumentException() {
- assertThrows(IllegalArgumentException.class, () -> new IsChunkBreakpoint(-1));
- }
-
- // Note: the following three tests are compute-intensive, so be cautious adding more.
-
- /**
- * If the provided average number of trials is zero, a breakpoint should be expected after one
- * trial on average.
- */
- @Test
- public void
- isBreakpoint_withZeroAverageNumberOfTrials_isTrueOnAverageAfterOneTrial() {
- assertExpectedTrials(new IsChunkBreakpoint(0), /*expectedTrials=*/ 1);
- }
-
- /**
- * If the provided average number of trials is 512, a breakpoint should be expected after 512
- * trials on average.
- */
- @Test
- public void
- isBreakpoint_with512AverageNumberOfTrials_isTrueOnAverageAfter512Trials() {
- assertExpectedTrials(new IsChunkBreakpoint(512), /*expectedTrials=*/ 512);
- }
-
- /**
- * If the provided average number of trials is 1024, a breakpoint should be expected after 1024
- * trials on average.
- */
- @Test
- public void
- isBreakpoint_with1024AverageNumberOfTrials_isTrueOnAverageAfter1024Trials() {
- assertExpectedTrials(new IsChunkBreakpoint(1024), /*expectedTrials=*/ 1024);
- }
-
- /** The number of leading zeros should be the logarithm of the average number of trials. */
- @Test
- public void getLeadingZeros_squaredIsAverageNumberOfTrials() {
- for (int i = 0; i < BITS_PER_LONG; i++) {
- long averageNumberOfTrials = (long) Math.pow(2, i);
-
- int leadingZeros = new IsChunkBreakpoint(averageNumberOfTrials).getLeadingZeros();
-
- assertThat(leadingZeros).isEqualTo(i);
- }
- }
-
- private void assertExpectedTrials(IsChunkBreakpoint isChunkBreakpoint, long expectedTrials) {
- long sum = 0;
- for (int i = 0; i < NUMBER_OF_TESTS; i++) {
- sum += numberOfTrialsTillBreakpoint(isChunkBreakpoint);
- }
- long averageTrials = sum / NUMBER_OF_TESTS;
- assertThat((double) Math.abs(averageTrials - expectedTrials))
- .isLessThan(TOLERANCE * expectedTrials);
- }
-
- private int numberOfTrialsTillBreakpoint(IsChunkBreakpoint isChunkBreakpoint) {
- int trials = 0;
-
- while (true) {
- trials++;
- if (isChunkBreakpoint.isBreakpoint(mRandom.nextLong())) {
- return trials;
- }
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64Test.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64Test.java
deleted file mode 100644
index 729580c..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64Test.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.chunking.cdc;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-/** Tests for {@link RabinFingerprint64}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class RabinFingerprint64Test {
- private static final int WINDOW_SIZE = 31;
- private static final ImmutableList<String> TEST_STRINGS =
- ImmutableList.of(
- "ervHTtChYXO6eXivYqThlyyzqkbRaOR",
- "IxaVunH9ZC3qneWfhj1GkBH4ys9CYqz",
- "wZRVjlE1p976icCFPX9pibk4PEBvjSH",
- "pHIVaT8x8If9D6s9croksgNmJpmGYWI");
-
- private final RabinFingerprint64 mRabinFingerprint64 = new RabinFingerprint64();
-
- /**
- * No matter where in the input buffer a string occurs, {@link
- * RabinFingerprint64#computeFingerprint64(byte, byte, long)} should return the same
- * fingerprint.
- */
- @Test
- public void computeFingerprint64_forSameWindow_returnsSameFingerprint() {
- long fingerprint1 =
- computeFingerprintAtPosition(getBytes(TEST_STRINGS.get(0)), WINDOW_SIZE - 1);
- long fingerprint2 =
- computeFingerprintAtPosition(
- getBytes(TEST_STRINGS.get(1), TEST_STRINGS.get(0)), WINDOW_SIZE * 2 - 1);
- long fingerprint3 =
- computeFingerprintAtPosition(
- getBytes(TEST_STRINGS.get(2), TEST_STRINGS.get(3), TEST_STRINGS.get(0)),
- WINDOW_SIZE * 3 - 1);
- String stub = "abc";
- long fingerprint4 =
- computeFingerprintAtPosition(
- getBytes(stub, TEST_STRINGS.get(0)), WINDOW_SIZE + stub.length() - 1);
-
- // Assert that all fingerprints are exactly the same
- assertThat(ImmutableSet.of(fingerprint1, fingerprint2, fingerprint3, fingerprint4))
- .hasSize(1);
- }
-
- /** The computed fingerprint should be different for different inputs. */
- @Test
- public void computeFingerprint64_withDifferentInput_returnsDifferentFingerprint() {
- long fingerprint1 = computeFingerprintOf(TEST_STRINGS.get(0));
- long fingerprint2 = computeFingerprintOf(TEST_STRINGS.get(1));
- long fingerprint3 = computeFingerprintOf(TEST_STRINGS.get(2));
- long fingerprint4 = computeFingerprintOf(TEST_STRINGS.get(3));
-
- assertThat(ImmutableList.of(fingerprint1, fingerprint2, fingerprint3, fingerprint4))
- .containsNoDuplicates();
- }
-
- /**
- * An input with the same characters in a different order should return a different fingerprint.
- */
- @Test
- public void computeFingerprint64_withSameInputInDifferentOrder_returnsDifferentFingerprint() {
- long fingerprint1 = computeFingerprintOf("abcdefghijklmnopqrstuvwxyz12345");
- long fingerprint2 = computeFingerprintOf("54321zyxwvutsrqponmlkjihgfedcba");
- long fingerprint3 = computeFingerprintOf("4bcdefghijklmnopqrstuvwxyz123a5");
- long fingerprint4 = computeFingerprintOf("bacdefghijklmnopqrstuvwxyz12345");
-
- assertThat(ImmutableList.of(fingerprint1, fingerprint2, fingerprint3, fingerprint4))
- .containsNoDuplicates();
- }
-
- /** UTF-8 bytes of all the given strings in order. */
- private byte[] getBytes(String... strings) {
- StringBuilder sb = new StringBuilder();
- for (String s : strings) {
- sb.append(s);
- }
- return sb.toString().getBytes(UTF_8);
- }
-
- /**
- * The Rabin fingerprint of a window of bytes ending at {@code position} in the {@code bytes}
- * array.
- */
- private long computeFingerprintAtPosition(byte[] bytes, int position) {
- assertThat(position).isAtMost(bytes.length - 1);
- long fingerprint = 0;
- for (int i = 0; i <= position; i++) {
- byte outChar;
- if (i >= WINDOW_SIZE) {
- outChar = bytes[i - WINDOW_SIZE];
- } else {
- outChar = (byte) 0;
- }
- fingerprint =
- mRabinFingerprint64.computeFingerprint64(
- /*inChar=*/ bytes[i], outChar, fingerprint);
- }
- return fingerprint;
- }
-
- private long computeFingerprintOf(String s) {
- assertThat(s.length()).isEqualTo(WINDOW_SIZE);
- return computeFingerprintAtPosition(s.getBytes(UTF_8), WINDOW_SIZE - 1);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java
deleted file mode 100644
index b607404..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * 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.keys;
-
-import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.security.InvalidKeyException;
-
-import javax.crypto.SecretKey;
-
-/** Key wrapping tests */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class KeyWrapUtilsTest {
- private static final int KEY_SIZE_BITS = 256;
- private static final int BITS_PER_BYTE = 8;
- private static final int GCM_NONCE_LENGTH_BYTES = 16;
- private static final int GCM_TAG_LENGTH_BYTES = 16;
-
- /** Test a wrapped key has metadata */
- @Test
- public void wrap_addsMetadata() throws Exception {
- WrappedKeyProto.WrappedKey wrappedKey =
- KeyWrapUtils.wrap(
- /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
- assertThat(wrappedKey.metadata).isNotNull();
- assertThat(wrappedKey.metadata.type).isEqualTo(WrappedKeyProto.KeyMetadata.AES_256_GCM);
- }
-
- /** Test a wrapped key has an algorithm specified */
- @Test
- public void wrap_addsWrapAlgorithm() throws Exception {
- WrappedKeyProto.WrappedKey wrappedKey =
- KeyWrapUtils.wrap(
- /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
- assertThat(wrappedKey.wrapAlgorithm).isEqualTo(WrappedKeyProto.WrappedKey.AES_256_GCM);
- }
-
- /** Test a wrapped key haas an nonce of the right length */
- @Test
- public void wrap_addsNonceOfAppropriateLength() throws Exception {
- WrappedKeyProto.WrappedKey wrappedKey =
- KeyWrapUtils.wrap(
- /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
- assertThat(wrappedKey.nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
- }
-
- /** Test a wrapped key has a key of the right length */
- @Test
- public void wrap_addsTagOfAppropriateLength() throws Exception {
- WrappedKeyProto.WrappedKey wrappedKey =
- KeyWrapUtils.wrap(
- /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
- assertThat(wrappedKey.key).hasLength(KEY_SIZE_BITS / BITS_PER_BYTE + GCM_TAG_LENGTH_BYTES);
- }
-
- /** Ensure a key can be wrapped and unwrapped again */
- @Test
- public void unwrap_unwrapsEncryptedKey() throws Exception {
- SecretKey secondaryKey = generateAesKey();
- SecretKey tertiaryKey = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, tertiaryKey);
- SecretKey unwrappedKey = KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
- assertThat(unwrappedKey).isEqualTo(tertiaryKey);
- }
-
- /** Ensure the unwrap method rejects keys with bad algorithms */
- @Test(expected = InvalidKeyException.class)
- public void unwrap_throwsForBadWrapAlgorithm() throws Exception {
- SecretKey secondaryKey = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
- wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.UNKNOWN;
-
- KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
- }
-
- /** Ensure the unwrap method rejects metadata indicating the encryption type is unknown */
- @Test(expected = InvalidKeyException.class)
- public void unwrap_throwsForBadKeyAlgorithm() throws Exception {
- SecretKey secondaryKey = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
- wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.UNKNOWN;
-
- KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
- }
-
- /** Ensure the unwrap method rejects wrapped keys missing the metadata */
- @Test(expected = InvalidKeyException.class)
- public void unwrap_throwsForMissingMetadata() throws Exception {
- SecretKey secondaryKey = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
- wrappedKey.metadata = null;
-
- KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
- }
-
- /** Ensure unwrap rejects invalid secondary keys */
- @Test(expected = InvalidKeyException.class)
- public void unwrap_throwsForBadSecondaryKey() throws Exception {
- WrappedKeyProto.WrappedKey wrappedKey =
- KeyWrapUtils.wrap(
- /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
-
- KeyWrapUtils.unwrap(generateAesKey(), wrappedKey);
- }
-
- /** Ensure rewrap can rewrap keys */
- @Test
- public void rewrap_canBeUnwrappedWithNewSecondaryKey() throws Exception {
- SecretKey tertiaryKey = generateAesKey();
- SecretKey oldSecondaryKey = generateAesKey();
- SecretKey newSecondaryKey = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
-
- WrappedKeyProto.WrappedKey wrappedWithNew =
- KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
-
- assertThat(KeyWrapUtils.unwrap(newSecondaryKey, wrappedWithNew)).isEqualTo(tertiaryKey);
- }
-
- /** Ensure rewrap doesn't create something decryptable by an old key */
- @Test(expected = InvalidKeyException.class)
- public void rewrap_cannotBeUnwrappedWithOldSecondaryKey() throws Exception {
- SecretKey tertiaryKey = generateAesKey();
- SecretKey oldSecondaryKey = generateAesKey();
- SecretKey newSecondaryKey = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
-
- WrappedKeyProto.WrappedKey wrappedWithNew =
- KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
-
- KeyWrapUtils.unwrap(oldSecondaryKey, wrappedWithNew);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManagerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManagerTest.java
deleted file mode 100644
index 5342efa..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManagerTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.RecoveryController;
-
-import com.android.server.testing.shadows.ShadowInternalRecoveryServiceException;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import java.security.SecureRandom;
-import java.util.Optional;
-
-/** Tests for {@link RecoverableKeyStoreSecondaryKeyManager}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-@Config(shadows = {ShadowRecoveryController.class, ShadowInternalRecoveryServiceException.class})
-public class RecoverableKeyStoreSecondaryKeyManagerTest {
- private static final String BACKUP_KEY_ALIAS_PREFIX =
- "com.android.server.backup/recoverablekeystore/";
- private static final int BITS_PER_BYTE = 8;
- private static final int BACKUP_KEY_SUFFIX_LENGTH_BYTES = 128 / BITS_PER_BYTE;
- private static final int HEX_PER_BYTE = 2;
- private static final int BACKUP_KEY_ALIAS_LENGTH =
- BACKUP_KEY_ALIAS_PREFIX.length() + BACKUP_KEY_SUFFIX_LENGTH_BYTES * HEX_PER_BYTE;
- private static final String NONEXISTENT_KEY_ALIAS = "NONEXISTENT_KEY_ALIAS";
-
- private RecoverableKeyStoreSecondaryKeyManager mRecoverableKeyStoreSecondaryKeyManager;
- private Context mContext;
-
- /** Create a new {@link RecoverableKeyStoreSecondaryKeyManager} to use in tests. */
- @Before
- public void setUp() throws Exception {
- mContext = RuntimeEnvironment.application;
-
- mRecoverableKeyStoreSecondaryKeyManager =
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(mContext), new SecureRandom());
- }
-
- /** Reset the {@link ShadowRecoveryController}. */
- @After
- public void tearDown() throws Exception {
- ShadowRecoveryController.reset();
- }
-
- /** The generated key should always have the prefix {@code BACKUP_KEY_ALIAS_PREFIX}. */
- @Test
- public void generate_generatesKeyWithExpectedPrefix() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
-
- assertThat(key.getAlias()).startsWith(BACKUP_KEY_ALIAS_PREFIX);
- }
-
- /** The generated key should always have length {@code BACKUP_KEY_ALIAS_LENGTH}. */
- @Test
- public void generate_generatesKeyWithExpectedLength() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
-
- assertThat(key.getAlias()).hasLength(BACKUP_KEY_ALIAS_LENGTH);
- }
-
- /** Ensure that hidden API exceptions are rethrown when generating keys. */
- @Test
- public void generate_encounteringHiddenApiException_rethrowsException() {
- ShadowRecoveryController.setThrowsInternalError(true);
-
- assertThrows(
- InternalRecoveryServiceException.class,
- mRecoverableKeyStoreSecondaryKeyManager::generate);
- }
-
- /** Ensure that retrieved keys correspond to those generated earlier. */
- @Test
- public void get_getsKeyGeneratedByController() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
-
- Optional<RecoverableKeyStoreSecondaryKey> retrievedKey =
- mRecoverableKeyStoreSecondaryKeyManager.get(key.getAlias());
-
- assertThat(retrievedKey.isPresent()).isTrue();
- assertThat(retrievedKey.get().getAlias()).isEqualTo(key.getAlias());
- assertThat(retrievedKey.get().getSecretKey()).isEqualTo(key.getSecretKey());
- }
-
- /**
- * Ensure that a call to {@link RecoverableKeyStoreSecondaryKeyManager#get(java.lang.String)}
- * for nonexistent aliases returns an emtpy {@link Optional}.
- */
- @Test
- public void get_forNonExistentKey_returnsEmptyOptional() throws Exception {
- Optional<RecoverableKeyStoreSecondaryKey> retrievedKey =
- mRecoverableKeyStoreSecondaryKeyManager.get(NONEXISTENT_KEY_ALIAS);
-
- assertThat(retrievedKey.isPresent()).isFalse();
- }
-
- /**
- * Ensure that exceptions occurring during {@link
- * RecoverableKeyStoreSecondaryKeyManager#get(java.lang.String)} are not rethrown.
- */
- @Test
- public void get_encounteringInternalException_doesNotPropagateException() throws Exception {
- ShadowRecoveryController.setThrowsInternalError(true);
-
- // Should not throw exception
- mRecoverableKeyStoreSecondaryKeyManager.get(NONEXISTENT_KEY_ALIAS);
- }
-
- /** Ensure that keys are correctly removed from the store. */
- @Test
- public void remove_removesKeyFromRecoverableStore() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
-
- mRecoverableKeyStoreSecondaryKeyManager.remove(key.getAlias());
-
- assertThat(RecoveryController.getInstance(mContext).getAliases())
- .doesNotContain(key.getAlias());
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyTest.java
deleted file mode 100644
index 89977f8..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-import android.security.keystore.recovery.RecoveryController;
-
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey.Status;
-import com.android.server.backup.testing.CryptoTestUtils;
-import com.android.server.testing.shadows.ShadowInternalRecoveryServiceException;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import javax.crypto.SecretKey;
-
-/** Tests for {@link RecoverableKeyStoreSecondaryKey}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-@Config(shadows = {ShadowRecoveryController.class, ShadowInternalRecoveryServiceException.class})
-public class RecoverableKeyStoreSecondaryKeyTest {
- private static final String TEST_ALIAS = "test";
- private static final int NONEXISTENT_STATUS_CODE = 42;
-
- private RecoverableKeyStoreSecondaryKey mSecondaryKey;
- private SecretKey mGeneratedSecretKey;
- private Context mContext;
-
- /** Instantiate a {@link RecoverableKeyStoreSecondaryKey} to use in tests. */
- @Before
- public void setUp() throws Exception {
- mContext = RuntimeEnvironment.application;
- mGeneratedSecretKey = CryptoTestUtils.generateAesKey();
- mSecondaryKey = new RecoverableKeyStoreSecondaryKey(TEST_ALIAS, mGeneratedSecretKey);
- }
-
- /** Reset the {@link ShadowRecoveryController}. */
- @After
- public void tearDown() throws Exception {
- ShadowRecoveryController.reset();
- }
-
- /**
- * Checks that {@link RecoverableKeyStoreSecondaryKey#getAlias()} returns the value supplied in
- * the constructor.
- */
- @Test
- public void getAlias() {
- String alias = mSecondaryKey.getAlias();
-
- assertThat(alias).isEqualTo(TEST_ALIAS);
- }
-
- /**
- * Checks that {@link RecoverableKeyStoreSecondaryKey#getSecretKey()} returns the value supplied
- * in the constructor.
- */
- @Test
- public void getSecretKey() {
- SecretKey secretKey = mSecondaryKey.getSecretKey();
-
- assertThat(secretKey).isEqualTo(mGeneratedSecretKey);
- }
-
- /**
- * Checks that passing a secret key that is null to the constructor throws an exception.
- */
- @Test
- public void constructor_withNullSecretKey_throwsNullPointerException() {
- assertThrows(
- NullPointerException.class,
- () -> new RecoverableKeyStoreSecondaryKey(TEST_ALIAS, null));
- }
-
- /**
- * Checks that passing an alias that is null to the constructor throws an exception.
- */
- @Test
- public void constructor_withNullAlias_throwsNullPointerException() {
- assertThrows(
- NullPointerException.class,
- () -> new RecoverableKeyStoreSecondaryKey(null, mGeneratedSecretKey));
- }
-
- /** Checks that the synced status is returned correctly. */
- @Test
- public void getStatus_whenSynced_returnsSynced() throws Exception {
- setStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
-
- int status = mSecondaryKey.getStatus(mContext);
-
- assertThat(status).isEqualTo(Status.SYNCED);
- }
-
- /** Checks that the in progress sync status is returned correctly. */
- @Test
- public void getStatus_whenNotSynced_returnsNotSynced() throws Exception {
- setStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
-
- int status = mSecondaryKey.getStatus(mContext);
-
- assertThat(status).isEqualTo(Status.NOT_SYNCED);
- }
-
- /** Checks that the failure status is returned correctly. */
- @Test
- public void getStatus_onPermanentFailure_returnsDestroyed() throws Exception {
- setStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
-
- int status = mSecondaryKey.getStatus(mContext);
-
- assertThat(status).isEqualTo(Status.DESTROYED);
- }
-
- /** Checks that an unknown status results in {@code NOT_SYNCED} being returned. */
- @Test
- public void getStatus_forUnknownStatusCode_returnsNotSynced() throws Exception {
- setStatus(NONEXISTENT_STATUS_CODE);
-
- int status = mSecondaryKey.getStatus(mContext);
-
- assertThat(status).isEqualTo(Status.NOT_SYNCED);
- }
-
- /** Checks that an internal error results in {@code NOT_SYNCED} being returned. */
- @Test
- public void getStatus_onInternalError_returnsNotSynced() throws Exception {
- ShadowRecoveryController.setThrowsInternalError(true);
-
- int status = mSecondaryKey.getStatus(mContext);
-
- assertThat(status).isEqualTo(Status.NOT_SYNCED);
- }
-
- private void setStatus(int status) throws Exception {
- ShadowRecoveryController.setRecoveryStatus(TEST_ALIAS, status);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RestoreKeyFetcherTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RestoreKeyFetcherTest.java
deleted file mode 100644
index 004f809..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/RestoreKeyFetcherTest.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.security.InvalidKeyException;
-import java.security.KeyException;
-import java.security.SecureRandom;
-import java.util.Optional;
-
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Test the restore key fetcher */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class RestoreKeyFetcherTest {
-
- private static final String KEY_GENERATOR_ALGORITHM = "AES";
-
- private static final String TEST_SECONDARY_KEY_ALIAS = "test_2ndary_key";
- private static final byte[] TEST_SECONDARY_KEY_BYTES = new byte[256 / Byte.SIZE];
-
- @Mock private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
-
- /** Initialise the mocks **/
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- }
-
- /** Ensure the unwrap method works as expected */
- @Test
- public void unwrapTertiaryKey_returnsUnwrappedKey() throws Exception {
- RecoverableKeyStoreSecondaryKey secondaryKey = createSecondaryKey();
- SecretKey tertiaryKey = createTertiaryKey();
- WrappedKeyProto.WrappedKey wrappedTertiaryKey =
- KeyWrapUtils.wrap(secondaryKey.getSecretKey(), tertiaryKey);
- when(mSecondaryKeyManager.get(TEST_SECONDARY_KEY_ALIAS))
- .thenReturn(Optional.of(secondaryKey));
-
- SecretKey actualTertiaryKey =
- RestoreKeyFetcher.unwrapTertiaryKey(
- () -> mSecondaryKeyManager,
- TEST_SECONDARY_KEY_ALIAS,
- wrappedTertiaryKey);
-
- assertThat(actualTertiaryKey).isEqualTo(tertiaryKey);
- }
-
- /** Ensure that missing secondary keys are detected and an appropriate exception is thrown */
- @Test
- public void unwrapTertiaryKey_missingSecondaryKey_throwsSpecificException() throws Exception {
- WrappedKeyProto.WrappedKey wrappedTertiaryKey =
- KeyWrapUtils.wrap(createSecondaryKey().getSecretKey(), createTertiaryKey());
- when(mSecondaryKeyManager.get(TEST_SECONDARY_KEY_ALIAS)).thenReturn(Optional.empty());
-
- assertThrows(
- KeyException.class,
- () ->
- RestoreKeyFetcher.unwrapTertiaryKey(
- () -> mSecondaryKeyManager,
- TEST_SECONDARY_KEY_ALIAS,
- wrappedTertiaryKey));
- }
-
- /** Ensure that invalid secondary keys are detected and an appropriate exception is thrown */
- @Test
- public void unwrapTertiaryKey_badSecondaryKey_throws() throws Exception {
- RecoverableKeyStoreSecondaryKey badSecondaryKey =
- new RecoverableKeyStoreSecondaryKey(
- TEST_SECONDARY_KEY_ALIAS,
- new SecretKeySpec(new byte[] {0, 1}, KEY_GENERATOR_ALGORITHM));
-
- WrappedKeyProto.WrappedKey wrappedTertiaryKey =
- KeyWrapUtils.wrap(createSecondaryKey().getSecretKey(), createTertiaryKey());
- when(mSecondaryKeyManager.get(TEST_SECONDARY_KEY_ALIAS))
- .thenReturn(Optional.of(badSecondaryKey));
-
- assertThrows(
- InvalidKeyException.class,
- () ->
- RestoreKeyFetcher.unwrapTertiaryKey(
- () -> mSecondaryKeyManager,
- TEST_SECONDARY_KEY_ALIAS,
- wrappedTertiaryKey));
- }
-
- private static RecoverableKeyStoreSecondaryKey createSecondaryKey() {
- return new RecoverableKeyStoreSecondaryKey(
- TEST_SECONDARY_KEY_ALIAS,
- new SecretKeySpec(TEST_SECONDARY_KEY_BYTES, KEY_GENERATOR_ALGORITHM));
- }
-
- private static SecretKey createTertiaryKey() {
- return new TertiaryKeyGenerator(new SecureRandom(new byte[] {0})).generate();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java
deleted file mode 100644
index c31d19d..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.Resetter;
-
-import java.io.File;
-import java.time.Clock;
-
-@Config(shadows = SecondaryKeyRotationSchedulerTest.ShadowStartSecondaryKeyRotationTask.class)
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class SecondaryKeyRotationSchedulerTest {
- private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";
-
- @Mock private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
- @Mock private Clock mClock;
-
- private CryptoSettings mCryptoSettings;
- private SecondaryKeyRotationScheduler mScheduler;
- private long mRotationIntervalMillis;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- Context application = ApplicationProvider.getApplicationContext();
-
- mCryptoSettings = CryptoSettings.getInstanceForTesting(application);
- mRotationIntervalMillis = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();
-
- mScheduler =
- new SecondaryKeyRotationScheduler(
- application, mSecondaryKeyManager, mCryptoSettings, mClock);
- ShadowStartSecondaryKeyRotationTask.reset();
- }
-
- @Test
- public void startRotationIfScheduled_rotatesIfRotationWasFarEnoughInThePast() {
- long lastRotated = 100009;
- mCryptoSettings.setSecondaryLastRotated(lastRotated);
- setNow(lastRotated + mRotationIntervalMillis);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
- }
-
- @Test
- public void startRotationIfScheduled_setsNewRotationTimeIfRotationWasFarEnoughInThePast() {
- long lastRotated = 100009;
- long now = lastRotated + mRotationIntervalMillis;
- mCryptoSettings.setSecondaryLastRotated(lastRotated);
- setNow(now);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
- }
-
- @Test
- public void startRotationIfScheduled_rotatesIfClockHasChanged() {
- long lastRotated = 100009;
- mCryptoSettings.setSecondaryLastRotated(lastRotated);
- setNow(lastRotated - 1);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
- }
-
- @Test
- public void startRotationIfScheduled_rotatesIfSentinelFileIsPresent() throws Exception {
- File file = new File(RuntimeEnvironment.application.getFilesDir(), SENTINEL_FILE_PATH);
- file.createNewFile();
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
- }
-
- @Test
- public void startRotationIfScheduled_setsNextRotationIfClockHasChanged() {
- long lastRotated = 100009;
- long now = lastRotated - 1;
- mCryptoSettings.setSecondaryLastRotated(lastRotated);
- setNow(now);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
- }
-
- @Test
- public void startRotationIfScheduled_doesNothingIfRotationWasRecentEnough() {
- long lastRotated = 100009;
- mCryptoSettings.setSecondaryLastRotated(lastRotated);
- setNow(lastRotated + mRotationIntervalMillis - 1);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isFalse();
- }
-
- @Test
- public void startRotationIfScheduled_doesNotSetRotationTimeIfRotationWasRecentEnough() {
- long lastRotated = 100009;
- mCryptoSettings.setSecondaryLastRotated(lastRotated);
- setNow(lastRotated + mRotationIntervalMillis - 1);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(lastRotated);
- }
-
- @Test
- public void startRotationIfScheduled_setsLastRotatedToNowIfNeverRotated() {
- long now = 13295436;
- setNow(now);
-
- mScheduler.startRotationIfScheduled();
-
- assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
- }
-
- private void setNow(long timestamp) {
- when(mClock.millis()).thenReturn(timestamp);
- }
-
- @Implements(StartSecondaryKeyRotationTask.class)
- public static class ShadowStartSecondaryKeyRotationTask {
- private static boolean sRan = false;
-
- @Implementation
- public void run() {
- sRan = true;
- }
-
- @Resetter
- public static void reset() {
- sRan = false;
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyGeneratorTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyGeneratorTest.java
deleted file mode 100644
index 48216f8..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyGeneratorTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.security.SecureRandom;
-
-import javax.crypto.SecretKey;
-
-/** Tests for {@link TertiaryKeyGenerator}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class TertiaryKeyGeneratorTest {
- private static final String KEY_ALGORITHM = "AES";
- private static final int KEY_SIZE_BITS = 256;
-
- private TertiaryKeyGenerator mTertiaryKeyGenerator;
-
- /** Instantiate a new {@link TertiaryKeyGenerator} for use in tests. */
- @Before
- public void setUp() {
- mTertiaryKeyGenerator = new TertiaryKeyGenerator(new SecureRandom());
- }
-
- /** Generated keys should be AES keys. */
- @Test
- public void generate_generatesAESKeys() {
- SecretKey secretKey = mTertiaryKeyGenerator.generate();
-
- assertThat(secretKey.getAlgorithm()).isEqualTo(KEY_ALGORITHM);
- }
-
- /** Generated keys should be 256 bits in size. */
- @Test
- public void generate_generates256BitKeys() {
- SecretKey secretKey = mTertiaryKeyGenerator.generate();
-
- assertThat(secretKey.getEncoded()).hasLength(KEY_SIZE_BITS / 8);
- }
-
- /**
- * Subsequent calls to {@link TertiaryKeyGenerator#generate()} should generate different keys.
- */
- @Test
- public void generate_generatesNewKeys() {
- SecretKey key1 = mTertiaryKeyGenerator.generate();
- SecretKey key2 = mTertiaryKeyGenerator.generate();
-
- assertThat(key1).isNotEqualTo(key2);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java
deleted file mode 100644
index 1ed8309..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * 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.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.robolectric.RuntimeEnvironment.application;
-
-import android.security.keystore.recovery.RecoveryController;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.security.SecureRandom;
-
-import javax.crypto.SecretKey;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = ShadowRecoveryController.class)
-public class TertiaryKeyManagerTest {
-
- private static final String TEST_PACKAGE_1 = "com.example.app1";
- private static final String TEST_PACKAGE_2 = "com.example.app2";
-
- private SecureRandom mSecureRandom;
- private RecoverableKeyStoreSecondaryKey mSecondaryKey;
-
- @Mock private TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mSecureRandom = new SecureRandom();
- mSecondaryKey =
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(application), mSecureRandom)
- .generate();
- ShadowRecoveryController.reset();
- }
-
- private TertiaryKeyManager createNewManager(String packageName) {
- return new TertiaryKeyManager(
- application,
- mSecureRandom,
- mTertiaryKeyRotationScheduler,
- mSecondaryKey,
- packageName);
- }
-
- @Test
- public void getKey_noExistingKey_returnsNewKey() throws Exception {
- assertThat(createNewManager(TEST_PACKAGE_1).getKey()).isNotNull();
- }
-
- @Test
- public void getKey_noExistingKey_recordsIncrementalBackup() throws Exception {
- createNewManager(TEST_PACKAGE_1).getKey();
- verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1);
- }
-
- @Test
- public void getKey_existingKey_returnsExistingKey() throws Exception {
- TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
- SecretKey existingKey = manager.getKey();
-
- assertThat(manager.getKey()).isEqualTo(existingKey);
- }
-
- @Test
- public void getKey_existingKey_recordsBackupButNotRotation() throws Exception {
- createNewManager(TEST_PACKAGE_1).getKey();
- reset(mTertiaryKeyRotationScheduler);
-
- createNewManager(TEST_PACKAGE_1).getKey();
-
- verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1);
- verify(mTertiaryKeyRotationScheduler, never()).recordKeyRotation(any());
- }
-
- @Test
- public void getKey_existingKeyButRotationRequired_returnsNewKey() throws Exception {
- SecretKey firstKey = createNewManager(TEST_PACKAGE_1).getKey();
- when(mTertiaryKeyRotationScheduler.isKeyRotationDue(TEST_PACKAGE_1)).thenReturn(true);
-
- SecretKey secondKey = createNewManager(TEST_PACKAGE_1).getKey();
-
- assertThat(secondKey).isNotEqualTo(firstKey);
- }
-
- @Test
- public void getKey_existingKeyButRotationRequired_recordsKeyRotationAndBackup()
- throws Exception {
- when(mTertiaryKeyRotationScheduler.isKeyRotationDue(TEST_PACKAGE_1)).thenReturn(true);
- createNewManager(TEST_PACKAGE_1).getKey();
-
- InOrder inOrder = inOrder(mTertiaryKeyRotationScheduler);
- inOrder.verify(mTertiaryKeyRotationScheduler).recordKeyRotation(TEST_PACKAGE_1);
- inOrder.verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1);
- }
-
- @Test
- public void getKey_twoApps_returnsDifferentKeys() throws Exception {
- TertiaryKeyManager firstManager = createNewManager(TEST_PACKAGE_1);
- TertiaryKeyManager secondManager = createNewManager(TEST_PACKAGE_2);
- SecretKey firstKey = firstManager.getKey();
-
- assertThat(secondManager.getKey()).isNotEqualTo(firstKey);
- }
-
- @Test
- public void getWrappedKey_noExistingKey_returnsWrappedNewKey() throws Exception {
- TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
- SecretKey unwrappedKey = manager.getKey();
- WrappedKeyProto.WrappedKey wrappedKey = manager.getWrappedKey();
-
- SecretKey expectedUnwrappedKey =
- KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey);
- assertThat(unwrappedKey).isEqualTo(expectedUnwrappedKey);
- }
-
- @Test
- public void getWrappedKey_existingKey_returnsWrappedExistingKey() throws Exception {
- TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
- WrappedKeyProto.WrappedKey wrappedKey = manager.getWrappedKey();
- SecretKey unwrappedKey = manager.getKey();
-
- SecretKey expectedUnwrappedKey =
- KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey);
- assertThat(unwrappedKey).isEqualTo(expectedUnwrappedKey);
- }
-
- @Test
- public void wasKeyRotated_noExistingKey_returnsTrue() throws Exception {
- TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
- assertThat(manager.wasKeyRotated()).isTrue();
- }
-
- @Test
- public void wasKeyRotated_existingKey_returnsFalse() throws Exception {
- createNewManager(TEST_PACKAGE_1).getKey();
- assertThat(createNewManager(TEST_PACKAGE_1).wasKeyRotated()).isFalse();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java
deleted file mode 100644
index dfc7e2b..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * 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.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-import static org.robolectric.RuntimeEnvironment.application;
-
-import android.content.Context;
-
-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.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.File;
-import java.time.Clock;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.TimeUnit;
-
-/** Tests for the tertiary key rotation scheduler */
-@RunWith(RobolectricTestRunner.class)
-public final class TertiaryKeyRotationSchedulerTest {
-
- private static final int MAXIMUM_ROTATIONS_PER_WINDOW = 2;
- private static final int MAX_BACKUPS_TILL_ROTATION = 31;
- private static final String SHARED_PREFS_NAME = "tertiary_key_rotation_tracker";
- private static final String PACKAGE_1 = "com.android.example1";
- private static final String PACKAGE_2 = "com.android.example2";
-
- @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- @Mock private Clock mClock;
-
- private File mFile;
- private TertiaryKeyRotationScheduler mScheduler;
-
- /** Setup the scheduler for test */
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- mFile = temporaryFolder.newFile();
- mScheduler =
- new TertiaryKeyRotationScheduler(
- new TertiaryKeyRotationTracker(
- application.getSharedPreferences(
- SHARED_PREFS_NAME, Context.MODE_PRIVATE),
- MAX_BACKUPS_TILL_ROTATION),
- new TertiaryKeyRotationWindowedCount(mFile, mClock),
- MAXIMUM_ROTATIONS_PER_WINDOW);
- }
-
- /** Test we don't trigger a rotation straight off */
- @Test
- public void isKeyRotationDue_isFalseInitially() {
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
- }
-
- /** Test we don't prematurely trigger a rotation */
- @Test
- public void isKeyRotationDue_isFalseAfterInsufficientBackups() {
- simulateBackups(MAX_BACKUPS_TILL_ROTATION - 1);
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
- }
-
- /** Test we do trigger a backup */
- @Test
- public void isKeyRotationDue_isTrueAfterEnoughBackups() {
- simulateBackups(MAX_BACKUPS_TILL_ROTATION);
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
- }
-
- /** Test rotation will occur if the quota allows */
- @Test
- public void isKeyRotationDue_isTrueIfRotationQuotaRemainsInWindow() {
- simulateBackups(MAX_BACKUPS_TILL_ROTATION);
- mScheduler.recordKeyRotation(PACKAGE_2);
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
- }
-
- /** Test rotation is blocked if the quota has been exhausted */
- @Test
- public void isKeyRotationDue_isFalseIfEnoughRotationsHaveHappenedInWindow() {
- simulateBackups(MAX_BACKUPS_TILL_ROTATION);
- mScheduler.recordKeyRotation(PACKAGE_2);
- mScheduler.recordKeyRotation(PACKAGE_2);
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
- }
-
- /** Test rotation is due after one window has passed */
- @Test
- public void isKeyRotationDue_isTrueAfterAWholeWindowHasPassed() {
- simulateBackups(MAX_BACKUPS_TILL_ROTATION);
- mScheduler.recordKeyRotation(PACKAGE_2);
- mScheduler.recordKeyRotation(PACKAGE_2);
- setTimeMillis(TimeUnit.HOURS.toMillis(24));
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
- }
-
- /** Test the rotation state changes after a rotation */
- @Test
- public void isKeyRotationDue_isFalseAfterRotation() {
- simulateBackups(MAX_BACKUPS_TILL_ROTATION);
- mScheduler.recordKeyRotation(PACKAGE_1);
- assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
- }
-
- /** Test the rate limiting for a given window */
- @Test
- public void isKeyRotationDue_neverAllowsMoreThanInWindow() {
- List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION);
-
- // simulate backups of all apps each night
- for (int i = 0; i < 300; i++) {
- setTimeMillis(i * TimeUnit.HOURS.toMillis(24));
- int rotationsThisNight = 0;
- for (String app : apps) {
- if (mScheduler.isKeyRotationDue(app)) {
- rotationsThisNight++;
- mScheduler.recordKeyRotation(app);
- } else {
- mScheduler.recordBackup(app);
- }
- }
- assertThat(rotationsThisNight).isAtMost(MAXIMUM_ROTATIONS_PER_WINDOW);
- }
- }
-
- /** Test that backups are staggered over the window */
- @Test
- public void isKeyRotationDue_naturallyStaggersBackupsOverTime() {
- List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION);
-
- HashMap<String, ArrayList<Integer>> rotationDays = new HashMap<>();
- for (String app : apps) {
- rotationDays.put(app, new ArrayList<>());
- }
-
- // simulate backups of all apps each night
- for (int i = 0; i < 300; i++) {
- setTimeMillis(i * TimeUnit.HOURS.toMillis(24));
- for (String app : apps) {
- if (mScheduler.isKeyRotationDue(app)) {
- rotationDays.get(app).add(i);
- mScheduler.recordKeyRotation(app);
- } else {
- mScheduler.recordBackup(app);
- }
- }
- }
-
- for (String app : apps) {
- List<Integer> days = rotationDays.get(app);
- for (int i = 1; i < days.size(); i++) {
- assertThat(days.get(i) - days.get(i - 1)).isEqualTo(MAX_BACKUPS_TILL_ROTATION + 1);
- }
- }
- }
-
- private ArrayList<String> makeTestApps(int n) {
- ArrayList<String> apps = new ArrayList<>();
- for (int i = 0; i < n; i++) {
- apps.add(String.format(Locale.US, "com.android.app%d", i));
- }
- return apps;
- }
-
- private void simulateBackups(int numberOfBackups) {
- while (numberOfBackups > 0) {
- mScheduler.recordBackup(PACKAGE_1);
- numberOfBackups--;
- }
- }
-
- private void setTimeMillis(long timeMillis) {
- when(mClock.millis()).thenReturn(timeMillis);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTrackerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTrackerTest.java
deleted file mode 100644
index 49bb410..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTrackerTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-/** Tests for {@link TertiaryKeyRotationTracker}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class TertiaryKeyRotationTrackerTest {
- private static final String PACKAGE_1 = "com.package.one";
- private static final int NUMBER_OF_BACKUPS_BEFORE_ROTATION = 31;
-
- private TertiaryKeyRotationTracker mTertiaryKeyRotationTracker;
-
- /** Instantiate a {@link TertiaryKeyRotationTracker} for use in tests. */
- @Before
- public void setUp() {
- mTertiaryKeyRotationTracker = newInstance();
- }
-
- /** New packages should not be due for key rotation. */
- @Test
- public void isKeyRotationDue_forNewPackage_isFalse() {
- // Simulate a new package by not calling simulateBackups(). As a result, PACKAGE_1 hasn't
- // been seen by mTertiaryKeyRotationTracker before.
- boolean keyRotationDue = mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1);
-
- assertThat(keyRotationDue).isFalse();
- }
-
- /**
- * Key rotation should not be due after less than {@code NUMBER_OF_BACKUPS_BEFORE_ROTATION}
- * backups.
- */
- @Test
- public void isKeyRotationDue_afterLessThanRotationAmountBackups_isFalse() {
- simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION - 1);
-
- boolean keyRotationDue = mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1);
-
- assertThat(keyRotationDue).isFalse();
- }
-
- /** Key rotation should be due after {@code NUMBER_OF_BACKUPS_BEFORE_ROTATION} backups. */
- @Test
- public void isKeyRotationDue_afterRotationAmountBackups_isTrue() {
- simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION);
-
- boolean keyRotationDue = mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1);
-
- assertThat(keyRotationDue).isTrue();
- }
-
- /**
- * A call to {@link TertiaryKeyRotationTracker#resetCountdown(String)} should make sure no key
- * rotation is due.
- */
- @Test
- public void resetCountdown_makesKeyRotationNotDue() {
- simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION);
-
- mTertiaryKeyRotationTracker.resetCountdown(PACKAGE_1);
-
- assertThat(mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1)).isFalse();
- }
-
- /**
- * New instances of {@link TertiaryKeyRotationTracker} should read state about the number of
- * backups from disk.
- */
- @Test
- public void isKeyRotationDue_forNewInstance_readsStateFromDisk() {
- simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION);
-
- boolean keyRotationDueForNewInstance = newInstance().isKeyRotationDue(PACKAGE_1);
-
- assertThat(keyRotationDueForNewInstance).isTrue();
- }
-
- /**
- * A call to {@link TertiaryKeyRotationTracker#markAllForRotation()} should mark all previously
- * seen packages for rotation.
- */
- @Test
- public void markAllForRotation_marksSeenPackagesForKeyRotation() {
- simulateBackups(PACKAGE_1, /*numberOfBackups=*/ 1);
-
- mTertiaryKeyRotationTracker.markAllForRotation();
-
- assertThat(mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1)).isTrue();
- }
-
- /**
- * A call to {@link TertiaryKeyRotationTracker#markAllForRotation()} should not mark any new
- * packages for rotation.
- */
- @Test
- public void markAllForRotation_doesNotMarkUnseenPackages() {
- mTertiaryKeyRotationTracker.markAllForRotation();
-
- assertThat(mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1)).isFalse();
- }
-
- private void simulateBackups(String packageName, int numberOfBackups) {
- while (numberOfBackups > 0) {
- mTertiaryKeyRotationTracker.recordBackup(packageName);
- numberOfBackups--;
- }
- }
-
- private static TertiaryKeyRotationTracker newInstance() {
- return TertiaryKeyRotationTracker.getInstance(RuntimeEnvironment.application);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java
deleted file mode 100644
index bd30977..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.keys;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-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.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.File;
-import java.io.IOException;
-import java.time.Clock;
-import java.util.concurrent.TimeUnit;
-
-/** Tests for {@link TertiaryKeyRotationWindowedCount}. */
-@RunWith(RobolectricTestRunner.class)
-public class TertiaryKeyRotationWindowedCountTest {
- private static final int TIMESTAMP_SIZE_IN_BYTES = 8;
-
- @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-
- @Mock private Clock mClock;
-
- private File mFile;
- private TertiaryKeyRotationWindowedCount mWindowedcount;
-
- /** Setup the windowed counter for testing */
- @Before
- public void setUp() throws IOException {
- MockitoAnnotations.initMocks(this);
- mFile = mTemporaryFolder.newFile();
- mWindowedcount = new TertiaryKeyRotationWindowedCount(mFile, mClock);
- }
-
- /** Test handling bad files */
- @Test
- public void constructor_doesNotFailForBadFile() throws IOException {
- new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock);
- }
-
- /** Test the count is 0 to start */
- @Test
- public void getCount_isZeroInitially() {
- assertThat(mWindowedcount.getCount()).isEqualTo(0);
- }
-
- /** Test the count is correct for a time window */
- @Test
- public void getCount_includesResultsInLastTwentyFourHours() {
- setTimeMillis(0);
- mWindowedcount.record();
- setTimeMillis(TimeUnit.HOURS.toMillis(4));
- mWindowedcount.record();
- setTimeMillis(TimeUnit.HOURS.toMillis(23));
- mWindowedcount.record();
- mWindowedcount.record();
- assertThat(mWindowedcount.getCount()).isEqualTo(4);
- }
-
- /** Test old results are ignored */
- @Test
- public void getCount_ignoresResultsOlderThanTwentyFourHours() {
- setTimeMillis(0);
- mWindowedcount.record();
- setTimeMillis(TimeUnit.HOURS.toMillis(24));
- assertThat(mWindowedcount.getCount()).isEqualTo(0);
- }
-
- /** Test future events are removed if the clock moves backways (e.g. DST, TZ change) */
- @Test
- public void getCount_removesFutureEventsIfClockHasChanged() {
- setTimeMillis(1000);
- mWindowedcount.record();
- setTimeMillis(0);
- assertThat(mWindowedcount.getCount()).isEqualTo(0);
- }
-
- /** Check recording doesn't fail for a bad file */
- @Test
- public void record_doesNotFailForBadFile() throws Exception {
- new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock).record();
- }
-
- /** Checks the state is persisted */
- @Test
- public void record_persistsStateToDisk() {
- setTimeMillis(0);
- mWindowedcount.record();
- assertThat(new TertiaryKeyRotationWindowedCount(mFile, mClock).getCount()).isEqualTo(1);
- }
-
- /** Test the file doesn't contain unnecessary data */
- @Test
- public void record_compactsFileToLast24Hours() {
- setTimeMillis(0);
- mWindowedcount.record();
- assertThat(mFile.length()).isEqualTo(TIMESTAMP_SIZE_IN_BYTES);
- setTimeMillis(1);
- mWindowedcount.record();
- assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES);
- setTimeMillis(TimeUnit.HOURS.toMillis(24));
- mWindowedcount.record();
- assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES);
- }
-
- private void setTimeMillis(long timeMillis) {
- when(mClock.millis()).thenReturn(timeMillis);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java
deleted file mode 100644
index ccc5f32..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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.keys;
-
-import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertThrows;
-import static org.testng.Assert.assertTrue;
-
-import android.content.Context;
-
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-import java.security.InvalidKeyException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-import javax.crypto.SecretKey;
-
-/** Tests for the tertiary key store */
-@RunWith(RobolectricTestRunner.class)
-public class TertiaryKeyStoreTest {
-
- private static final String SECONDARY_KEY_ALIAS = "Robbo/Ranx";
-
- private Context mApplication;
- private TertiaryKeyStore mTertiaryKeyStore;
- private SecretKey mSecretKey;
-
- /** Initialise the keystore for testing */
- @Before
- public void setUp() throws Exception {
- mApplication = RuntimeEnvironment.application;
- mSecretKey = generateAesKey();
- mTertiaryKeyStore =
- TertiaryKeyStore.newInstance(
- mApplication,
- new RecoverableKeyStoreSecondaryKey(SECONDARY_KEY_ALIAS, mSecretKey));
- }
-
- /** Test a reound trip for a key */
- @Test
- public void load_loadsAKeyThatWasSaved() throws Exception {
- String packageName = "com.android.example";
- SecretKey packageKey = generateAesKey();
- mTertiaryKeyStore.save(packageName, packageKey);
-
- Optional<SecretKey> maybeLoadedKey = mTertiaryKeyStore.load(packageName);
-
- assertTrue(maybeLoadedKey.isPresent());
- assertEquals(packageKey, maybeLoadedKey.get());
- }
-
- /** Test isolation between packages */
- @Test
- public void load_doesNotLoadAKeyForAnotherSecondary() throws Exception {
- String packageName = "com.android.example";
- SecretKey packageKey = generateAesKey();
- mTertiaryKeyStore.save(packageName, packageKey);
- TertiaryKeyStore managerWithOtherSecondaryKey =
- TertiaryKeyStore.newInstance(
- mApplication,
- new RecoverableKeyStoreSecondaryKey(
- "myNewSecondaryKeyAlias", generateAesKey()));
-
- assertFalse(managerWithOtherSecondaryKey.load(packageName).isPresent());
- }
-
- /** Test non-existent key handling */
- @Test
- public void load_returnsAbsentForANonExistentKey() throws Exception {
- assertFalse(mTertiaryKeyStore.load("mystery.package").isPresent());
- }
-
- /** Test handling incorrect keys */
- @Test
- public void load_throwsIfHasWrongBackupKey() throws Exception {
- String packageName = "com.android.example";
- SecretKey packageKey = generateAesKey();
- mTertiaryKeyStore.save(packageName, packageKey);
- TertiaryKeyStore managerWithBadKey =
- TertiaryKeyStore.newInstance(
- mApplication,
- new RecoverableKeyStoreSecondaryKey(SECONDARY_KEY_ALIAS, generateAesKey()));
-
- assertThrows(InvalidKeyException.class, () -> managerWithBadKey.load(packageName));
- }
-
- /** Test handling of empty app name */
- @Test
- public void load_throwsForEmptyApplicationName() throws Exception {
- assertThrows(IllegalArgumentException.class, () -> mTertiaryKeyStore.load(""));
- }
-
- /** Test handling of an invalid app name */
- @Test
- public void load_throwsForBadApplicationName() throws Exception {
- assertThrows(
- IllegalArgumentException.class,
- () -> mTertiaryKeyStore.load("com/android/example"));
- }
-
- /** Test key replacement */
- @Test
- public void save_overwritesPreviousKey() throws Exception {
- String packageName = "com.android.example";
- SecretKey oldKey = generateAesKey();
- mTertiaryKeyStore.save(packageName, oldKey);
- SecretKey newKey = generateAesKey();
-
- mTertiaryKeyStore.save(packageName, newKey);
-
- Optional<SecretKey> maybeLoadedKey = mTertiaryKeyStore.load(packageName);
- assertTrue(maybeLoadedKey.isPresent());
- SecretKey loadedKey = maybeLoadedKey.get();
- assertThat(loadedKey).isNotEqualTo(oldKey);
- assertThat(loadedKey).isEqualTo(newKey);
- }
-
- /** Test saving with an empty application name fails */
- @Test
- public void save_throwsForEmptyApplicationName() throws Exception {
- assertThrows(
- IllegalArgumentException.class, () -> mTertiaryKeyStore.save("", generateAesKey()));
- }
-
- /** Test saving an invalid application name fails */
- @Test
- public void save_throwsForBadApplicationName() throws Exception {
- assertThrows(
- IllegalArgumentException.class,
- () -> mTertiaryKeyStore.save("com/android/example", generateAesKey()));
- }
-
- /** Test handling an empty database */
- @Test
- public void getAll_returnsEmptyMapForEmptyDb() throws Exception {
- assertThat(mTertiaryKeyStore.getAll()).isEmpty();
- }
-
- /** Test loading all available keys works as expected */
- @Test
- public void getAll_returnsAllKeysSaved() throws Exception {
- String package1 = "com.android.example";
- SecretKey key1 = generateAesKey();
- String package2 = "com.anndroid.example1";
- SecretKey key2 = generateAesKey();
- String package3 = "com.android.example2";
- SecretKey key3 = generateAesKey();
- mTertiaryKeyStore.save(package1, key1);
- mTertiaryKeyStore.save(package2, key2);
- mTertiaryKeyStore.save(package3, key3);
-
- Map<String, SecretKey> keys = mTertiaryKeyStore.getAll();
-
- assertThat(keys).containsExactly(package1, key1, package2, key2, package3, key3);
- }
-
- /** Test cross-secondary isolation */
- @Test
- public void getAll_doesNotReturnKeysForOtherSecondary() throws Exception {
- String packageName = "com.android.example";
- TertiaryKeyStore managerWithOtherSecondaryKey =
- TertiaryKeyStore.newInstance(
- mApplication,
- new RecoverableKeyStoreSecondaryKey(
- "myNewSecondaryKeyAlias", generateAesKey()));
- managerWithOtherSecondaryKey.save(packageName, generateAesKey());
-
- assertThat(mTertiaryKeyStore.getAll()).isEmpty();
- }
-
- /** Test mass put into the keystore */
- @Test
- public void putAll_putsAllWrappedKeysInTheStore() throws Exception {
- String packageName = "com.android.example";
- SecretKey key = generateAesKey();
- WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(mSecretKey, key);
-
- Map<String, WrappedKeyProto.WrappedKey> testElements = new HashMap<>();
- testElements.put(packageName, wrappedKey);
- mTertiaryKeyStore.putAll(testElements);
-
- assertThat(mTertiaryKeyStore.getAll()).containsKey(packageName);
- assertThat(mTertiaryKeyStore.getAll().get(packageName).getEncoded())
- .isEqualTo(key.getEncoded());
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java
deleted file mode 100644
index 215e1cb..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * 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.kv;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.os.Debug;
-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.protos.nano.KeyValuePairProto;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.security.MessageDigest;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.stream.Stream;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class DecryptedChunkKvOutputTest {
- private static final String TEST_KEY_1 = "key_1";
- private static final String TEST_KEY_2 = "key_2";
- private static final byte[] TEST_VALUE_1 = {1, 2, 3};
- private static final byte[] TEST_VALUE_2 = {10, 11, 12, 13};
- private static final byte[] TEST_PAIR_1 = toByteArray(createPair(TEST_KEY_1, TEST_VALUE_1));
- private static final byte[] TEST_PAIR_2 = toByteArray(createPair(TEST_KEY_2, TEST_VALUE_2));
- private static final int TEST_BUFFER_SIZE = Math.max(TEST_PAIR_1.length, TEST_PAIR_2.length);
-
- @Mock private ChunkHasher mChunkHasher;
- private DecryptedChunkKvOutput mOutput;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- when(mChunkHasher.computeHash(any()))
- .thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
- mOutput = new DecryptedChunkKvOutput(mChunkHasher);
- }
-
- @Test
- public void open_returnsInstance() throws Exception {
- assertThat(mOutput.open()).isEqualTo(mOutput);
- }
-
- @Test
- public void processChunk_alreadyClosed_throws() throws Exception {
- mOutput.open();
- mOutput.close();
-
- assertThrows(
- IllegalStateException.class,
- () -> mOutput.processChunk(TEST_PAIR_1, TEST_PAIR_1.length));
- }
-
- @Test
- public void getDigest_beforeClose_throws() throws Exception {
- // TODO: b/141356823 We should add a test which calls .open() here
- assertThrows(IllegalStateException.class, () -> mOutput.getDigest());
- }
-
- @Test
- public void getDigest_returnsDigestOfSortedHashes() throws Exception {
- mOutput.open();
- Debug.waitForDebugger();
- mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
- mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
- mOutput.close();
-
- byte[] actualDigest = mOutput.getDigest();
-
- MessageDigest digest = MessageDigest.getInstance(DecryptedChunkKvOutput.DIGEST_ALGORITHM);
- Stream.of(TEST_PAIR_1, TEST_PAIR_2)
- .map(DecryptedChunkKvOutputTest::fakeHash)
- .sorted(Comparator.naturalOrder())
- .forEachOrdered(hash -> digest.update(hash.getHash()));
- assertThat(actualDigest).isEqualTo(digest.digest());
- }
-
- @Test
- public void getPairs_beforeClose_throws() throws Exception {
- // TODO: b/141356823 We should add a test which calls .open() here
- assertThrows(IllegalStateException.class, () -> mOutput.getPairs());
- }
-
- @Test
- public void getPairs_returnsPairsSortedByKey() throws Exception {
- mOutput.open();
- // Write out of order to check that it sorts the chunks.
- mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
- mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
- mOutput.close();
-
- List<KeyValuePairProto.KeyValuePair> pairs = mOutput.getPairs();
-
- assertThat(
- isInOrder(
- pairs,
- Comparator.comparing(
- (KeyValuePairProto.KeyValuePair pair) -> pair.key)))
- .isTrue();
- assertThat(pairs).hasSize(2);
- assertThat(pairs.get(0).key).isEqualTo(TEST_KEY_1);
- assertThat(pairs.get(0).value).isEqualTo(TEST_VALUE_1);
- assertThat(pairs.get(1).key).isEqualTo(TEST_KEY_2);
- assertThat(pairs.get(1).value).isEqualTo(TEST_VALUE_2);
- }
-
- private static KeyValuePairProto.KeyValuePair createPair(String key, byte[] value) {
- KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
- pair.key = key;
- pair.value = value;
- return pair;
- }
-
- private boolean isInOrder(
- List<KeyValuePairProto.KeyValuePair> list,
- Comparator<KeyValuePairProto.KeyValuePair> comparator) {
- if (list.size() < 2) {
- return true;
- }
-
- List<KeyValuePairProto.KeyValuePair> sortedList = new ArrayList<>(list);
- Collections.sort(sortedList, comparator);
- return list.equals(sortedList);
- }
-
- private static byte[] toByteArray(KeyValuePairProto.KeyValuePair nano) {
- return KeyValuePairProto.KeyValuePair.toByteArray(nano);
- }
-
- private static ChunkHash fakeHash(byte[] data) {
- return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java
deleted file mode 100644
index acc6628..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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.kv;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class KeyValueListingBuilderTest {
- private static final String TEST_KEY_1 = "test_key_1";
- private static final String TEST_KEY_2 = "test_key_2";
- private static final ChunkHash TEST_HASH_1 =
- new ChunkHash(Arrays.copyOf(new byte[] {1, 2}, ChunkHash.HASH_LENGTH_BYTES));
- private static final ChunkHash TEST_HASH_2 =
- new ChunkHash(Arrays.copyOf(new byte[] {5, 6}, ChunkHash.HASH_LENGTH_BYTES));
-
- private KeyValueListingBuilder mBuilder;
-
- @Before
- public void setUp() {
- mBuilder = new KeyValueListingBuilder();
- }
-
- @Test
- public void addPair_nullKey_throws() {
- assertThrows(NullPointerException.class, () -> mBuilder.addPair(null, TEST_HASH_1));
- }
-
- @Test
- public void addPair_emptyKey_throws() {
- assertThrows(IllegalArgumentException.class, () -> mBuilder.addPair("", TEST_HASH_1));
- }
-
- @Test
- public void addPair_nullHash_throws() {
- assertThrows(NullPointerException.class, () -> mBuilder.addPair(TEST_KEY_1, null));
- }
-
- @Test
- public void build_noPairs_buildsEmptyListing() {
- KeyValueListingProto.KeyValueListing listing = mBuilder.build();
-
- assertThat(listing.entries).isEmpty();
- }
-
- @Test
- public void build_returnsCorrectListing() {
- mBuilder.addPair(TEST_KEY_1, TEST_HASH_1);
-
- KeyValueListingProto.KeyValueListing listing = mBuilder.build();
-
- assertThat(listing.entries.length).isEqualTo(1);
- assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
- assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
- }
-
- @Test
- public void addAll_addsAllPairsInMap() {
- ImmutableMap<String, ChunkHash> pairs =
- new ImmutableMap.Builder<String, ChunkHash>()
- .put(TEST_KEY_1, TEST_HASH_1)
- .put(TEST_KEY_2, TEST_HASH_2)
- .build();
-
- mBuilder.addAll(pairs);
- KeyValueListingProto.KeyValueListing listing = mBuilder.build();
-
- assertThat(listing.entries.length).isEqualTo(2);
- assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
- assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
- assertThat(listing.entries[1].key).isEqualTo(TEST_KEY_2);
- assertThat(listing.entries[1].hash).isEqualTo(TEST_HASH_2.getHash());
- }
-
- @Test
- public void emptyListing_returnsListingWithoutAnyPairs() {
- KeyValueListingProto.KeyValueListing emptyListing = KeyValueListingBuilder.emptyListing();
- assertThat(emptyListing.entries).isEmpty();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/storage/BackupEncryptionDbTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/storage/BackupEncryptionDbTest.java
deleted file mode 100644
index 87f21bf..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/storage/BackupEncryptionDbTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-/** Tests for {@link BackupEncryptionDb}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class BackupEncryptionDbTest {
- private BackupEncryptionDb mBackupEncryptionDb;
-
- /** Creates an empty {@link BackupEncryptionDb} */
- @Before
- public void setUp() {
- mBackupEncryptionDb = BackupEncryptionDb.newInstance(RuntimeEnvironment.application);
- }
-
- /**
- * Tests that the tertiary keys table gets cleared when calling {@link
- * BackupEncryptionDb#clear()}.
- */
- @Test
- public void clear_withNonEmptyTertiaryKeysTable_clearsTertiaryKeysTable() throws Exception {
- String secondaryKeyAlias = "secondaryKeyAlias";
- TertiaryKeysTable tertiaryKeysTable = mBackupEncryptionDb.getTertiaryKeysTable();
- tertiaryKeysTable.addKey(new TertiaryKey(secondaryKeyAlias, "packageName", new byte[0]));
-
- mBackupEncryptionDb.clear();
-
- assertThat(tertiaryKeysTable.getAllKeys(secondaryKeyAlias)).isEmpty();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/storage/TertiaryKeysTableTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/storage/TertiaryKeysTableTest.java
deleted file mode 100644
index 319ec89..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/storage/TertiaryKeysTableTest.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.encryption.storage;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.testing.CryptoTestUtils;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-import java.util.Map;
-import java.util.Optional;
-
-/** Tests for {@link TertiaryKeysTable}. */
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class TertiaryKeysTableTest {
- private static final int KEY_SIZE_BYTES = 32;
- private static final String SECONDARY_ALIAS = "phoebe";
- private static final String PACKAGE_NAME = "generic.package.name";
-
- private TertiaryKeysTable mTertiaryKeysTable;
-
- /** Creates an empty {@link BackupEncryptionDb}. */
- @Before
- public void setUp() {
- mTertiaryKeysTable =
- BackupEncryptionDb.newInstance(RuntimeEnvironment.application)
- .getTertiaryKeysTable();
- }
-
- /** Tests that new {@link TertiaryKey}s get successfully added to the database. */
- @Test
- public void addKey_onEmptyDatabase_putsKeyInDb() throws Exception {
- byte[] key = generateRandomKey();
- TertiaryKey keyToInsert = new TertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME, key);
-
- long result = mTertiaryKeysTable.addKey(keyToInsert);
-
- assertThat(result).isNotEqualTo(-1);
- Optional<TertiaryKey> maybeKeyInDb =
- mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME);
- assertThat(maybeKeyInDb.isPresent()).isTrue();
- TertiaryKey keyInDb = maybeKeyInDb.get();
- assertTertiaryKeysEqual(keyInDb, keyToInsert);
- }
-
- /** Tests that keys replace older keys with the same secondary alias and package name. */
- @Test
- public void addKey_havingSameSecondaryAliasAndPackageName_replacesOldKey() throws Exception {
- mTertiaryKeysTable.addKey(
- new TertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME, generateRandomKey()));
- byte[] newKey = generateRandomKey();
-
- long result =
- mTertiaryKeysTable.addKey(new TertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME, newKey));
-
- assertThat(result).isNotEqualTo(-1);
- TertiaryKey keyInDb = mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME).get();
- assertThat(keyInDb.getWrappedKeyBytes()).isEqualTo(newKey);
- }
-
- /**
- * Tests that keys do not replace older keys with the same package name but a different alias.
- */
- @Test
- public void addKey_havingSamePackageNameButDifferentAlias_doesNotReplaceOldKey()
- throws Exception {
- String alias2 = "karl";
- TertiaryKey key1 = generateTertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME);
- TertiaryKey key2 = generateTertiaryKey(alias2, PACKAGE_NAME);
-
- long primaryKey1 = mTertiaryKeysTable.addKey(key1);
- long primaryKey2 = mTertiaryKeysTable.addKey(key2);
-
- assertThat(primaryKey1).isNotEqualTo(primaryKey2);
- assertThat(mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME).isPresent()).isTrue();
- assertTertiaryKeysEqual(
- mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME).get(), key1);
- assertThat(mTertiaryKeysTable.getKey(alias2, PACKAGE_NAME).isPresent()).isTrue();
- assertTertiaryKeysEqual(mTertiaryKeysTable.getKey(alias2, PACKAGE_NAME).get(), key2);
- }
-
- /**
- * Tests that {@link TertiaryKeysTable#getKey(String, String)} returns an empty {@link Optional}
- * for a missing key.
- */
- @Test
- public void getKey_forMissingKey_returnsEmptyOptional() throws Exception {
- Optional<TertiaryKey> key = mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME);
-
- assertThat(key.isPresent()).isFalse();
- }
-
- /**
- * Tests that {@link TertiaryKeysTable#getAllKeys(String)} returns an empty map when no keys
- * with the secondary alias exist.
- */
- @Test
- public void getAllKeys_withNoKeysForAlias_returnsEmptyMap() throws Exception {
- assertThat(mTertiaryKeysTable.getAllKeys(SECONDARY_ALIAS)).isEmpty();
- }
-
- /**
- * Tests that {@link TertiaryKeysTable#getAllKeys(String)} returns all keys corresponding to the
- * provided secondary alias.
- */
- @Test
- public void getAllKeys_withMatchingKeys_returnsAllKeysWrappedWithSecondary() throws Exception {
- TertiaryKey key1 = generateTertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME);
- mTertiaryKeysTable.addKey(key1);
- String package2 = "generic.package.two";
- TertiaryKey key2 = generateTertiaryKey(SECONDARY_ALIAS, package2);
- mTertiaryKeysTable.addKey(key2);
- String package3 = "generic.package.three";
- TertiaryKey key3 = generateTertiaryKey(SECONDARY_ALIAS, package3);
- mTertiaryKeysTable.addKey(key3);
-
- Map<String, TertiaryKey> keysByPackageName = mTertiaryKeysTable.getAllKeys(SECONDARY_ALIAS);
-
- assertThat(keysByPackageName).hasSize(3);
- assertThat(keysByPackageName).containsKey(PACKAGE_NAME);
- assertTertiaryKeysEqual(keysByPackageName.get(PACKAGE_NAME), key1);
- assertThat(keysByPackageName).containsKey(package2);
- assertTertiaryKeysEqual(keysByPackageName.get(package2), key2);
- assertThat(keysByPackageName).containsKey(package3);
- assertTertiaryKeysEqual(keysByPackageName.get(package3), key3);
- }
-
- /**
- * Tests that {@link TertiaryKeysTable#getAllKeys(String)} does not return any keys wrapped with
- * another alias.
- */
- @Test
- public void getAllKeys_withMatchingKeys_doesNotReturnKeysWrappedWithOtherAlias()
- throws Exception {
- mTertiaryKeysTable.addKey(generateTertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME));
- mTertiaryKeysTable.addKey(generateTertiaryKey("somekey", "generic.package.two"));
-
- Map<String, TertiaryKey> keysByPackageName = mTertiaryKeysTable.getAllKeys(SECONDARY_ALIAS);
-
- assertThat(keysByPackageName).hasSize(1);
- assertThat(keysByPackageName).containsKey(PACKAGE_NAME);
- }
-
- private void assertTertiaryKeysEqual(TertiaryKey a, TertiaryKey b) {
- assertThat(a.getSecondaryKeyAlias()).isEqualTo(b.getSecondaryKeyAlias());
- assertThat(a.getPackageName()).isEqualTo(b.getPackageName());
- assertThat(a.getWrappedKeyBytes()).isEqualTo(b.getWrappedKeyBytes());
- }
-
- private TertiaryKey generateTertiaryKey(String alias, String packageName) {
- return new TertiaryKey(alias, packageName, generateRandomKey());
- }
-
- private byte[] generateRandomKey() {
- return CryptoTestUtils.generateRandomBytes(KEY_SIZE_BYTES);
- }
-}
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
deleted file mode 100644
index 07a6fd2..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
+++ /dev/null
@@ -1,583 +0,0 @@
-/*
- * 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/backup/encryption/tasks/BackupStreamEncrypterTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupStreamEncrypterTest.java
deleted file mode 100644
index 21c4e07..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupStreamEncrypterTest.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-import com.android.server.backup.testing.CryptoTestUtils;
-import com.android.server.backup.testing.RandomInputStream;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayInputStream;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Random;
-
-import javax.crypto.SecretKey;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class BackupStreamEncrypterTest {
- private static final int SALT_LENGTH = 32;
- private static final int BITS_PER_BYTE = 8;
- private static final int BYTES_PER_KILOBYTE = 1024;
- private static final int BYTES_PER_MEGABYTE = 1024 * 1024;
- private static final int MIN_CHUNK_SIZE = 2 * BYTES_PER_KILOBYTE;
- private static final int AVERAGE_CHUNK_SIZE = 4 * BYTES_PER_KILOBYTE;
- private static final int MAX_CHUNK_SIZE = 64 * BYTES_PER_KILOBYTE;
- private static final int BACKUP_SIZE = 2 * BYTES_PER_MEGABYTE;
- private static final int SMALL_BACKUP_SIZE = BYTES_PER_KILOBYTE;
- // 16 bytes for the mac. iv is encoded in a separate field.
- private static final int BYTES_OVERHEAD_PER_CHUNK = 16;
- private static final int MESSAGE_DIGEST_SIZE_IN_BYTES = 256 / BITS_PER_BYTE;
- private static final int RANDOM_SEED = 42;
- private static final double TOLERANCE = 0.1;
-
- private Random mRandom;
- private SecretKey mSecretKey;
- private byte[] mSalt;
-
- @Before
- public void setUp() throws Exception {
- mSecretKey = CryptoTestUtils.generateAesKey();
-
- mSalt = new byte[SALT_LENGTH];
- // Make these tests deterministic
- mRandom = new Random(RANDOM_SEED);
- mRandom.nextBytes(mSalt);
- }
-
- @Test
- public void testBackup_producesChunksOfTheGivenAverageSize() throws Exception {
- BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
-
- long totalSize = 0;
- for (EncryptedChunk chunk : result.getNewChunks()) {
- totalSize += chunk.encryptedBytes().length;
- }
-
- double meanSize = totalSize / result.getNewChunks().size();
- double expectedChunkSize = AVERAGE_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK;
- assertThat(Math.abs(meanSize - expectedChunkSize) / expectedChunkSize)
- .isLessThan(TOLERANCE);
- }
-
- @Test
- public void testBackup_producesNoChunksSmallerThanMinSize() throws Exception {
- BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
- List<EncryptedChunk> chunks = result.getNewChunks();
-
- // Last chunk could be smaller, depending on the file size and how it is chunked
- for (EncryptedChunk chunk : chunks.subList(0, chunks.size() - 2)) {
- assertThat(chunk.encryptedBytes().length)
- .isAtLeast(MIN_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK);
- }
- }
-
- @Test
- public void testBackup_producesNoChunksLargerThanMaxSize() throws Exception {
- BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
- List<EncryptedChunk> chunks = result.getNewChunks();
-
- for (EncryptedChunk chunk : chunks) {
- assertThat(chunk.encryptedBytes().length)
- .isAtMost(MAX_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK);
- }
- }
-
- @Test
- public void testBackup_producesAFileOfTheExpectedSize() throws Exception {
- BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
- HashMap<ChunkHash, EncryptedChunk> chunksBySha256 =
- chunksIndexedByKey(result.getNewChunks());
-
- int expectedSize = BACKUP_SIZE + result.getAllChunks().size() * BYTES_OVERHEAD_PER_CHUNK;
- int size = 0;
- for (ChunkHash byteString : result.getAllChunks()) {
- size += chunksBySha256.get(byteString).encryptedBytes().length;
- }
- assertThat(size).isEqualTo(expectedSize);
- }
-
- @Test
- public void testBackup_forSameFile_producesNoNewChunks() throws Exception {
- byte[] backupData = getRandomData(BACKUP_SIZE);
- BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
-
- BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
-
- assertThat(incrementalResult.getNewChunks()).isEmpty();
- }
-
- @Test
- public void testBackup_onlyUpdatesChangedChunks() throws Exception {
- byte[] backupData = getRandomData(BACKUP_SIZE);
- BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
-
- // Let's update the 2nd and 5th chunk
- backupData[positionOfChunk(result, 1)]++;
- backupData[positionOfChunk(result, 4)]++;
- BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
-
- assertThat(incrementalResult.getNewChunks()).hasSize(2);
- }
-
- @Test
- public void testBackup_doesNotIncludeUpdatedChunksInNewListing() throws Exception {
- byte[] backupData = getRandomData(BACKUP_SIZE);
- BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
-
- // Let's update the 2nd and 5th chunk
- backupData[positionOfChunk(result, 1)]++;
- backupData[positionOfChunk(result, 4)]++;
- BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
-
- List<EncryptedChunk> newChunks = incrementalResult.getNewChunks();
- List<ChunkHash> chunkListing = result.getAllChunks();
- assertThat(newChunks).doesNotContain(chunkListing.get(1));
- assertThat(newChunks).doesNotContain(chunkListing.get(4));
- }
-
- @Test
- public void testBackup_includesUnchangedChunksInNewListing() throws Exception {
- byte[] backupData = getRandomData(BACKUP_SIZE);
- BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
-
- // Let's update the 2nd and 5th chunk
- backupData[positionOfChunk(result, 1)]++;
- backupData[positionOfChunk(result, 4)]++;
- BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
-
- HashSet<ChunkHash> chunksPresentInIncremental =
- new HashSet<>(incrementalResult.getAllChunks());
- chunksPresentInIncremental.removeAll(result.getAllChunks());
-
- assertThat(chunksPresentInIncremental).hasSize(2);
- }
-
- @Test
- public void testBackup_forSameData_createsSameDigest() throws Exception {
- byte[] backupData = getRandomData(SMALL_BACKUP_SIZE);
-
- BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
- BackupEncrypter.Result result2 = runBackup(backupData, ImmutableList.of());
- assertThat(result.getDigest()).isEqualTo(result2.getDigest());
- }
-
- @Test
- public void testBackup_forDifferentData_createsDifferentDigest() throws Exception {
- byte[] backup1Data = getRandomData(SMALL_BACKUP_SIZE);
- byte[] backup2Data = getRandomData(SMALL_BACKUP_SIZE);
-
- BackupEncrypter.Result result = runBackup(backup1Data, ImmutableList.of());
- BackupEncrypter.Result result2 = runBackup(backup2Data, ImmutableList.of());
- assertThat(result.getDigest()).isNotEqualTo(result2.getDigest());
- }
-
- @Test
- public void testBackup_createsDigestOf32Bytes() throws Exception {
- assertThat(runBackup(getRandomData(SMALL_BACKUP_SIZE), ImmutableList.of()).getDigest())
- .hasLength(MESSAGE_DIGEST_SIZE_IN_BYTES);
- }
-
- private byte[] getRandomData(int size) throws Exception {
- RandomInputStream randomInputStream = new RandomInputStream(mRandom, size);
- byte[] backupData = new byte[size];
- randomInputStream.read(backupData);
- return backupData;
- }
-
- private BackupEncrypter.Result runBackup(int backupSize) throws Exception {
- RandomInputStream dataStream = new RandomInputStream(mRandom, backupSize);
- BackupStreamEncrypter task =
- new BackupStreamEncrypter(
- dataStream, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, AVERAGE_CHUNK_SIZE);
- return task.backup(mSecretKey, mSalt, ImmutableSet.of());
- }
-
- private BackupEncrypter.Result runBackup(byte[] data, List<ChunkHash> existingChunks)
- throws Exception {
- ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
- BackupStreamEncrypter task =
- new BackupStreamEncrypter(
- dataStream, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, AVERAGE_CHUNK_SIZE);
- return task.backup(mSecretKey, mSalt, ImmutableSet.copyOf(existingChunks));
- }
-
- /** Returns a {@link HashMap} of the chunks, indexed by the SHA-256 Mac key. */
- private static HashMap<ChunkHash, EncryptedChunk> chunksIndexedByKey(
- List<EncryptedChunk> chunks) {
- HashMap<ChunkHash, EncryptedChunk> chunksByKey = new HashMap<>();
- for (EncryptedChunk chunk : chunks) {
- chunksByKey.put(chunk.key(), chunk);
- }
- return chunksByKey;
- }
-
- /**
- * Returns the start position of the chunk in the plaintext backup data.
- *
- * @param result The result from a backup.
- * @param index The index of the chunk in question.
- * @return the start position.
- */
- private static int positionOfChunk(BackupEncrypter.Result result, int index) {
- HashMap<ChunkHash, EncryptedChunk> byKey = chunksIndexedByKey(result.getNewChunks());
- List<ChunkHash> listing = result.getAllChunks();
-
- int position = 0;
- for (int i = 0; i < index - 1; i++) {
- EncryptedChunk chunk = byKey.get(listing.get(i));
- position += chunk.encryptedBytes().length - BYTES_OVERHEAD_PER_CHUNK;
- }
-
- return position;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/ClearCryptoStateTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/ClearCryptoStateTaskTest.java
deleted file mode 100644
index 81bfce1..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/ClearCryptoStateTaskTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.spy;
-
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto.KeyValueListing;
-import com.android.server.backup.encryption.storage.BackupEncryptionDb;
-import com.android.server.backup.encryption.storage.TertiaryKey;
-import com.android.server.backup.encryption.storage.TertiaryKeysTable;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class ClearCryptoStateTaskTest {
- private static final String TEST_PACKAGE_NAME = "com.android.example";
-
- private ClearCryptoStateTask mClearCryptoStateTask;
- private CryptoSettings mCryptoSettings;
- private Context mApplication;
-
- @Before
- public void setUp() {
- mApplication = ApplicationProvider.getApplicationContext();
- mCryptoSettings = spy(CryptoSettings.getInstanceForTesting(mApplication));
- mClearCryptoStateTask = new ClearCryptoStateTask(mApplication, mCryptoSettings);
- }
-
- @Test
- public void run_clearsChunkListingProtoState() throws Exception {
- String packageName = TEST_PACKAGE_NAME;
- ChunkListing chunkListing = new ChunkListing();
- ProtoStore.createChunkListingStore(mApplication).saveProto(packageName, chunkListing);
-
- mClearCryptoStateTask.run();
-
- assertThat(
- ProtoStore.createChunkListingStore(mApplication)
- .loadProto(packageName)
- .isPresent())
- .isFalse();
- }
-
- @Test
- public void run_clearsKeyValueProtoState() throws Exception {
- String packageName = TEST_PACKAGE_NAME;
- KeyValueListing keyValueListing = new KeyValueListing();
- ProtoStore.createKeyValueListingStore(mApplication).saveProto(packageName, keyValueListing);
-
- mClearCryptoStateTask.run();
-
- assertThat(
- ProtoStore.createKeyValueListingStore(mApplication)
- .loadProto(packageName)
- .isPresent())
- .isFalse();
- }
-
- @Test
- public void run_clearsTertiaryKeysTable() throws Exception {
- String secondaryKeyAlias = "bob";
- TertiaryKeysTable tertiaryKeysTable =
- BackupEncryptionDb.newInstance(mApplication).getTertiaryKeysTable();
- tertiaryKeysTable.addKey(
- new TertiaryKey(
- secondaryKeyAlias, "packageName", /*wrappedKeyBytes=*/ new byte[0]));
-
- mClearCryptoStateTask.run();
-
- assertThat(tertiaryKeysTable.getAllKeys(secondaryKeyAlias)).isEmpty();
- }
-
- @Test
- public void run_clearsSettings() {
- mCryptoSettings.setSecondaryLastRotated(100001);
-
- mClearCryptoStateTask.run();
-
- assertThat(mCryptoSettings.getSecondaryLastRotated().isPresent()).isFalse();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java
deleted file mode 100644
index 23d6e34..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * 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.encryption.protos.nano.ChunksMetadataProto.AES_256_GCM;
-import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
-import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.SHA_256;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.BackupFileBuilder;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-import com.android.server.backup.encryption.chunking.EncryptedChunkEncoder;
-import com.android.server.backup.encryption.chunking.LengthlessEncryptedChunkEncoder;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.TertiaryKeyGenerator;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
-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.WrappedKeyProto.WrappedKey;
-import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
-import com.android.server.backup.testing.CryptoTestUtils;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.protobuf.nano.MessageNano;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-import java.io.OutputStream;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.concurrent.CancellationException;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-
-@Config(shadows = {EncryptedBackupTaskTest.ShadowBackupFileBuilder.class})
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class EncryptedBackupTaskTest {
-
- 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 byte[] TEST_FINGERPRINT_MIXER_SALT =
- Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
-
- private static final byte[] TEST_NONCE =
- Arrays.copyOf(new byte[] {55}, EncryptedChunk.NONCE_LENGTH_BYTES);
-
- private static final ChunkHash TEST_HASH_1 =
- new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
- private static final ChunkHash TEST_HASH_2 =
- new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
- private static final ChunkHash TEST_HASH_3 =
- new ChunkHash(Arrays.copyOf(new byte[] {3}, ChunkHash.HASH_LENGTH_BYTES));
-
- private static final EncryptedChunk TEST_CHUNK_1 =
- EncryptedChunk.create(TEST_HASH_1, TEST_NONCE, new byte[] {1, 2, 3, 4, 5});
- private static final EncryptedChunk TEST_CHUNK_2 =
- EncryptedChunk.create(TEST_HASH_2, TEST_NONCE, new byte[] {6, 7, 8, 9, 10});
- private static final EncryptedChunk TEST_CHUNK_3 =
- EncryptedChunk.create(TEST_HASH_3, TEST_NONCE, new byte[] {11, 12, 13, 14, 15});
-
- private static final byte[] TEST_CHECKSUM = Arrays.copyOf(new byte[] {10}, 258 / 8);
- private static final String TEST_PACKAGE_NAME = "com.example.package";
- private static final String TEST_OLD_DOCUMENT_ID = "old_doc_1";
- private static final String TEST_NEW_DOCUMENT_ID = "new_doc_1";
-
- @Captor private ArgumentCaptor<ChunksMetadata> mMetadataCaptor;
-
- @Mock private CryptoBackupServer mCryptoBackupServer;
- @Mock private BackupEncrypter mBackupEncrypter;
- @Mock private BackupFileBuilder mBackupFileBuilder;
-
- private ChunkListing mOldChunkListing;
- private SecretKey mTertiaryKey;
- private WrappedKey mWrappedTertiaryKey;
- private EncryptedChunkEncoder mEncryptedChunkEncoder;
- private EncryptedBackupTask mTask;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- SecureRandom secureRandom = new SecureRandom();
- mTertiaryKey = new TertiaryKeyGenerator(secureRandom).generate();
- mWrappedTertiaryKey = new WrappedKey();
-
- mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
-
- ShadowBackupFileBuilder.sInstance = mBackupFileBuilder;
-
- mTask =
- new EncryptedBackupTask(
- mCryptoBackupServer, secureRandom, TEST_PACKAGE_NAME, mBackupEncrypter);
- }
-
- @Test
- public void performNonIncrementalBackup_performsBackup() throws Exception {
- setUpWithoutExistingBackup();
-
- // Chunk listing and ordering don't matter for this test.
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
-
- when(mCryptoBackupServer.uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), any()))
- .thenReturn(TEST_NEW_DOCUMENT_ID);
-
- mTask.performNonIncrementalBackup(
- mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT);
-
- verify(mBackupFileBuilder)
- .writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
- ImmutableMap.of(TEST_HASH_1, TEST_CHUNK_1, TEST_HASH_2, TEST_CHUNK_2));
- verify(mBackupFileBuilder).finish(any());
- verify(mCryptoBackupServer)
- .uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), eq(mWrappedTertiaryKey));
- }
-
- @Test
- public void performIncrementalBackup_performsBackup() throws Exception {
- setUpWithExistingBackup();
-
- // Chunk listing and ordering don't matter for this test.
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
-
- when(mCryptoBackupServer.uploadIncrementalBackup(
- eq(TEST_PACKAGE_NAME), eq(TEST_OLD_DOCUMENT_ID), any(), any()))
- .thenReturn(TEST_NEW_DOCUMENT_ID);
-
- mTask.performIncrementalBackup(mTertiaryKey, mWrappedTertiaryKey, mOldChunkListing);
-
- verify(mBackupFileBuilder)
- .writeChunks(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
- ImmutableMap.of(TEST_HASH_2, TEST_CHUNK_2));
- verify(mBackupFileBuilder).finish(any());
- verify(mCryptoBackupServer)
- .uploadIncrementalBackup(
- eq(TEST_PACKAGE_NAME),
- eq(TEST_OLD_DOCUMENT_ID),
- any(),
- eq(mWrappedTertiaryKey));
- }
-
- @Test
- public void performIncrementalBackup_returnsNewChunkListingWithDocId() throws Exception {
- setUpWithExistingBackup();
-
- ChunkListing chunkListingWithoutDocId =
- CryptoTestUtils.newChunkListingWithoutDocId(
- TEST_FINGERPRINT_MIXER_SALT,
- AES_256_GCM,
- CHUNK_ORDERING_TYPE_UNSPECIFIED,
- createChunkProtoFor(TEST_HASH_1, TEST_CHUNK_1),
- createChunkProtoFor(TEST_HASH_2, TEST_CHUNK_2));
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(chunkListingWithoutDocId);
-
- // Chunk ordering doesn't matter for this test.
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
-
- when(mCryptoBackupServer.uploadIncrementalBackup(
- eq(TEST_PACKAGE_NAME), eq(TEST_OLD_DOCUMENT_ID), any(), any()))
- .thenReturn(TEST_NEW_DOCUMENT_ID);
-
- ChunkListing actualChunkListing =
- mTask.performIncrementalBackup(mTertiaryKey, mWrappedTertiaryKey, mOldChunkListing);
-
- ChunkListing expectedChunkListing = CryptoTestUtils.clone(chunkListingWithoutDocId);
- expectedChunkListing.documentId = TEST_NEW_DOCUMENT_ID;
- assertChunkListingsAreEqual(actualChunkListing, expectedChunkListing);
- }
-
- @Test
- public void performNonIncrementalBackup_returnsNewChunkListingWithDocId() throws Exception {
- setUpWithoutExistingBackup();
-
- ChunkListing chunkListingWithoutDocId =
- CryptoTestUtils.newChunkListingWithoutDocId(
- TEST_FINGERPRINT_MIXER_SALT,
- AES_256_GCM,
- CHUNK_ORDERING_TYPE_UNSPECIFIED,
- createChunkProtoFor(TEST_HASH_1, TEST_CHUNK_1),
- createChunkProtoFor(TEST_HASH_2, TEST_CHUNK_2));
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(chunkListingWithoutDocId);
-
- // Chunk ordering doesn't matter for this test.
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
-
- when(mCryptoBackupServer.uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), any()))
- .thenReturn(TEST_NEW_DOCUMENT_ID);
-
- ChunkListing actualChunkListing =
- mTask.performNonIncrementalBackup(
- mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT);
-
- ChunkListing expectedChunkListing = CryptoTestUtils.clone(chunkListingWithoutDocId);
- expectedChunkListing.documentId = TEST_NEW_DOCUMENT_ID;
- assertChunkListingsAreEqual(actualChunkListing, expectedChunkListing);
- }
-
- @Test
- public void performNonIncrementalBackup_buildsCorrectChunkMetadata() throws Exception {
- setUpWithoutExistingBackup();
-
- // Chunk listing doesn't matter for this test.
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
-
- ChunkOrdering expectedOrdering =
- CryptoTestUtils.newChunkOrdering(new int[10], TEST_CHECKSUM);
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(expectedOrdering);
-
- when(mCryptoBackupServer.uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), any()))
- .thenReturn(TEST_NEW_DOCUMENT_ID);
-
- mTask.performNonIncrementalBackup(
- mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT);
-
- verify(mBackupFileBuilder).finish(mMetadataCaptor.capture());
-
- ChunksMetadata actualMetadata = mMetadataCaptor.getValue();
- assertThat(actualMetadata.checksumType).isEqualTo(SHA_256);
- assertThat(actualMetadata.cipherType).isEqualTo(AES_256_GCM);
-
- ChunkOrdering actualOrdering = decryptChunkOrdering(actualMetadata.chunkOrdering);
- assertThat(actualOrdering.checksum).isEqualTo(TEST_CHECKSUM);
- assertThat(actualOrdering.starts).isEqualTo(expectedOrdering.starts);
- }
-
- @Test
- public void cancel_incrementalBackup_doesNotUploadOrSaveChunkListing() throws Exception {
- setUpWithExistingBackup();
-
- // Chunk listing and ordering don't matter for this test.
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
-
- mTask.cancel();
- assertThrows(
- CancellationException.class,
- () ->
- mTask.performIncrementalBackup(
- mTertiaryKey, mWrappedTertiaryKey, mOldChunkListing));
-
- verify(mCryptoBackupServer, never()).uploadIncrementalBackup(any(), any(), any(), any());
- verify(mCryptoBackupServer, never()).uploadNonIncrementalBackup(any(), any(), any());
- }
-
- @Test
- public void cancel_nonIncrementalBackup_doesNotUploadOrSaveChunkListing() throws Exception {
- setUpWithoutExistingBackup();
-
- // Chunk listing and ordering don't matter for this test.
- when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
- when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
-
- mTask.cancel();
- assertThrows(
- CancellationException.class,
- () ->
- mTask.performNonIncrementalBackup(
- mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT));
-
- verify(mCryptoBackupServer, never()).uploadIncrementalBackup(any(), any(), any(), any());
- verify(mCryptoBackupServer, never()).uploadNonIncrementalBackup(any(), any(), any());
- }
-
- /** Sets up a backup of [CHUNK 1][CHUNK 2] with no existing data. */
- private void setUpWithoutExistingBackup() throws Exception {
- Result result =
- new Result(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
- ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_2),
- TEST_CHECKSUM);
- when(mBackupEncrypter.backup(any(), eq(TEST_FINGERPRINT_MIXER_SALT), eq(ImmutableSet.of())))
- .thenReturn(result);
- }
-
- /**
- * Sets up a backup of [CHUNK 1][CHUNK 2][CHUNK 3] where the previous backup contained [CHUNK
- * 1][CHUNK 3].
- */
- private void setUpWithExistingBackup() throws Exception {
- mOldChunkListing =
- CryptoTestUtils.newChunkListing(
- TEST_OLD_DOCUMENT_ID,
- TEST_FINGERPRINT_MIXER_SALT,
- AES_256_GCM,
- CHUNK_ORDERING_TYPE_UNSPECIFIED,
- createChunkProtoFor(TEST_HASH_1, TEST_CHUNK_1),
- createChunkProtoFor(TEST_HASH_3, TEST_CHUNK_3));
-
- Result result =
- new Result(
- ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
- ImmutableList.of(TEST_CHUNK_2),
- TEST_CHECKSUM);
- when(mBackupEncrypter.backup(
- any(),
- eq(TEST_FINGERPRINT_MIXER_SALT),
- eq(ImmutableSet.of(TEST_HASH_1, TEST_HASH_3))))
- .thenReturn(result);
- }
-
- private ChunksMetadataProto.Chunk createChunkProtoFor(
- ChunkHash chunkHash, EncryptedChunk encryptedChunk) {
- return CryptoTestUtils.newChunk(
- chunkHash, mEncryptedChunkEncoder.getEncodedLengthOfChunk(encryptedChunk));
- }
-
- private ChunkOrdering decryptChunkOrdering(byte[] encryptedOrdering) throws Exception {
- Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- cipher.init(
- Cipher.DECRYPT_MODE,
- mTertiaryKey,
- new GCMParameterSpec(
- GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
- encryptedOrdering,
- /*offset=*/ 0,
- GCM_NONCE_LENGTH_BYTES));
- byte[] decrypted =
- cipher.doFinal(
- encryptedOrdering,
- GCM_NONCE_LENGTH_BYTES,
- encryptedOrdering.length - GCM_NONCE_LENGTH_BYTES);
- return ChunkOrdering.parseFrom(decrypted);
- }
-
- // This method is needed because nano protobuf generated classes dont implmenent
- // .equals
- private void assertChunkListingsAreEqual(ChunkListing a, ChunkListing b) {
- byte[] aBytes = MessageNano.toByteArray(a);
- byte[] bBytes = MessageNano.toByteArray(b);
-
- assertThat(aBytes).isEqualTo(bBytes);
- }
-
- @Implements(BackupFileBuilder.class)
- public static class ShadowBackupFileBuilder {
-
- private static BackupFileBuilder sInstance;
-
- @Implementation
- public static BackupFileBuilder createForNonIncremental(OutputStream outputStream) {
- return sInstance;
- }
-
- @Implementation
- public static BackupFileBuilder createForIncremental(
- OutputStream outputStream, ChunkListing oldChunkListing) {
- return sInstance;
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessorTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessorTest.java
deleted file mode 100644
index 675d03f..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessorTest.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.testng.Assert.assertThrows;
-
-import android.annotation.Nullable;
-import android.app.backup.BackupTransport;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.FullBackupDataProcessor;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.testing.QueuingNonAutomaticExecutorService;
-
-import com.google.common.io.ByteStreams;
-import com.google.common.primitives.Bytes;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-
-import javax.crypto.spec.SecretKeySpec;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-@Config(
- shadows = {
- EncryptedFullBackupDataProcessorTest.ShadowEncryptedFullBackupTask.class,
- })
-public class EncryptedFullBackupDataProcessorTest {
-
- private static final String KEY_GENERATOR_ALGORITHM = "AES";
-
- private static final String TEST_PACKAGE = "com.example.app1";
- private static final byte[] TEST_DATA_1 = {1, 2, 3, 4};
- private static final byte[] TEST_DATA_2 = {5, 6, 7, 8};
-
- private final RecoverableKeyStoreSecondaryKey mTestSecondaryKey =
- new RecoverableKeyStoreSecondaryKey(
- /*alias=*/ "test_key",
- new SecretKeySpec(
- new byte[] {
- 1, 2, 3,
- },
- KEY_GENERATOR_ALGORITHM));
-
- private QueuingNonAutomaticExecutorService mExecutorService;
- private FullBackupDataProcessor mFullBackupDataProcessor;
- @Mock private FullBackupDataProcessor.FullBackupCallbacks mFullBackupCallbacks;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mExecutorService = new QueuingNonAutomaticExecutorService();
- mFullBackupDataProcessor =
- new EncryptedFullBackupDataProcessor(
- ApplicationProvider.getApplicationContext(),
- mExecutorService,
- mock(CryptoBackupServer.class),
- new SecureRandom(),
- mTestSecondaryKey,
- TEST_PACKAGE);
- }
-
- @After
- public void tearDown() {
- ShadowEncryptedFullBackupTask.reset();
- }
-
- @Test
- public void initiate_callTwice_throws() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(new byte[10]));
-
- assertThrows(
- IllegalStateException.class,
- () -> mFullBackupDataProcessor.initiate(new ByteArrayInputStream(new byte[10])));
- }
-
- @Test
- public void pushData_writesDataToTask() throws Exception {
- byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
- finishBackupTask();
- mFullBackupDataProcessor.finish();
-
- byte[] result = ByteStreams.toByteArray(ShadowEncryptedFullBackupTask.sInputStream);
- assertThat(result).isEqualTo(Bytes.concat(TEST_DATA_1, TEST_DATA_2));
- }
-
- @Test
- public void pushData_noError_returnsOk() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- int result = mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTask();
- mFullBackupDataProcessor.finish();
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_OK);
- }
-
- @Test
- public void pushData_ioExceptionOnCopy_returnsError() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
-
- // Close the stream so there's an IO error when the processor tries to write to it.
- ShadowEncryptedFullBackupTask.sInputStream.close();
- int result = mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
-
- finishBackupTask();
- mFullBackupDataProcessor.finish();
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
- }
-
- @Test
- public void pushData_exceptionDuringUpload_returnsError() throws Exception {
- byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new IOException("Test exception"));
- int result = mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
- }
-
- @Test
- public void pushData_quotaExceptionDuringUpload_doesNotLogAndReturnsQuotaExceeded()
- throws Exception {
- mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
- byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new SizeQuotaExceededException());
- int result = mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_QUOTA_EXCEEDED);
-
- verify(mFullBackupCallbacks, never()).onSuccess();
- verify(mFullBackupCallbacks, never())
- .onTransferFailed(); // FullBackupSession will handle this.
- }
-
- @Test
- public void pushData_unexpectedEncryptedBackup_logs() throws Exception {
- byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new GeneralSecurityException());
- int result = mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
- }
-
- @Test
- public void pushData_permanentExceptionDuringUpload_callsErrorCallback() throws Exception {
- mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
- byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new IOException());
- mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
-
- verify(mFullBackupCallbacks, never()).onSuccess();
- verify(mFullBackupCallbacks).onTransferFailed();
- }
-
- @Test
- public void pushData_beforeInitiate_throws() {
- assertThrows(
- IllegalStateException.class,
- () -> mFullBackupDataProcessor.pushData(/*numBytes=*/ 10));
- }
-
- @Test
- public void cancel_cancelsTask() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- mFullBackupDataProcessor.cancel();
-
- assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
- }
-
- @Test
- public void cancel_beforeInitiate_throws() {
- assertThrows(IllegalStateException.class, () -> mFullBackupDataProcessor.cancel());
- }
-
- @Test
- public void finish_noException_returnsTransportOk() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTask();
- int result = mFullBackupDataProcessor.finish();
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_OK);
- }
-
- @Test
- public void finish_exceptionDuringUpload_returnsTransportError() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new IOException("Test exception"));
- int result = mFullBackupDataProcessor.finish();
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
- }
-
- @Test
- public void finish_successfulBackup_callsSuccessCallback() throws Exception {
- mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTask();
- mFullBackupDataProcessor.finish();
-
- verify(mFullBackupCallbacks).onSuccess();
- verify(mFullBackupCallbacks, never()).onTransferFailed();
- }
-
- @Test
- public void finish_backupFailedWithPermanentError_callsErrorCallback() throws Exception {
- mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new IOException());
- mFullBackupDataProcessor.finish();
-
- verify(mFullBackupCallbacks, never()).onSuccess();
- verify(mFullBackupCallbacks).onTransferFailed();
- }
-
- @Test
- public void finish_backupFailedWithQuotaException_doesNotCallbackAndReturnsQuotaExceeded()
- throws Exception {
- mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
-
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- finishBackupTaskWithException(new SizeQuotaExceededException());
- int result = mFullBackupDataProcessor.finish();
-
- assertThat(result).isEqualTo(BackupTransport.TRANSPORT_QUOTA_EXCEEDED);
- verify(mFullBackupCallbacks, never()).onSuccess();
- verify(mFullBackupCallbacks, never())
- .onTransferFailed(); // FullBackupSession will handle this.
- }
-
- @Test
- public void finish_beforeInitiate_throws() {
- assertThrows(IllegalStateException.class, () -> mFullBackupDataProcessor.finish());
- }
-
- @Test
- public void handleCheckSizeRejectionZeroBytes_cancelsTask() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(new byte[10]));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.handleCheckSizeRejectionZeroBytes();
-
- assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
- }
-
- @Test
- public void handleCheckSizeRejectionQuotaExceeded_cancelsTask() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- mFullBackupDataProcessor.handleCheckSizeRejectionQuotaExceeded();
-
- assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
- }
-
- @Test
- public void handleSendBytesQuotaExceeded_cancelsTask() throws Exception {
- mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
- mFullBackupDataProcessor.start();
- mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
- mFullBackupDataProcessor.handleSendBytesQuotaExceeded();
-
- assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
- }
-
- private void finishBackupTask() {
- mExecutorService.runNext();
- }
-
- private void finishBackupTaskWithException(Exception exception) {
- ShadowEncryptedFullBackupTask.sOnCallException = exception;
- finishBackupTask();
- }
-
- @Implements(EncryptedFullBackupTask.class)
- public static class ShadowEncryptedFullBackupTask {
-
- private static InputStream sInputStream;
- @Nullable private static Exception sOnCallException;
- private static boolean sCancelled;
-
- public void __constructor__(
- ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore,
- TertiaryKeyManager tertiaryKeyManager,
- EncryptedBackupTask task,
- InputStream inputStream,
- String packageName,
- SecureRandom secureRandom) {
- sInputStream = inputStream;
- }
-
- @Implementation
- public Void call() throws Exception {
- if (sOnCallException != null) {
- throw sOnCallException;
- }
-
- return null;
- }
-
- @Implementation
- public void cancel() {
- sCancelled = true;
- }
-
- public static void reset() {
- sOnCallException = null;
- sCancelled = false;
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java
deleted file mode 100644
index bfc5d0d..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto.WrappedKey;
-import com.android.server.backup.testing.CryptoTestUtils;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Optional;
-
-import javax.crypto.SecretKey;
-
-@Config(shadows = {EncryptedBackupTaskTest.ShadowBackupFileBuilder.class})
-@RunWith(RobolectricTestRunner.class)
-public class EncryptedFullBackupTaskTest {
- private static final String TEST_PACKAGE_NAME = "com.example.package";
- private static final byte[] TEST_EXISTING_FINGERPRINT_MIXER_SALT =
- Arrays.copyOf(new byte[] {11}, ChunkHash.HASH_LENGTH_BYTES);
- private static final byte[] TEST_GENERATED_FINGERPRINT_MIXER_SALT =
- Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
- private static final ChunkHash TEST_CHUNK_HASH_1 =
- new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
- private static final ChunkHash TEST_CHUNK_HASH_2 =
- new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
- private static final int TEST_CHUNK_LENGTH_1 = 20;
- private static final int TEST_CHUNK_LENGTH_2 = 40;
-
- @Mock private ProtoStore<ChunkListing> mChunkListingStore;
- @Mock private TertiaryKeyManager mTertiaryKeyManager;
- @Mock private InputStream mInputStream;
- @Mock private EncryptedBackupTask mEncryptedBackupTask;
- @Mock private SecretKey mTertiaryKey;
- @Mock private SecureRandom mSecureRandom;
-
- private EncryptedFullBackupTask mTask;
- private ChunkListing mOldChunkListing;
- private ChunkListing mNewChunkListing;
- private WrappedKey mWrappedTertiaryKey;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mWrappedTertiaryKey = new WrappedKey();
- when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
- when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
-
- mOldChunkListing =
- CryptoTestUtils.newChunkListing(
- /* docId */ null,
- TEST_EXISTING_FINGERPRINT_MIXER_SALT,
- ChunksMetadataProto.AES_256_GCM,
- ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
- CryptoTestUtils.newChunk(TEST_CHUNK_HASH_1.getHash(), TEST_CHUNK_LENGTH_1));
- mNewChunkListing =
- CryptoTestUtils.newChunkListing(
- /* docId */ null,
- /* fingerprintSalt */ null,
- ChunksMetadataProto.AES_256_GCM,
- ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
- CryptoTestUtils.newChunk(TEST_CHUNK_HASH_1.getHash(), TEST_CHUNK_LENGTH_1),
- CryptoTestUtils.newChunk(TEST_CHUNK_HASH_2.getHash(), TEST_CHUNK_LENGTH_2));
- when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
- .thenReturn(mNewChunkListing);
- when(mEncryptedBackupTask.performIncrementalBackup(any(), any(), any()))
- .thenReturn(mNewChunkListing);
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
-
- doAnswer(invocation -> {
- byte[] byteArray = (byte[]) invocation.getArguments()[0];
- System.arraycopy(
- TEST_GENERATED_FINGERPRINT_MIXER_SALT,
- /* srcPos */ 0,
- byteArray,
- /* destPos */ 0,
- FingerprintMixer.SALT_LENGTH_BYTES);
- return null;
- })
- .when(mSecureRandom)
- .nextBytes(any(byte[].class));
-
- mTask =
- new EncryptedFullBackupTask(
- mChunkListingStore,
- mTertiaryKeyManager,
- mEncryptedBackupTask,
- mInputStream,
- TEST_PACKAGE_NAME,
- mSecureRandom);
- }
-
- @Test
- public void call_existingChunkListingButTertiaryKeyRotated_performsNonIncrementalBackup()
- throws Exception {
- when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
- .thenReturn(Optional.of(mOldChunkListing));
-
- mTask.call();
-
- verify(mEncryptedBackupTask)
- .performNonIncrementalBackup(
- eq(mTertiaryKey),
- eq(mWrappedTertiaryKey),
- eq(TEST_GENERATED_FINGERPRINT_MIXER_SALT));
- }
-
- @Test
- public void call_noExistingChunkListing_performsNonIncrementalBackup() throws Exception {
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
- mTask.call();
- verify(mEncryptedBackupTask)
- .performNonIncrementalBackup(
- eq(mTertiaryKey),
- eq(mWrappedTertiaryKey),
- eq(TEST_GENERATED_FINGERPRINT_MIXER_SALT));
- }
-
- @Test
- public void call_existingChunkListing_performsIncrementalBackup() throws Exception {
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
- .thenReturn(Optional.of(mOldChunkListing));
- mTask.call();
- verify(mEncryptedBackupTask)
- .performIncrementalBackup(
- eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(mOldChunkListing));
- }
-
- @Test
- public void
- call_existingChunkListingWithNoFingerprintMixerSalt_doesntSetSaltBeforeIncBackup()
- throws Exception {
- mOldChunkListing.fingerprintMixerSalt = new byte[0];
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
- .thenReturn(Optional.of(mOldChunkListing));
-
- mTask.call();
-
- verify(mEncryptedBackupTask)
- .performIncrementalBackup(
- eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(mOldChunkListing));
- }
-
- @Test
- public void call_noExistingChunkListing_storesNewChunkListing() throws Exception {
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
- mTask.call();
- verify(mChunkListingStore).saveProto(TEST_PACKAGE_NAME, mNewChunkListing);
- }
-
- @Test
- public void call_existingChunkListing_storesNewChunkListing() throws Exception {
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
- .thenReturn(Optional.of(mOldChunkListing));
- mTask.call();
- verify(mChunkListingStore).saveProto(TEST_PACKAGE_NAME, mNewChunkListing);
- }
-
- @Test
- public void call_exceptionDuringBackup_doesNotSaveNewChunkListing() throws Exception {
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
- when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
- .thenThrow(GeneralSecurityException.class);
-
- assertThrows(Exception.class, () -> mTask.call());
-
- assertThat(mChunkListingStore.loadProto(TEST_PACKAGE_NAME).isPresent()).isFalse();
- }
-
- @Test
- public void call_incrementalThrowsPermanentException_clearsState() throws Exception {
- when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
- .thenReturn(Optional.of(mOldChunkListing));
- when(mEncryptedBackupTask.performIncrementalBackup(any(), any(), any()))
- .thenThrow(IOException.class);
-
- assertThrows(IOException.class, () -> mTask.call());
-
- verify(mChunkListingStore).deleteProto(TEST_PACKAGE_NAME);
- }
-
- @Test
- public void call_closesInputStream() throws Exception {
- mTask.call();
- verify(mInputStream).close();
- }
-
- @Test
- public void cancel_cancelsTask() throws Exception {
- mTask.cancel();
- verify(mEncryptedBackupTask).cancel();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java
deleted file mode 100644
index 0affacd..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-
-import static java.util.stream.Collectors.toList;
-
-import com.android.server.backup.encryption.FullRestoreDownloader;
-
-import com.google.common.io.Files;
-import com.google.common.primitives.Bytes;
-
-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.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-
-@RunWith(RobolectricTestRunner.class)
-public class EncryptedFullRestoreTaskTest {
- private static final int TEST_BUFFER_SIZE = 10;
- private static final byte[] TEST_ENCRYPTED_DATA = {1, 2, 3, 4, 5, 6};
- private static final byte[] TEST_DECRYPTED_DATA = fakeDecrypt(TEST_ENCRYPTED_DATA);
-
- @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- @Mock private BackupFileDecryptorTask mDecryptorTask;
-
- private File mFolder;
- private FakeFullRestoreDownloader mFullRestorePackageWrapper;
- private EncryptedFullRestoreTask mTask;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mFolder = temporaryFolder.newFolder();
- mFullRestorePackageWrapper = new FakeFullRestoreDownloader(TEST_ENCRYPTED_DATA);
-
- doAnswer(
- invocation -> {
- File source = invocation.getArgument(0);
- DecryptedChunkOutput target = invocation.getArgument(1);
- byte[] decrypted = fakeDecrypt(Files.toByteArray(source));
- target.open();
- target.processChunk(decrypted, decrypted.length);
- target.close();
- return null;
- })
- .when(mDecryptorTask)
- .decryptFile(any(), any());
-
- mTask = new EncryptedFullRestoreTask(mFolder, mFullRestorePackageWrapper, mDecryptorTask);
- }
-
- @Test
- public void readNextChunk_downloadsAndDecryptsBackup() throws Exception {
- ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
-
- byte[] buffer = new byte[TEST_BUFFER_SIZE];
- int bytesRead = mTask.readNextChunk(buffer);
- while (bytesRead != -1) {
- decryptedOutput.write(buffer, 0, bytesRead);
- bytesRead = mTask.readNextChunk(buffer);
- }
-
- assertThat(decryptedOutput.toByteArray()).isEqualTo(TEST_DECRYPTED_DATA);
- }
-
- @Test
- public void finish_deletesTemporaryFiles() throws Exception {
- mTask.readNextChunk(new byte[10]);
- mTask.finish(FullRestoreDownloader.FinishType.UNKNOWN_FINISH);
-
- assertThat(mFolder.listFiles()).isEmpty();
- }
-
- /** Fake package wrapper which returns data from a byte array. */
- private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
- private final ByteArrayInputStream mData;
-
- FakeFullRestoreDownloader(byte[] data) {
- // We override all methods of the superclass, so it does not require any collaborators.
- super();
- mData = new ByteArrayInputStream(data);
- }
-
- @Override
- public int readNextChunk(byte[] buffer) throws IOException {
- return mData.read(buffer);
- }
-
- @Override
- public void finish(FinishType finishType) {
- // Nothing to do.
- }
- }
-
- /** Fake decrypts a byte array by subtracting 1 from each byte. */
- private static byte[] fakeDecrypt(byte[] input) {
- return Bytes.toArray(Bytes.asList(input).stream().map(b -> b + 1).collect(toList()));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java
deleted file mode 100644
index 222b882..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertThrows;
-
-import android.app.Application;
-import android.util.Pair;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ProtoStore;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.TertiaryKeyManager;
-import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
-import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-import com.android.server.backup.testing.CryptoTestUtils;
-
-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.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import javax.crypto.SecretKey;
-
-
-@RunWith(RobolectricTestRunner.class)
-public class EncryptedKvBackupTaskTest {
- private static final boolean INCREMENTAL = true;
- private static final boolean NON_INCREMENTAL = false;
-
- private static final String TEST_PACKAGE_1 = "com.example.app1";
- private static final String TEST_KEY_1 = "key_1";
- private static final String TEST_KEY_2 = "key_2";
- private static final ChunkHash TEST_HASH_1 =
- new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
- private static final ChunkHash TEST_HASH_2 =
- new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
- private static final int TEST_LENGTH_1 = 200;
- private static final int TEST_LENGTH_2 = 300;
-
- @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-
- @Captor private ArgumentCaptor<ChunksMetadataProto.ChunkListing> mChunkListingCaptor;
-
- @Mock private TertiaryKeyManager mTertiaryKeyManager;
- @Mock private RecoverableKeyStoreSecondaryKey mSecondaryKey;
- @Mock private ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
- @Mock private ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
- @Mock private KvBackupEncrypter mKvBackupEncrypter;
- @Mock private EncryptedBackupTask mEncryptedBackupTask;
- @Mock private SecretKey mTertiaryKey;
-
- private WrappedKeyProto.WrappedKey mWrappedTertiaryKey;
- private KeyValueListingProto.KeyValueListing mNewKeyValueListing;
- private ChunksMetadataProto.ChunkListing mNewChunkListing;
- private EncryptedKvBackupTask mTask;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- Application application = ApplicationProvider.getApplicationContext();
- mKeyValueListingStore = ProtoStore.createKeyValueListingStore(application);
- mChunkListingStore = ProtoStore.createChunkListingStore(application);
-
- mWrappedTertiaryKey = new WrappedKeyProto.WrappedKey();
-
- when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(false);
- when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
- when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
-
- mNewKeyValueListing =
- createKeyValueListing(
- CryptoTestUtils.mapOf(
- new Pair<>(TEST_KEY_1, TEST_HASH_1),
- new Pair<>(TEST_KEY_2, TEST_HASH_2)));
- mNewChunkListing =
- createChunkListing(
- CryptoTestUtils.mapOf(
- new Pair<>(TEST_HASH_1, TEST_LENGTH_1),
- new Pair<>(TEST_HASH_2, TEST_LENGTH_2)));
- when(mKvBackupEncrypter.getNewKeyValueListing()).thenReturn(mNewKeyValueListing);
- when(mEncryptedBackupTask.performIncrementalBackup(
- eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
- .thenReturn(mNewChunkListing);
- when(mEncryptedBackupTask.performNonIncrementalBackup(
- eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
- .thenReturn(mNewChunkListing);
-
- mTask =
- new EncryptedKvBackupTask(
- mTertiaryKeyManager,
- mKeyValueListingStore,
- mSecondaryKey,
- mChunkListingStore,
- mKvBackupEncrypter,
- mEncryptedBackupTask,
- TEST_PACKAGE_1);
- }
-
- @Test
- public void testPerformBackup_rotationRequired_deletesListings() throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
- mChunkListingStore.saveProto(
- TEST_PACKAGE_1,
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
-
- when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
- // Throw an IOException so it aborts before saving the new listings.
- when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
- .thenThrow(IOException.class);
-
- assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
-
- assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- }
-
- @Test
- public void testPerformBackup_rotationRequiredButIncremental_throws() throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
- mChunkListingStore.saveProto(
- TEST_PACKAGE_1,
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
-
- when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
-
- assertThrows(NonIncrementalBackupRequiredException.class,
- () -> mTask.performBackup(INCREMENTAL));
- }
-
- @Test
- public void testPerformBackup_rotationRequiredAndNonIncremental_performsNonIncrementalBackup()
- throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
- mChunkListingStore.saveProto(
- TEST_PACKAGE_1,
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
-
- when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
-
- mTask.performBackup(NON_INCREMENTAL);
-
- verify(mEncryptedBackupTask)
- .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), any());
- }
-
- @Test
- public void testPerformBackup_existingStateButNonIncremental_deletesListings() throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
- mChunkListingStore.saveProto(
- TEST_PACKAGE_1,
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
-
- // Throw an IOException so it aborts before saving the new listings.
- when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
- .thenThrow(IOException.class);
-
- assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
-
- assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- }
-
- @Test
- public void testPerformBackup_keyValueListingMissing_deletesChunkListingAndPerformsNonIncremental()
- throws Exception {
- mChunkListingStore.saveProto(
- TEST_PACKAGE_1,
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
-
- // Throw an IOException so it aborts before saving the new listings.
- when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
- .thenThrow(IOException.class);
-
- assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
-
- verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
- assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- }
-
- @Test
- public void testPerformBackup_chunkListingMissing_deletesKeyValueListingAndPerformsNonIncremental()
- throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
-
- // Throw an IOException so it aborts before saving the new listings.
- when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
- .thenThrow(IOException.class);
-
- assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
-
- verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
- assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
- }
-
- @Test
- public void testPerformBackup_existingStateAndIncremental_performsIncrementalBackup()
- throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
- ChunksMetadataProto.ChunkListing oldChunkListing =
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1)));
- mChunkListingStore.saveProto(TEST_PACKAGE_1, oldChunkListing);
-
- mTask.performBackup(INCREMENTAL);
-
- verify(mEncryptedBackupTask)
- .performIncrementalBackup(
- eq(mTertiaryKey), eq(mWrappedTertiaryKey), mChunkListingCaptor.capture());
- assertChunkListingsEqual(mChunkListingCaptor.getValue(), oldChunkListing);
- }
-
- @Test
- public void testPerformBackup_noExistingStateAndNonIncremental_performsNonIncrementalBackup()
- throws Exception {
- mTask.performBackup(NON_INCREMENTAL);
-
- verify(mEncryptedBackupTask)
- .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(null));
- }
-
- @Test
- public void testPerformBackup_incremental_savesNewListings() throws Exception {
- mKeyValueListingStore.saveProto(
- TEST_PACKAGE_1,
- createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
- mChunkListingStore.saveProto(
- TEST_PACKAGE_1,
- createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
-
- mTask.performBackup(INCREMENTAL);
-
- KeyValueListingProto.KeyValueListing actualKeyValueListing =
- mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
- ChunksMetadataProto.ChunkListing actualChunkListing =
- mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
- assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
- assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
- }
-
- @Test
- public void testPerformBackup_nonIncremental_savesNewListings() throws Exception {
- mTask.performBackup(NON_INCREMENTAL);
-
- KeyValueListingProto.KeyValueListing actualKeyValueListing =
- mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
- ChunksMetadataProto.ChunkListing actualChunkListing =
- mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
- assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
- assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
- }
-
- private static KeyValueListingProto.KeyValueListing createKeyValueListing(
- Map<String, ChunkHash> pairs) {
- return new KeyValueListingBuilder().addAll(pairs).build();
- }
-
- private static ChunksMetadataProto.ChunkListing createChunkListing(
- Map<ChunkHash, Integer> chunks) {
- ChunksMetadataProto.Chunk[] listingChunks = new ChunksMetadataProto.Chunk[chunks.size()];
- int chunksAdded = 0;
- for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
- listingChunks[chunksAdded] = CryptoTestUtils.newChunk(entry.getKey(), entry.getValue());
- chunksAdded++;
- }
- return CryptoTestUtils.newChunkListingWithoutDocId(
- /* fingerprintSalt */ new byte[0],
- ChunksMetadataProto.AES_256_GCM,
- ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
- listingChunks);
- }
-
- private static void assertKeyValueListingsEqual(
- KeyValueListingProto.KeyValueListing actual,
- KeyValueListingProto.KeyValueListing expected) {
- KeyValueListingProto.KeyValueEntry[] actualEntries = actual.entries;
- KeyValueListingProto.KeyValueEntry[] expectedEntries = expected.entries;
- assertThat(actualEntries.length).isEqualTo(expectedEntries.length);
- for (int i = 0; i < actualEntries.length; i++) {
- assertWithMessage("entry " + i)
- .that(actualEntries[i].key)
- .isEqualTo(expectedEntries[i].key);
- assertWithMessage("entry " + i)
- .that(actualEntries[i].hash)
- .isEqualTo(expectedEntries[i].hash);
- }
- }
-
- private static void assertChunkListingsEqual(
- ChunksMetadataProto.ChunkListing actual, ChunksMetadataProto.ChunkListing expected) {
- ChunksMetadataProto.Chunk[] actualChunks = actual.chunks;
- ChunksMetadataProto.Chunk[] expectedChunks = expected.chunks;
- assertThat(actualChunks.length).isEqualTo(expectedChunks.length);
- for (int i = 0; i < actualChunks.length; i++) {
- assertWithMessage("chunk " + i)
- .that(actualChunks[i].hash)
- .isEqualTo(expectedChunks[i].hash);
- assertWithMessage("chunk " + i)
- .that(actualChunks[i].length)
- .isEqualTo(expectedChunks[i].length);
- }
- assertThat(actual.cipherType).isEqualTo(expected.cipherType);
- assertThat(actual.documentId)
- .isEqualTo(expected.documentId == null ? "" : expected.documentId);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java
deleted file mode 100644
index 6666d95..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.os.ParcelFileDescriptor;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ChunkHasher;
-import com.android.server.backup.testing.CryptoTestUtils;
-import com.android.server.testing.shadows.DataEntity;
-import com.android.server.testing.shadows.ShadowBackupDataOutput;
-
-import com.google.protobuf.nano.MessageNano;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-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.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-@Config(shadows = {ShadowBackupDataOutput.class})
-@RunWith(RobolectricTestRunner.class)
-public class EncryptedKvRestoreTaskTest {
- private static final String TEST_KEY_1 = "test_key_1";
- private static final String TEST_KEY_2 = "test_key_2";
- private static final String TEST_KEY_3 = "test_key_3";
- private static final byte[] TEST_VALUE_1 = {1, 2, 3};
- private static final byte[] TEST_VALUE_2 = {4, 5, 6};
- private static final byte[] TEST_VALUE_3 = {20, 25, 30, 35};
-
- @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- private File temporaryDirectory;
-
- @Mock private ParcelFileDescriptor mParcelFileDescriptor;
- @Mock private ChunkHasher mChunkHasher;
- @Mock private FullRestoreToFileTask mFullRestoreToFileTask;
- @Mock private BackupFileDecryptorTask mBackupFileDecryptorTask;
-
- private EncryptedKvRestoreTask task;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- when(mChunkHasher.computeHash(any()))
- .thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
- doAnswer(invocation -> writeTestPairsToFile(invocation.getArgument(0)))
- .when(mFullRestoreToFileTask)
- .restoreToFile(any());
- doAnswer(
- invocation ->
- readPairsFromFile(
- invocation.getArgument(0), invocation.getArgument(1)))
- .when(mBackupFileDecryptorTask)
- .decryptFile(any(), any());
-
- temporaryDirectory = temporaryFolder.newFolder();
- task =
- new EncryptedKvRestoreTask(
- temporaryDirectory,
- mChunkHasher,
- mFullRestoreToFileTask,
- mBackupFileDecryptorTask);
- }
-
- @Test
- public void testGetRestoreData_writesPairsToOutputInOrder() throws Exception {
- task.getRestoreData(mParcelFileDescriptor);
-
- assertThat(ShadowBackupDataOutput.getEntities())
- .containsExactly(
- new DataEntity(TEST_KEY_1, TEST_VALUE_1),
- new DataEntity(TEST_KEY_2, TEST_VALUE_2),
- new DataEntity(TEST_KEY_3, TEST_VALUE_3))
- .inOrder();
- }
-
- @Test
- public void testGetRestoreData_exceptionDuringDecryption_throws() throws Exception {
- doThrow(IOException.class).when(mBackupFileDecryptorTask).decryptFile(any(), any());
- assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
- }
-
- @Test
- public void testGetRestoreData_exceptionDuringDownload_throws() throws Exception {
- doThrow(IOException.class).when(mFullRestoreToFileTask).restoreToFile(any());
- assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
- }
-
- @Test
- public void testGetRestoreData_exceptionDuringDecryption_deletesTemporaryFiles() throws Exception {
- doThrow(InvalidKeyException.class).when(mBackupFileDecryptorTask).decryptFile(any(), any());
- assertThrows(InvalidKeyException.class, () -> task.getRestoreData(mParcelFileDescriptor));
- assertThat(temporaryDirectory.listFiles()).isEmpty();
- }
-
- @Test
- public void testGetRestoreData_exceptionDuringDownload_deletesTemporaryFiles() throws Exception {
- doThrow(IOException.class).when(mFullRestoreToFileTask).restoreToFile(any());
- assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
- assertThat(temporaryDirectory.listFiles()).isEmpty();
- }
-
- private static Void writeTestPairsToFile(File file) throws IOException {
- // Write the pairs out of order to check the task sorts them.
- Set<byte[]> pairs =
- new HashSet<>(
- Arrays.asList(
- createPair(TEST_KEY_1, TEST_VALUE_1),
- createPair(TEST_KEY_3, TEST_VALUE_3),
- createPair(TEST_KEY_2, TEST_VALUE_2)));
-
- try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
- oos.writeObject(pairs);
- }
- return null;
- }
-
- private static Void readPairsFromFile(File file, DecryptedChunkOutput decryptedChunkOutput)
- throws IOException, ClassNotFoundException, InvalidKeyException,
- NoSuchAlgorithmException {
- try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
- DecryptedChunkOutput output = decryptedChunkOutput.open()) {
- Set<byte[]> pairs = readPairs(ois);
- for (byte[] pair : pairs) {
- output.processChunk(pair, pair.length);
- }
- }
-
- return null;
- }
-
- private static byte[] createPair(String key, byte[] value) {
- return MessageNano.toByteArray(CryptoTestUtils.newPair(key, value));
- }
-
- @SuppressWarnings("unchecked") // deserialization.
- private static Set<byte[]> readPairs(ObjectInputStream ois)
- throws IOException, ClassNotFoundException {
- return (Set<byte[]>) ois.readObject();
- }
-
- private static ChunkHash fakeHash(byte[] data) {
- return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/FullRestoreToFileTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/FullRestoreToFileTaskTest.java
deleted file mode 100644
index de8b734..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/FullRestoreToFileTaskTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.Presubmit;
-
-import com.android.server.backup.encryption.FullRestoreDownloader;
-import com.android.server.backup.encryption.FullRestoreDownloader.FinishType;
-
-import com.google.common.io.Files;
-
-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.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.util.Random;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-public class FullRestoreToFileTaskTest {
- private static final int TEST_RANDOM_SEED = 34;
- private static final int TEST_MAX_CHUNK_SIZE_BYTES = 5;
- private static final int TEST_DATA_LENGTH_BYTES = TEST_MAX_CHUNK_SIZE_BYTES * 20;
-
- @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-
- private byte[] mTestData;
- private File mTargetFile;
- private FakeFullRestoreDownloader mFakeFullRestoreDownloader;
- @Mock private FullRestoreDownloader mMockFullRestoreDownloader;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mTargetFile = mTemporaryFolder.newFile();
-
- mTestData = new byte[TEST_DATA_LENGTH_BYTES];
- new Random(TEST_RANDOM_SEED).nextBytes(mTestData);
- mFakeFullRestoreDownloader = new FakeFullRestoreDownloader(mTestData);
- }
-
- private FullRestoreToFileTask createTaskWithFakeDownloader() {
- return new FullRestoreToFileTask(mFakeFullRestoreDownloader, TEST_MAX_CHUNK_SIZE_BYTES);
- }
-
- private FullRestoreToFileTask createTaskWithMockDownloader() {
- return new FullRestoreToFileTask(mMockFullRestoreDownloader, TEST_MAX_CHUNK_SIZE_BYTES);
- }
-
- @Test
- public void restoreToFile_readsDataAndWritesToFile() throws Exception {
- FullRestoreToFileTask task = createTaskWithFakeDownloader();
- task.restoreToFile(mTargetFile);
- assertThat(Files.toByteArray(mTargetFile)).isEqualTo(mTestData);
- }
-
- @Test
- public void restoreToFile_noErrors_closesDownloaderWithFinished() throws Exception {
- FullRestoreToFileTask task = createTaskWithMockDownloader();
- when(mMockFullRestoreDownloader.readNextChunk(any())).thenReturn(-1);
-
- task.restoreToFile(mTargetFile);
-
- verify(mMockFullRestoreDownloader).finish(FinishType.FINISHED);
- }
-
- @Test
- public void restoreToFile_ioException_closesDownloaderWithTransferFailure() throws Exception {
- FullRestoreToFileTask task = createTaskWithMockDownloader();
- when(mMockFullRestoreDownloader.readNextChunk(any())).thenThrow(IOException.class);
-
- assertThrows(IOException.class, () -> task.restoreToFile(mTargetFile));
-
- verify(mMockFullRestoreDownloader).finish(FinishType.TRANSFER_FAILURE);
- }
-
- /** Fake package wrapper which returns data from a byte array. */
- private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
-
- private final ByteArrayInputStream mData;
-
- FakeFullRestoreDownloader(byte[] data) {
- // We override all methods of the superclass, so it does not require any collaborators.
- super();
- this.mData = new ByteArrayInputStream(data);
- }
-
- @Override
- public int readNextChunk(byte[] buffer) throws IOException {
- return mData.read(buffer);
- }
-
- @Override
- public void finish(FinishType finishType) {
- // Do nothing.
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTaskTest.java
deleted file mode 100644
index 4a7ae03..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTaskTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * 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 android.security.keystore.recovery.RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.app.Application;
-import android.security.keystore.recovery.RecoveryController;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.testing.fakes.FakeCryptoBackupServer;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import java.security.InvalidKeyException;
-import java.security.SecureRandom;
-import java.util.Optional;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-@Config(shadows = {ShadowRecoveryController.class})
-@RunWith(RobolectricTestRunner.class)
-public class InitializeRecoverableSecondaryKeyTaskTest {
- @Mock private CryptoSettings mMockCryptoSettings;
-
- private Application mApplication;
- private InitializeRecoverableSecondaryKeyTask mTask;
- private CryptoSettings mCryptoSettings;
- private FakeCryptoBackupServer mFakeCryptoBackupServer;
- private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- ShadowRecoveryController.reset();
-
- mApplication = ApplicationProvider.getApplicationContext();
- mFakeCryptoBackupServer = new FakeCryptoBackupServer();
- mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
- mSecondaryKeyManager =
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(mApplication), new SecureRandom());
-
- mTask =
- new InitializeRecoverableSecondaryKeyTask(
- mApplication, mCryptoSettings, mSecondaryKeyManager, mFakeCryptoBackupServer);
- }
-
- @Test
- public void testRun_generatesNewKeyInRecoveryController() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mTask.run();
-
- assertThat(RecoveryController.getInstance(mApplication).getAliases())
- .contains(key.getAlias());
- }
-
- @Test
- public void testRun_setsAliasOnServer() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mTask.run();
-
- assertThat(mFakeCryptoBackupServer.getActiveSecondaryKeyAlias().get())
- .isEqualTo(key.getAlias());
- }
-
- @Test
- public void testRun_setsAliasInSettings() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(key.getAlias());
- }
-
- @Test
- public void testRun_initializesSettings() throws Exception {
- mTask.run();
-
- assertThat(mCryptoSettings.getIsInitialized()).isTrue();
- }
-
- @Test
- public void testRun_initializeSettingsFails_throws() throws Exception {
- useMockCryptoSettings();
- doThrow(IllegalArgumentException.class)
- .when(mMockCryptoSettings)
- .initializeWithKeyAlias(any());
-
-
- assertThrows(IllegalArgumentException.class, () -> mTask.run());
- }
-
- @Test
- public void testRun_doesNotGenerateANewKeyIfOneIsAvailable() throws Exception {
- RecoverableKeyStoreSecondaryKey key1 = mTask.run();
- RecoverableKeyStoreSecondaryKey key2 = mTask.run();
-
- assertThat(key1.getAlias()).isEqualTo(key2.getAlias());
- assertThat(key2.getSecretKey()).isEqualTo(key2.getSecretKey());
- }
-
- @Test
- public void testRun_existingKeyButDestroyed_throws() throws Exception {
- RecoverableKeyStoreSecondaryKey key = mTask.run();
- ShadowRecoveryController.setRecoveryStatus(
- key.getAlias(), RECOVERY_STATUS_PERMANENT_FAILURE);
-
- assertThrows(InvalidKeyException.class, () -> mTask.run());
- }
-
- @Test
- public void testRun_settingsInitializedButNotSecondaryKeyAlias_throws() {
- useMockCryptoSettings();
- when(mMockCryptoSettings.getIsInitialized()).thenReturn(true);
- when(mMockCryptoSettings.getActiveSecondaryKeyAlias()).thenReturn(Optional.empty());
-
- assertThrows(InvalidKeyException.class, () -> mTask.run());
- }
-
- @Test
- public void testRun_keyAliasSetButNotInStore_throws() {
- useMockCryptoSettings();
- when(mMockCryptoSettings.getIsInitialized()).thenReturn(true);
- when(mMockCryptoSettings.getActiveSecondaryKeyAlias())
- .thenReturn(Optional.of("missingAlias"));
-
- assertThrows(InvalidKeyException.class, () -> mTask.run());
- }
-
- private void useMockCryptoSettings() {
- mTask =
- new InitializeRecoverableSecondaryKeyTask(
- mApplication,
- mMockCryptoSettings,
- mSecondaryKeyManager,
- mFakeCryptoBackupServer);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/KvBackupEncrypterTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/KvBackupEncrypterTest.java
deleted file mode 100644
index ccfbfa4..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/KvBackupEncrypterTest.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.app.backup.BackupDataInput;
-import android.platform.test.annotations.Presubmit;
-import android.util.Pair;
-
-import com.android.server.backup.encryption.chunk.ChunkHash;
-import com.android.server.backup.encryption.chunking.ChunkHasher;
-import com.android.server.backup.encryption.chunking.EncryptedChunk;
-import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
-import com.android.server.backup.encryption.protos.nano.KeyValueListingProto.KeyValueListing;
-import com.android.server.backup.encryption.protos.nano.KeyValuePairProto.KeyValuePair;
-import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
-import com.android.server.testing.shadows.DataEntity;
-import com.android.server.testing.shadows.ShadowBackupDataInput;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Ordering;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.security.MessageDigest;
-import java.util.Arrays;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-@Config(shadows = {ShadowBackupDataInput.class})
-public class KvBackupEncrypterTest {
- private static final String KEY_ALGORITHM = "AES";
- private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
- private static final int GCM_TAG_LENGTH_BYTES = 16;
-
- private static final byte[] TEST_TERTIARY_KEY = Arrays.copyOf(new byte[0], 256 / Byte.SIZE);
- private static final String TEST_KEY_1 = "test_key_1";
- private static final String TEST_KEY_2 = "test_key_2";
- private static final String TEST_KEY_3 = "test_key_3";
- private static final byte[] TEST_VALUE_1 = {10, 11, 12};
- private static final byte[] TEST_VALUE_2 = {13, 14, 15};
- private static final byte[] TEST_VALUE_2B = {13, 14, 15, 16};
- private static final byte[] TEST_VALUE_3 = {16, 17, 18};
-
- private SecretKey mSecretKey;
- private ChunkHasher mChunkHasher;
-
- @Before
- public void setUp() {
- mSecretKey = new SecretKeySpec(TEST_TERTIARY_KEY, KEY_ALGORITHM);
- mChunkHasher = new ChunkHasher(mSecretKey);
-
- ShadowBackupDataInput.reset();
- }
-
- private KvBackupEncrypter createEncrypter(KeyValueListing keyValueListing) {
- KvBackupEncrypter encrypter = new KvBackupEncrypter(new BackupDataInput(null));
- encrypter.setOldKeyValueListing(keyValueListing);
- return encrypter;
- }
-
- @Test
- public void backup_noExistingBackup_encryptsAllPairs() throws Exception {
- ShadowBackupDataInput.addEntity(TEST_KEY_1, TEST_VALUE_1);
- ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2);
-
- KeyValueListing emptyKeyValueListing = new KeyValueListingBuilder().build();
- ImmutableSet<ChunkHash> emptyExistingChunks = ImmutableSet.of();
- KvBackupEncrypter encrypter = createEncrypter(emptyKeyValueListing);
-
- Result result =
- encrypter.backup(
- mSecretKey, /*unusedFingerprintMixerSalt=*/ null, emptyExistingChunks);
-
- assertThat(result.getAllChunks()).hasSize(2);
- EncryptedChunk chunk1 = result.getNewChunks().get(0);
- EncryptedChunk chunk2 = result.getNewChunks().get(1);
- assertThat(chunk1.key()).isEqualTo(getChunkHash(TEST_KEY_1, TEST_VALUE_1));
- KeyValuePair pair1 = decryptChunk(chunk1);
- assertThat(pair1.key).isEqualTo(TEST_KEY_1);
- assertThat(pair1.value).isEqualTo(TEST_VALUE_1);
- assertThat(chunk2.key()).isEqualTo(getChunkHash(TEST_KEY_2, TEST_VALUE_2));
- KeyValuePair pair2 = decryptChunk(chunk2);
- assertThat(pair2.key).isEqualTo(TEST_KEY_2);
- assertThat(pair2.value).isEqualTo(TEST_VALUE_2);
- }
-
- @Test
- public void backup_existingBackup_encryptsNewAndUpdatedPairs() throws Exception {
- Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
-
- // Update key 2 and add the new key 3.
- ShadowBackupDataInput.reset();
- ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2B);
- ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
-
- KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
- BackupEncrypter.Result secondResult =
- encrypter.backup(
- mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
-
- assertThat(secondResult.getAllChunks()).hasSize(3);
- assertThat(secondResult.getNewChunks()).hasSize(2);
- EncryptedChunk newChunk2 = secondResult.getNewChunks().get(0);
- EncryptedChunk newChunk3 = secondResult.getNewChunks().get(1);
- assertThat(newChunk2.key()).isEqualTo(getChunkHash(TEST_KEY_2, TEST_VALUE_2B));
- assertThat(decryptChunk(newChunk2).value).isEqualTo(TEST_VALUE_2B);
- assertThat(newChunk3.key()).isEqualTo(getChunkHash(TEST_KEY_3, TEST_VALUE_3));
- assertThat(decryptChunk(newChunk3).value).isEqualTo(TEST_VALUE_3);
- }
-
- @Test
- public void backup_allChunksContainsHashesOfAllChunks() throws Exception {
- Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
-
- ShadowBackupDataInput.reset();
- ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
-
- KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
- BackupEncrypter.Result secondResult =
- encrypter.backup(
- mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
-
- assertThat(secondResult.getAllChunks())
- .containsExactly(
- getChunkHash(TEST_KEY_1, TEST_VALUE_1),
- getChunkHash(TEST_KEY_2, TEST_VALUE_2),
- getChunkHash(TEST_KEY_3, TEST_VALUE_3));
- }
-
- @Test
- public void backup_negativeSize_deletesKeyFromExistingBackup() throws Exception {
- Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
-
- ShadowBackupDataInput.reset();
- ShadowBackupDataInput.addEntity(new DataEntity(TEST_KEY_2));
-
- KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
- Result secondResult =
- encrypter.backup(
- mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
-
- assertThat(secondResult.getAllChunks())
- .containsExactly(getChunkHash(TEST_KEY_1, TEST_VALUE_1));
- assertThat(secondResult.getNewChunks()).isEmpty();
- }
-
- @Test
- public void backup_returnsMessageDigestOverChunkHashes() throws Exception {
- Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
-
- ShadowBackupDataInput.reset();
- ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
-
- KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
- Result secondResult =
- encrypter.backup(
- mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
-
- MessageDigest messageDigest =
- MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
- ImmutableList<ChunkHash> sortedHashes =
- Ordering.natural()
- .immutableSortedCopy(
- ImmutableList.of(
- getChunkHash(TEST_KEY_1, TEST_VALUE_1),
- getChunkHash(TEST_KEY_2, TEST_VALUE_2),
- getChunkHash(TEST_KEY_3, TEST_VALUE_3)));
- messageDigest.update(sortedHashes.get(0).getHash());
- messageDigest.update(sortedHashes.get(1).getHash());
- messageDigest.update(sortedHashes.get(2).getHash());
- assertThat(secondResult.getDigest()).isEqualTo(messageDigest.digest());
- }
-
- @Test
- public void getNewKeyValueListing_noExistingBackup_returnsCorrectListing() throws Exception {
- KeyValueListing keyValueListing = runInitialBackupOfPairs1And2().first;
-
- assertThat(keyValueListing.entries.length).isEqualTo(2);
- assertThat(keyValueListing.entries[0].key).isEqualTo(TEST_KEY_1);
- assertThat(keyValueListing.entries[0].hash)
- .isEqualTo(getChunkHash(TEST_KEY_1, TEST_VALUE_1).getHash());
- assertThat(keyValueListing.entries[1].key).isEqualTo(TEST_KEY_2);
- assertThat(keyValueListing.entries[1].hash)
- .isEqualTo(getChunkHash(TEST_KEY_2, TEST_VALUE_2).getHash());
- }
-
- @Test
- public void getNewKeyValueListing_existingBackup_returnsCorrectListing() throws Exception {
- Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
-
- ShadowBackupDataInput.reset();
- ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2B);
- ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
-
- KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
- encrypter.backup(mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
-
- ImmutableMap<String, ChunkHash> keyValueListing =
- listingToMap(encrypter.getNewKeyValueListing());
- assertThat(keyValueListing).hasSize(3);
- assertThat(keyValueListing)
- .containsEntry(TEST_KEY_1, getChunkHash(TEST_KEY_1, TEST_VALUE_1));
- assertThat(keyValueListing)
- .containsEntry(TEST_KEY_2, getChunkHash(TEST_KEY_2, TEST_VALUE_2B));
- assertThat(keyValueListing)
- .containsEntry(TEST_KEY_3, getChunkHash(TEST_KEY_3, TEST_VALUE_3));
- }
-
- @Test
- public void getNewKeyValueChunkListing_beforeBackup_throws() throws Exception {
- KvBackupEncrypter encrypter = createEncrypter(new KeyValueListing());
- assertThrows(IllegalStateException.class, encrypter::getNewKeyValueListing);
- }
-
- private ImmutableMap<String, ChunkHash> listingToMap(KeyValueListing listing) {
- // We can't use the ImmutableMap collector directly because it isn't supported in Android
- // guava.
- return ImmutableMap.copyOf(
- Arrays.stream(listing.entries)
- .collect(
- Collectors.toMap(
- entry -> entry.key, entry -> new ChunkHash(entry.hash))));
- }
-
- private Pair<KeyValueListing, Set<ChunkHash>> runInitialBackupOfPairs1And2() throws Exception {
- ShadowBackupDataInput.addEntity(TEST_KEY_1, TEST_VALUE_1);
- ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2);
-
- KeyValueListing initialKeyValueListing = new KeyValueListingBuilder().build();
- ImmutableSet<ChunkHash> initialExistingChunks = ImmutableSet.of();
- KvBackupEncrypter encrypter = createEncrypter(initialKeyValueListing);
- Result firstResult =
- encrypter.backup(
- mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialExistingChunks);
-
- return Pair.create(
- encrypter.getNewKeyValueListing(), ImmutableSet.copyOf(firstResult.getAllChunks()));
- }
-
- private ChunkHash getChunkHash(String key, byte[] value) throws Exception {
- KeyValuePair pair = new KeyValuePair();
- pair.key = key;
- pair.value = Arrays.copyOf(value, value.length);
- return mChunkHasher.computeHash(KeyValuePair.toByteArray(pair));
- }
-
- private KeyValuePair decryptChunk(EncryptedChunk encryptedChunk) throws Exception {
- Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- cipher.init(
- Cipher.DECRYPT_MODE,
- mSecretKey,
- new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * Byte.SIZE, encryptedChunk.nonce()));
- byte[] decryptedBytes = cipher.doFinal(encryptedChunk.encryptedBytes());
- return KeyValuePair.parseFrom(decryptedBytes);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java
deleted file mode 100644
index cda7317..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertFalse;
-
-import android.app.Application;
-import android.platform.test.annotations.Presubmit;
-import android.security.keystore.recovery.RecoveryController;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.keys.KeyWrapUtils;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.backup.encryption.keys.TertiaryKeyStore;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-import com.android.server.testing.fakes.FakeCryptoBackupServer;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import java.security.SecureRandom;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.crypto.SecretKey;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-@Config(shadows = {ShadowRecoveryController.class, ShadowRecoveryController.class})
-public class RotateSecondaryKeyTaskTest {
- private static final String APP_1 = "app1";
- private static final String APP_2 = "app2";
- private static final String APP_3 = "app3";
-
- private static final String CURRENT_SECONDARY_KEY_ALIAS =
- "recoverablekey.alias/d524796bd07de3c2225c63d434eff698";
- private static final String NEXT_SECONDARY_KEY_ALIAS =
- "recoverablekey.alias/6c6d198a7f12e662b6bc45f4849db170";
-
- private Application mApplication;
- private RotateSecondaryKeyTask mTask;
- private RecoveryController mRecoveryController;
- private FakeCryptoBackupServer mBackupServer;
- private CryptoSettings mCryptoSettings;
- private Map<String, SecretKey> mTertiaryKeysByPackageName;
- private RecoverableKeyStoreSecondaryKeyManager mRecoverableSecondaryKeyManager;
-
- @Before
- public void setUp() throws Exception {
- mApplication = ApplicationProvider.getApplicationContext();
-
- mTertiaryKeysByPackageName = new HashMap<>();
- mTertiaryKeysByPackageName.put(APP_1, generateAesKey());
- mTertiaryKeysByPackageName.put(APP_2, generateAesKey());
- mTertiaryKeysByPackageName.put(APP_3, generateAesKey());
-
- mRecoveryController = RecoveryController.getInstance(mApplication);
- mRecoverableSecondaryKeyManager =
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(mApplication), new SecureRandom());
- mBackupServer = new FakeCryptoBackupServer();
- mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
- addNextSecondaryKeyToRecoveryController();
- mCryptoSettings.setNextSecondaryAlias(NEXT_SECONDARY_KEY_ALIAS);
-
- mTask =
- new RotateSecondaryKeyTask(
- mApplication,
- mRecoverableSecondaryKeyManager,
- mBackupServer,
- mCryptoSettings,
- mRecoveryController);
-
- ShadowRecoveryController.reset();
- }
-
- @Test
- public void run_failsIfThereIsNoActiveSecondaryKey() throws Exception {
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertFalse(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent());
- }
-
- @Test
- public void run_failsIfActiveSecondaryIsNotInRecoveryController() throws Exception {
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- // Have to add it first as otherwise CryptoSettings throws an exception when trying to set
- // it
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
- .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_doesNothingIfFlagIsDisabled() throws Exception {
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
- addWrappedTertiaries();
-
- mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
- .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_setsActiveSecondary() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
- addWrappedTertiaries();
-
- mTask.run();
-
- assertThat(mBackupServer.getActiveSecondaryKeyAlias().get())
- .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_rewrapsExistingTertiaryKeys() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
- addWrappedTertiaries();
-
- mTask.run();
-
- Map<String, WrappedKeyProto.WrappedKey> rewrappedKeys =
- mBackupServer.getAllTertiaryKeys(NEXT_SECONDARY_KEY_ALIAS);
- SecretKey secondaryKey = (SecretKey) mRecoveryController.getKey(NEXT_SECONDARY_KEY_ALIAS);
- for (String packageName : mTertiaryKeysByPackageName.keySet()) {
- WrappedKeyProto.WrappedKey rewrappedKey = rewrappedKeys.get(packageName);
- assertThat(KeyWrapUtils.unwrap(secondaryKey, rewrappedKey))
- .isEqualTo(mTertiaryKeysByPackageName.get(packageName));
- }
- }
-
- @Test
- public void run_persistsRewrappedKeysToDisk() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
- addWrappedTertiaries();
-
- mTask.run();
-
- RecoverableKeyStoreSecondaryKey secondaryKey = getRecoverableKey(NEXT_SECONDARY_KEY_ALIAS);
- Map<String, SecretKey> keys =
- TertiaryKeyStore.newInstance(mApplication, secondaryKey).getAll();
- for (String packageName : mTertiaryKeysByPackageName.keySet()) {
- SecretKey tertiaryKey = mTertiaryKeysByPackageName.get(packageName);
- SecretKey newlyWrappedKey = keys.get(packageName);
- assertThat(tertiaryKey.getEncoded()).isEqualTo(newlyWrappedKey.getEncoded());
- }
- }
-
- @Test
- public void run_stillSetsActiveSecondaryIfNoTertiaries() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mBackupServer.getActiveSecondaryKeyAlias().get())
- .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_setsActiveSecondaryKeyAliasInSettings() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
- .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_removesNextSecondaryKeyAliasInSettings() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
- }
-
- @Test
- public void run_deletesOldKeyFromRecoverableKeyStoreLoader() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mRecoveryController.getKey(CURRENT_SECONDARY_KEY_ALIAS)).isNull();
- }
-
- @Test
- public void run_doesNotRotateIfNoNextAlias() throws Exception {
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
- mCryptoSettings.removeNextSecondaryKeyAlias();
-
- mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
- .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
- assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
- }
-
- @Test
- public void run_doesNotRotateIfKeyIsNotSyncedYet() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
- .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_doesNotClearNextKeyIfSyncIsJustPending() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get())
- .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_doesNotRotateIfPermanentFailure() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
- .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
- }
-
- @Test
- public void run_removesNextKeyIfPermanentFailure() throws Exception {
- addNextSecondaryKeyToRecoveryController();
- setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
- addCurrentSecondaryKeyToRecoveryController();
- mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
- mBackupServer.setActiveSecondaryKeyAlias(
- CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
-
- mTask.run();
-
- assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
- }
-
- private void setNextKeyRecoveryStatus(int status) throws Exception {
- mRecoveryController.setRecoveryStatus(NEXT_SECONDARY_KEY_ALIAS, status);
- }
-
- private void addCurrentSecondaryKeyToRecoveryController() throws Exception {
- mRecoveryController.generateKey(CURRENT_SECONDARY_KEY_ALIAS);
- }
-
- private void addNextSecondaryKeyToRecoveryController() throws Exception {
- mRecoveryController.generateKey(NEXT_SECONDARY_KEY_ALIAS);
- }
-
- private void addWrappedTertiaries() throws Exception {
- TertiaryKeyStore tertiaryKeyStore =
- TertiaryKeyStore.newInstance(
- mApplication, getRecoverableKey(CURRENT_SECONDARY_KEY_ALIAS));
-
- for (String packageName : mTertiaryKeysByPackageName.keySet()) {
- tertiaryKeyStore.save(packageName, mTertiaryKeysByPackageName.get(packageName));
- }
- }
-
- private RecoverableKeyStoreSecondaryKey getRecoverableKey(String alias) throws Exception {
- return mRecoverableSecondaryKeyManager.get(alias).get();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java
deleted file mode 100644
index 4ac4fa8..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.Presubmit;
-import android.security.keystore.recovery.RecoveryController;
-
-import com.android.server.backup.encryption.CryptoSettings;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
-import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
-import com.android.server.testing.shadows.ShadowRecoveryController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import java.security.SecureRandom;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowRecoveryController.class})
-@Presubmit
-public class StartSecondaryKeyRotationTaskTest {
-
- private CryptoSettings mCryptoSettings;
- private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
- private StartSecondaryKeyRotationTask mStartSecondaryKeyRotationTask;
-
- @Before
- public void setUp() throws Exception {
- mSecondaryKeyManager =
- new RecoverableKeyStoreSecondaryKeyManager(
- RecoveryController.getInstance(RuntimeEnvironment.application),
- new SecureRandom());
- mCryptoSettings = CryptoSettings.getInstanceForTesting(RuntimeEnvironment.application);
- mStartSecondaryKeyRotationTask =
- new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager);
-
- ShadowRecoveryController.reset();
- }
-
- @Test
- public void run_doesNothingIfNoActiveSecondaryExists() {
- mStartSecondaryKeyRotationTask.run();
-
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
- }
-
- @Test
- public void run_doesNotRemoveExistingNextSecondaryKeyIfItIsAlreadyActive() throws Exception {
- generateAnActiveKey();
- String activeAlias = mCryptoSettings.getActiveSecondaryKeyAlias().get();
- mCryptoSettings.setNextSecondaryAlias(activeAlias);
-
- mStartSecondaryKeyRotationTask.run();
-
- assertThat(mSecondaryKeyManager.get(activeAlias).isPresent()).isTrue();
- }
-
- @Test
- public void run_doesRemoveExistingNextSecondaryKeyIfItIsNotYetActive() throws Exception {
- generateAnActiveKey();
- RecoverableKeyStoreSecondaryKey nextKey = mSecondaryKeyManager.generate();
- String nextAlias = nextKey.getAlias();
- mCryptoSettings.setNextSecondaryAlias(nextAlias);
-
- mStartSecondaryKeyRotationTask.run();
-
- assertThat(mSecondaryKeyManager.get(nextAlias).isPresent()).isFalse();
- }
-
- @Test
- public void run_generatesANewNextSecondaryKey() throws Exception {
- generateAnActiveKey();
-
- mStartSecondaryKeyRotationTask.run();
-
- assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isTrue();
- }
-
- @Test
- public void run_generatesANewKeyThatExistsInKeyStore() throws Exception {
- generateAnActiveKey();
-
- mStartSecondaryKeyRotationTask.run();
-
- String nextAlias = mCryptoSettings.getNextSecondaryKeyAlias().get();
- assertThat(mSecondaryKeyManager.get(nextAlias).isPresent()).isTrue();
- }
-
- private void generateAnActiveKey() throws Exception {
- RecoverableKeyStoreSecondaryKey secondaryKey = mSecondaryKeyManager.generate();
- mCryptoSettings.setActiveSecondaryKeyAlias(secondaryKey.getAlias());
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/testing/DiffScriptProcessor.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/testing/DiffScriptProcessor.java
deleted file mode 100644
index 7e97924..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/testing/DiffScriptProcessor.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * 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.testing;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.RandomAccessFile;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Scanner;
-import java.util.regex.Pattern;
-
-/**
- * To be used as part of a fake backup server. Processes a Scotty diff script.
- *
- * <p>A Scotty diff script consists of an ASCII line denoting a command, optionally followed by a
- * range of bytes. Command format is either
- *
- * <ul>
- * <li>A single 64-bit integer, followed by a new line: this denotes that the given number of
- * bytes are to follow in the stream. These bytes should be written directly to the new file.
- * <li>Two 64-bit integers, separated by a hyphen, followed by a new line: this says that the
- * given range of bytes from the original file ought to be copied into the new file.
- * </ul>
- */
-public class DiffScriptProcessor {
-
- private static final int COPY_BUFFER_SIZE = 1024;
-
- private static final String READ_MODE = "r";
- private static final Pattern VALID_COMMAND_PATTERN = Pattern.compile("^\\d+(-\\d+)?$");
-
- private final File mInput;
- private final File mOutput;
- private final long mInputLength;
-
- /**
- * A new instance, with {@code input} as previous file, and {@code output} as new file.
- *
- * @param input Previous file from which ranges of bytes are to be copied. This file should be
- * immutable.
- * @param output Output file, to which the new data should be written.
- * @throws IllegalArgumentException if input does not exist.
- */
- public DiffScriptProcessor(File input, File output) {
- checkArgument(input.exists(), "input file did not exist.");
- mInput = input;
- mInputLength = input.length();
- mOutput = Objects.requireNonNull(output);
- }
-
- public void process(InputStream diffScript) throws IOException, MalformedDiffScriptException {
- RandomAccessFile randomAccessInput = new RandomAccessFile(mInput, READ_MODE);
-
- try (FileOutputStream outputStream = new FileOutputStream(mOutput)) {
- while (true) {
- Optional<String> commandString = readCommand(diffScript);
- if (!commandString.isPresent()) {
- return;
- }
- Command command = Command.parse(commandString.get());
-
- if (command.mIsRange) {
- checkFileRange(command.mCount, command.mLimit);
- copyRange(randomAccessInput, outputStream, command.mCount, command.mLimit);
- } else {
- long bytesCopied = copyBytes(diffScript, outputStream, command.mCount);
- if (bytesCopied < command.mCount) {
- throw new MalformedDiffScriptException(
- String.format(
- Locale.US,
- "Command to copy %d bytes from diff script, but only %d"
- + " bytes available",
- command.mCount,
- bytesCopied));
- }
- if (diffScript.read() != '\n') {
- throw new MalformedDiffScriptException("Expected new line after bytes.");
- }
- }
- }
- }
- }
-
- private void checkFileRange(long start, long end) throws MalformedDiffScriptException {
- if (end < start) {
- throw new MalformedDiffScriptException(
- String.format(
- Locale.US,
- "Command to copy %d-%d bytes from original file, but %2$d < %1$d.",
- start,
- end));
- }
-
- if (end >= mInputLength) {
- throw new MalformedDiffScriptException(
- String.format(
- Locale.US,
- "Command to copy %d-%d bytes from original file, but file is only %d"
- + " bytes long.",
- start,
- end,
- mInputLength));
- }
- }
-
- /**
- * Reads a command from the input stream.
- *
- * @param inputStream The input.
- * @return Optional of command, or empty if EOF.
- */
- private static Optional<String> readCommand(InputStream inputStream) throws IOException {
- ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-
- int b;
- while (!isEndOfCommand(b = inputStream.read())) {
- byteArrayOutputStream.write(b);
- }
-
- byte[] bytes = byteArrayOutputStream.toByteArray();
- if (bytes.length == 0) {
- return Optional.empty();
- } else {
- return Optional.of(new String(bytes, UTF_8));
- }
- }
-
- /**
- * If the given output from {@link InputStream#read()} is the end of a command - i.e., a new
- * line or the EOF.
- *
- * @param b The byte or -1.
- * @return {@code true} if ends the command.
- */
- private static boolean isEndOfCommand(int b) {
- return b == -1 || b == '\n';
- }
-
- /**
- * Copies {@code n} bytes from {@code inputStream} to {@code outputStream}.
- *
- * @return The number of bytes copied.
- * @throws IOException if there was a problem reading or writing.
- */
- private static long copyBytes(InputStream inputStream, OutputStream outputStream, long n)
- throws IOException {
- byte[] buffer = new byte[COPY_BUFFER_SIZE];
- long copied = 0;
- while (n - copied > COPY_BUFFER_SIZE) {
- long read = copyBlock(inputStream, outputStream, buffer, COPY_BUFFER_SIZE);
- if (read <= 0) {
- return copied;
- }
- }
- while (n - copied > 0) {
- copied += copyBlock(inputStream, outputStream, buffer, (int) (n - copied));
- }
- return copied;
- }
-
- private static long copyBlock(
- InputStream inputStream, OutputStream outputStream, byte[] buffer, int size)
- throws IOException {
- int read = inputStream.read(buffer, 0, size);
- outputStream.write(buffer, 0, read);
- return read;
- }
-
- /**
- * Copies the given range of bytes from the input file to the output stream.
- *
- * @param input The input file.
- * @param output The output stream.
- * @param start Start position in the input file.
- * @param end End position in the output file (inclusive).
- * @throws IOException if there was a problem reading or writing.
- */
- private static void copyRange(RandomAccessFile input, OutputStream output, long start, long end)
- throws IOException {
- input.seek(start);
-
- // Inefficient but obviously correct. If tests become slow, optimize.
- for (; start <= end; start++) {
- output.write(input.read());
- }
- }
-
- /** Error thrown for a malformed diff script. */
- public static class MalformedDiffScriptException extends Exception {
- public MalformedDiffScriptException(String message) {
- super(message);
- }
- }
-
- /**
- * A command telling the processor either to insert n bytes, which follow, or copy n-m bytes
- * from the original file.
- */
- private static class Command {
- private final long mCount;
- private final long mLimit;
- private final boolean mIsRange;
-
- private Command(long count, long limit, boolean isRange) {
- mCount = count;
- mLimit = limit;
- mIsRange = isRange;
- }
-
- /**
- * Attempts to parse the command string into a usable structure.
- *
- * @param command The command string, without a new line at the end.
- * @throws MalformedDiffScriptException if the command is not a valid diff script command.
- * @return The parsed command.
- */
- private static Command parse(String command) throws MalformedDiffScriptException {
- if (!VALID_COMMAND_PATTERN.matcher(command).matches()) {
- throw new MalformedDiffScriptException("Bad command: " + command);
- }
-
- Scanner commandScanner = new Scanner(command);
- commandScanner.useDelimiter("-");
- long n = commandScanner.nextLong();
- if (!commandScanner.hasNextLong()) {
- return new Command(n, 0L, /*isRange=*/ false);
- }
- long m = commandScanner.nextLong();
- return new Command(n, m, /*isRange=*/ true);
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/testing/QueuingNonAutomaticExecutorService.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/testing/QueuingNonAutomaticExecutorService.java
deleted file mode 100644
index 9d2272e..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/testing/QueuingNonAutomaticExecutorService.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.testing;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.AbstractExecutorService;
-import java.util.concurrent.TimeUnit;
-
-/**
- * ExecutorService which needs to be stepped through the jobs in its' queue.
- *
- * <p>This is a deliberately simple implementation because it's only used in testing. The queued
- * jobs are run on the main thread to eliminate any race condition bugs.
- */
-public class QueuingNonAutomaticExecutorService extends AbstractExecutorService {
-
- private List<Runnable> mWaitingJobs = new ArrayList<>();
- private int mWaitingJobCount = 0;
-
- @Override
- public void shutdown() {
- mWaitingJobCount = mWaitingJobs.size();
- mWaitingJobs = null; // This will force an error if jobs are submitted after shutdown
- }
-
- @Override
- public List<Runnable> shutdownNow() {
- List<Runnable> queuedJobs = mWaitingJobs;
- shutdown();
- return queuedJobs;
- }
-
- @Override
- public boolean isShutdown() {
- return mWaitingJobs == null;
- }
-
- @Override
- public boolean isTerminated() {
- return mWaitingJobs == null && mWaitingJobCount == 0;
- }
-
- @Override
- public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
- long expiry = System.currentTimeMillis() + unit.toMillis(timeout);
- for (Runnable job : mWaitingJobs) {
- if (System.currentTimeMillis() > expiry) {
- return false;
- }
-
- job.run();
- }
- return true;
- }
-
- @Override
- public void execute(Runnable command) {
- mWaitingJobs.add(command);
- }
-
- public void runNext() {
- if (mWaitingJobs.isEmpty()) {
- throw new IllegalStateException("Attempted to run jobs on an empty paused executor");
- }
-
- mWaitingJobs.remove(0).run();
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/testing/RandomInputStream.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/testing/RandomInputStream.java
deleted file mode 100644
index 998da0b..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/testing/RandomInputStream.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.testing;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Random;
-
-/** {@link InputStream} that generates random bytes up to a given length. For testing purposes. */
-public class RandomInputStream extends InputStream {
- private static final int BYTE_MAX_VALUE = 255;
-
- private final Random mRandom;
- private final int mSizeBytes;
- private int mBytesRead;
-
- /**
- * A new instance, generating {@code sizeBytes} from {@code random} as a source.
- *
- * @param random Source of random bytes.
- * @param sizeBytes The number of bytes to generate before closing the stream.
- */
- public RandomInputStream(Random random, int sizeBytes) {
- mRandom = random;
- mSizeBytes = sizeBytes;
- mBytesRead = 0;
- }
-
- @Override
- public int read() throws IOException {
- if (isFinished()) {
- return -1;
- }
- mBytesRead++;
- return mRandom.nextInt(BYTE_MAX_VALUE);
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- checkArgument(off + len <= b.length);
- if (isFinished()) {
- return -1;
- }
- int length = Math.min(len, mSizeBytes - mBytesRead);
- int end = off + length;
-
- for (int i = off; i < end; ) {
- for (int rnd = mRandom.nextInt(), n = Math.min(end - i, Integer.SIZE / Byte.SIZE);
- n-- > 0;
- rnd >>= Byte.SIZE) {
- b[i++] = (byte) rnd;
- }
- }
-
- mBytesRead += length;
- return length;
- }
-
- private boolean isFinished() {
- return mBytesRead >= mSizeBytes;
- }
-}
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
deleted file mode 100644
index b0c02ba..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.backup.testing;
-
-import android.util.Pair;
-
-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.HashMap;
-import java.util.Map;
-import java.util.Random;
-
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-
-/** Helpers for crypto code tests. */
-public class CryptoTestUtils {
- private static final String KEY_ALGORITHM = "AES";
- private static final int KEY_SIZE_BITS = 256;
-
- private CryptoTestUtils() {}
-
- public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- keyGenerator.init(KEY_SIZE_BITS);
- return keyGenerator.generateKey();
- }
-
- /** Generates a byte array of size {@code n} containing random bytes. */
- public static byte[] generateRandomBytes(int n) {
- byte[] bytes = new byte[n];
- Random random = new Random();
- random.nextBytes(bytes);
- return bytes;
- }
-
- public static ChunksMetadataProto.Chunk newChunk(ChunkHash hash, int length) {
- return newChunk(hash.getHash(), length);
- }
-
- public static ChunksMetadataProto.Chunk newChunk(byte[] hash, int length) {
- ChunksMetadataProto.Chunk newChunk = new ChunksMetadataProto.Chunk();
- newChunk.hash = Arrays.copyOf(hash, hash.length);
- newChunk.length = length;
- return newChunk;
- }
-
- public static ChunksMetadataProto.ChunkListing newChunkListing(
- String docId,
- byte[] fingerprintSalt,
- int cipherType,
- int orderingType,
- ChunksMetadataProto.Chunk... chunks) {
- ChunksMetadataProto.ChunkListing chunkListing =
- newChunkListingWithoutDocId(fingerprintSalt, cipherType, orderingType, chunks);
- chunkListing.documentId = docId;
- return chunkListing;
- }
-
- public static ChunksMetadataProto.ChunkListing newChunkListingWithoutDocId(
- byte[] fingerprintSalt,
- int cipherType,
- int orderingType,
- ChunksMetadataProto.Chunk... chunks) {
- ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
- chunkListing.fingerprintMixerSalt =
- fingerprintSalt == null
- ? null
- : Arrays.copyOf(fingerprintSalt, fingerprintSalt.length);
- chunkListing.cipherType = cipherType;
- chunkListing.chunkOrderingType = orderingType;
- chunkListing.chunks = chunks;
- return chunkListing;
- }
-
- public static ChunksMetadataProto.ChunkOrdering newChunkOrdering(
- int[] starts, byte[] checksum) {
- ChunksMetadataProto.ChunkOrdering chunkOrdering = new ChunksMetadataProto.ChunkOrdering();
- 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;
- if (original.chunks == null) {
- clonedChunks = null;
- } else {
- clonedChunks = new ChunksMetadataProto.Chunk[original.chunks.length];
- for (int i = 0; i < original.chunks.length; i++) {
- clonedChunks[i] = clone(original.chunks[i]);
- }
- }
-
- return newChunkListing(
- original.documentId,
- original.fingerprintMixerSalt,
- original.cipherType,
- original.chunkOrderingType,
- clonedChunks);
- }
-
- 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;
- }
-
- public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
- Map<K, V> map = new HashMap<>();
- for (Pair<K, V> pair : pairs) {
- map.put(pair.first, pair.second);
- }
- return map;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/TestFileUtils.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/TestFileUtils.java
deleted file mode 100644
index e5d73ba..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/TestFileUtils.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.
- */
-
-import com.google.common.io.ByteStreams;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-
-/** Utility methods for use in tests */
-public class TestFileUtils {
- /** Read the contents of a file into a byte array */
- public static byte[] toByteArray(File file) throws IOException {
- try (FileInputStream fis = new FileInputStream(file)) {
- return ByteStreams.toByteArray(fis);
- }
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java
deleted file mode 100644
index 3329060..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.testing.fakes;
-
-import android.annotation.Nullable;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.backup.encryption.client.CryptoBackupServer;
-import com.android.server.backup.encryption.client.UnexpectedActiveSecondaryOnServerException;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Optional;
-
-/** Fake {@link CryptoBackupServer}, for tests. Stores tertiary keys in memory. */
-public class FakeCryptoBackupServer implements CryptoBackupServer {
- @GuardedBy("this")
- @Nullable
- private String mActiveSecondaryKeyAlias;
-
- // Secondary key alias -> (package name -> tertiary key)
- @GuardedBy("this")
- private Map<String, Map<String, WrappedKeyProto.WrappedKey>> mWrappedKeyStore = new HashMap<>();
-
- @Override
- public String uploadIncrementalBackup(
- String packageName,
- String oldDocId,
- byte[] diffScript,
- WrappedKeyProto.WrappedKey tertiaryKey) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public String uploadNonIncrementalBackup(
- String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public synchronized void setActiveSecondaryKeyAlias(
- String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
- mActiveSecondaryKeyAlias = keyAlias;
-
- mWrappedKeyStore.putIfAbsent(keyAlias, new HashMap<>());
- Map<String, WrappedKeyProto.WrappedKey> keyStore = mWrappedKeyStore.get(keyAlias);
-
- for (String packageName : tertiaryKeys.keySet()) {
- keyStore.put(packageName, tertiaryKeys.get(packageName));
- }
- }
-
- public synchronized Optional<String> getActiveSecondaryKeyAlias() {
- return Optional.ofNullable(mActiveSecondaryKeyAlias);
- }
-
- public synchronized Map<String, WrappedKeyProto.WrappedKey> getAllTertiaryKeys(
- String secondaryKeyAlias) throws UnexpectedActiveSecondaryOnServerException {
- if (!secondaryKeyAlias.equals(mActiveSecondaryKeyAlias)) {
- throw new UnexpectedActiveSecondaryOnServerException(
- String.format(
- Locale.US,
- "Requested tertiary keys wrapped with %s but %s was active secondary.",
- secondaryKeyAlias,
- mActiveSecondaryKeyAlias));
- }
-
- if (!mWrappedKeyStore.containsKey(secondaryKeyAlias)) {
- return Collections.emptyMap();
- }
- return new HashMap<>(mWrappedKeyStore.get(secondaryKeyAlias));
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java
deleted file mode 100644
index 4cd8333b..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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.testing.fakes;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertThrows;
-
-import android.util.Pair;
-
-import com.android.server.backup.encryption.client.UnexpectedActiveSecondaryOnServerException;
-import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import java.nio.charset.Charset;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-public class FakeCryptoBackupServerTest {
- private static final String PACKAGE_NAME_1 = "package1";
- private static final String PACKAGE_NAME_2 = "package2";
- private static final String PACKAGE_NAME_3 = "package3";
- private static final WrappedKeyProto.WrappedKey PACKAGE_KEY_1 = createWrappedKey("key1");
- private static final WrappedKeyProto.WrappedKey PACKAGE_KEY_2 = createWrappedKey("key2");
- private static final WrappedKeyProto.WrappedKey PACKAGE_KEY_3 = createWrappedKey("key3");
-
- private FakeCryptoBackupServer mServer;
-
- @Before
- public void setUp() {
- mServer = new FakeCryptoBackupServer();
- }
-
- @Test
- public void getActiveSecondaryKeyAlias_isInitiallyAbsent() throws Exception {
- assertFalse(mServer.getActiveSecondaryKeyAlias().isPresent());
- }
-
- @Test
- public void setActiveSecondaryKeyAlias_setsTheKeyAlias() throws Exception {
- String keyAlias = "test";
- mServer.setActiveSecondaryKeyAlias(keyAlias, Collections.emptyMap());
- assertThat(mServer.getActiveSecondaryKeyAlias().get()).isEqualTo(keyAlias);
- }
-
- @Test
- public void getAllTertiaryKeys_returnsWrappedKeys() throws Exception {
- Map<String, WrappedKeyProto.WrappedKey> entries =
- createKeyMap(
- new Pair<>(PACKAGE_NAME_1, PACKAGE_KEY_1),
- new Pair<>(PACKAGE_NAME_2, PACKAGE_KEY_2));
- String secondaryKeyAlias = "doge";
- mServer.setActiveSecondaryKeyAlias(secondaryKeyAlias, entries);
-
- assertThat(mServer.getAllTertiaryKeys(secondaryKeyAlias)).containsExactlyEntriesIn(entries);
- }
-
- @Test
- public void addTertiaryKeys_updatesExistingSet() throws Exception {
- String keyId = "karlin";
- WrappedKeyProto.WrappedKey replacementKey = createWrappedKey("some replacement bytes");
-
- mServer.setActiveSecondaryKeyAlias(
- keyId,
- createKeyMap(
- new Pair<>(PACKAGE_NAME_1, PACKAGE_KEY_1),
- new Pair<>(PACKAGE_NAME_2, PACKAGE_KEY_2)));
-
- mServer.setActiveSecondaryKeyAlias(
- keyId,
- createKeyMap(
- new Pair<>(PACKAGE_NAME_1, replacementKey),
- new Pair<>(PACKAGE_NAME_3, PACKAGE_KEY_3)));
-
- assertThat(mServer.getAllTertiaryKeys(keyId))
- .containsExactlyEntriesIn(
- createKeyMap(
- new Pair<>(PACKAGE_NAME_1, replacementKey),
- new Pair<>(PACKAGE_NAME_2, PACKAGE_KEY_2),
- new Pair<>(PACKAGE_NAME_3, PACKAGE_KEY_3)));
- }
-
- @Test
- public void getAllTertiaryKeys_throwsForUnknownSecondaryKeyAlias() throws Exception {
- assertThrows(
- UnexpectedActiveSecondaryOnServerException.class,
- () -> mServer.getAllTertiaryKeys("unknown"));
- }
-
- @Test
- public void uploadIncrementalBackup_throwsUnsupportedOperationException() {
- assertThrows(
- UnsupportedOperationException.class,
- () ->
- mServer.uploadIncrementalBackup(
- PACKAGE_NAME_1,
- "docid",
- new byte[0],
- new WrappedKeyProto.WrappedKey()));
- }
-
- @Test
- public void uploadNonIncrementalBackup_throwsUnsupportedOperationException() {
- assertThrows(
- UnsupportedOperationException.class,
- () ->
- mServer.uploadNonIncrementalBackup(
- PACKAGE_NAME_1, new byte[0], new WrappedKeyProto.WrappedKey()));
- }
-
- private static WrappedKeyProto.WrappedKey createWrappedKey(String data) {
- WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
- wrappedKey.key = data.getBytes(Charset.forName("UTF-8"));
- return wrappedKey;
- }
-
- private Map<String, WrappedKeyProto.WrappedKey> createKeyMap(
- Pair<String, WrappedKeyProto.WrappedKey>... pairs) {
- Map<String, WrappedKeyProto.WrappedKey> map = new HashMap<>();
- for (Pair<String, WrappedKeyProto.WrappedKey> pair : pairs) {
- map.put(pair.first, pair.second);
- }
- return map;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/DataEntity.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/DataEntity.java
deleted file mode 100644
index 06f4859..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/DataEntity.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.testing.shadows;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * Represents a key value pair in {@link ShadowBackupDataInput} and {@link ShadowBackupDataOutput}.
- */
-public class DataEntity {
- public final String mKey;
- public final byte[] mValue;
- public final int mSize;
-
- /**
- * Constructs a pair with a string value. The value will be converted to a byte array in {@link
- * StandardCharsets#UTF_8}.
- */
- public DataEntity(String key, String value) {
- this.mKey = Objects.requireNonNull(key);
- this.mValue = value.getBytes(StandardCharsets.UTF_8);
- mSize = this.mValue.length;
- }
-
- /**
- * Constructs a new entity with the given key but a negative size. This represents a deleted
- * pair.
- */
- public DataEntity(String key) {
- this.mKey = Objects.requireNonNull(key);
- mSize = -1;
- mValue = null;
- }
-
- /** Constructs a new entity where the size of the value is the entire array. */
- public DataEntity(String key, byte[] value) {
- this(key, value, value.length);
- }
-
- /**
- * Constructs a new entity.
- *
- * @param key the key of the pair
- * @param data the value to associate with the key
- * @param size the length of the value in bytes
- */
- public DataEntity(String key, byte[] data, int size) {
- this.mKey = Objects.requireNonNull(key);
- this.mSize = size;
- mValue = new byte[size];
- for (int i = 0; i < size; i++) {
- mValue[i] = data[i];
- }
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- DataEntity that = (DataEntity) o;
-
- if (mSize != that.mSize) {
- return false;
- }
- if (!mKey.equals(that.mKey)) {
- return false;
- }
- return Arrays.equals(mValue, that.mValue);
- }
-
- @Override
- public int hashCode() {
- int result = mKey.hashCode();
- result = 31 * result + Arrays.hashCode(mValue);
- result = 31 * result + mSize;
- return result;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataInput.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataInput.java
deleted file mode 100644
index 7ac6ec4..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataInput.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.testing.shadows;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import android.annotation.Nullable;
-import android.app.backup.BackupDataInput;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-import java.io.ByteArrayInputStream;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Shadow for BackupDataInput. */
-@Implements(BackupDataInput.class)
-public class ShadowBackupDataInput {
- private static final List<DataEntity> ENTITIES = new ArrayList<>();
- @Nullable private static IOException sReadNextHeaderException;
-
- @Nullable private ByteArrayInputStream mCurrentEntityInputStream;
- private int mCurrentEntity = -1;
-
- /** Resets the shadow, clearing any entities or exception. */
- public static void reset() {
- ENTITIES.clear();
- sReadNextHeaderException = null;
- }
-
- /** Sets the exception which the input will throw for any call to {@link #readNextHeader}. */
- public static void setReadNextHeaderException(@Nullable IOException readNextHeaderException) {
- ShadowBackupDataInput.sReadNextHeaderException = readNextHeaderException;
- }
-
- /** Adds the given entity to the input. */
- public static void addEntity(DataEntity e) {
- ENTITIES.add(e);
- }
-
- /** Adds an entity to the input with the given key and value. */
- public static void addEntity(String key, byte[] value) {
- ENTITIES.add(new DataEntity(key, value, value.length));
- }
-
- public void __constructor__(FileDescriptor fd) {}
-
- @Implementation
- public boolean readNextHeader() throws IOException {
- if (sReadNextHeaderException != null) {
- throw sReadNextHeaderException;
- }
-
- mCurrentEntity++;
-
- if (mCurrentEntity >= ENTITIES.size()) {
- return false;
- }
-
- byte[] value = ENTITIES.get(mCurrentEntity).mValue;
- if (value == null) {
- mCurrentEntityInputStream = new ByteArrayInputStream(new byte[0]);
- } else {
- mCurrentEntityInputStream = new ByteArrayInputStream(value);
- }
- return true;
- }
-
- @Implementation
- public String getKey() {
- return ENTITIES.get(mCurrentEntity).mKey;
- }
-
- @Implementation
- public int getDataSize() {
- return ENTITIES.get(mCurrentEntity).mSize;
- }
-
- @Implementation
- public void skipEntityData() {
- // Do nothing.
- }
-
- @Implementation
- public int readEntityData(byte[] data, int offset, int size) {
- checkState(mCurrentEntityInputStream != null, "Must call readNextHeader() first");
- return mCurrentEntityInputStream.read(data, offset, size);
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
deleted file mode 100644
index 2302e55..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.testing.shadows;
-
-import android.app.backup.BackupDataOutput;
-
-import java.io.FileDescriptor;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.junit.Assert;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-/** Shadow for BackupDataOutput. */
-@Implements(BackupDataOutput.class)
-public class ShadowBackupDataOutput {
- private static final List<DataEntity> ENTRIES = new ArrayList<>();
-
- private String mCurrentKey;
- private int mDataSize;
-
- public static void reset() {
- ENTRIES.clear();
- }
-
- public static Set<DataEntity> getEntities() {
- return new LinkedHashSet<>(ENTRIES);
- }
-
- public void __constructor__(FileDescriptor fd) {}
-
- public void __constructor__(FileDescriptor fd, long quota) {}
-
- public void __constructor__(FileDescriptor fd, long quota, int transportFlags) {}
-
- @Implementation
- public int writeEntityHeader(String key, int size) {
- mCurrentKey = key;
- mDataSize = size;
- return 0;
- }
-
- @Implementation
- public int writeEntityData(byte[] data, int size) {
- Assert.assertEquals("ShadowBackupDataOutput expects size = mDataSize", size, mDataSize);
- ENTRIES.add(new DataEntity(mCurrentKey, data, mDataSize));
- return 0;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowInternalRecoveryServiceException.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowInternalRecoveryServiceException.java
deleted file mode 100644
index 9c06d81..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowInternalRecoveryServiceException.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.testing.shadows;
-
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-/** Shadow {@link InternalRecoveryServiceException}. */
-@Implements(InternalRecoveryServiceException.class)
-public class ShadowInternalRecoveryServiceException {
- private String mMessage;
-
- @Implementation
- public void __constructor__(String message) {
- mMessage = message;
- }
-
- @Implementation
- public void __constructor__(String message, Throwable cause) {
- mMessage = message;
- }
-
- @Implementation
- public String getMessage() {
- return mMessage;
- }
-}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowRecoveryController.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowRecoveryController.java
deleted file mode 100644
index 7dad8a4..0000000
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowRecoveryController.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.testing.shadows;
-
-import android.content.Context;
-import android.security.keystore.recovery.InternalRecoveryServiceException;
-import android.security.keystore.recovery.LockScreenRequiredException;
-import android.security.keystore.recovery.RecoveryController;
-
-import com.google.common.collect.ImmutableList;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.Resetter;
-
-import java.lang.reflect.Constructor;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.util.HashMap;
-import java.util.List;
-
-import javax.crypto.KeyGenerator;
-
-/**
- * Shadow of {@link RecoveryController}.
- *
- * <p>Instead of generating keys via the {@link RecoveryController}, this shadow generates them in
- * memory.
- */
-@Implements(RecoveryController.class)
-public class ShadowRecoveryController {
- private static final String KEY_GENERATOR_ALGORITHM = "AES";
- private static final int KEY_SIZE_BITS = 256;
-
- private static boolean sIsSupported = true;
- private static boolean sThrowsInternalError = false;
- private static HashMap<String, Key> sKeysByAlias = new HashMap<>();
- private static HashMap<String, Integer> sKeyStatusesByAlias = new HashMap<>();
-
- @Implementation
- public void __constructor__() {
- // do not throw
- }
-
- @Implementation
- public static RecoveryController getInstance(Context context) {
- // Call non-public constructor.
- try {
- Constructor<RecoveryController> constructor = RecoveryController.class.getConstructor();
- return constructor.newInstance();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- @Implementation
- public static boolean isRecoverableKeyStoreEnabled(Context context) {
- return sIsSupported;
- }
-
- @Implementation
- public Key generateKey(String alias)
- throws InternalRecoveryServiceException, LockScreenRequiredException {
- maybeThrowError();
- KeyGenerator keyGenerator;
- try {
- keyGenerator = KeyGenerator.getInstance(KEY_GENERATOR_ALGORITHM);
- } catch (NoSuchAlgorithmException e) {
- // Should never happen
- throw new RuntimeException(e);
- }
-
- keyGenerator.init(KEY_SIZE_BITS);
- Key key = keyGenerator.generateKey();
- sKeysByAlias.put(alias, key);
- sKeyStatusesByAlias.put(alias, RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
- return key;
- }
-
- @Implementation
- public Key getKey(String alias)
- throws InternalRecoveryServiceException, UnrecoverableKeyException {
- return sKeysByAlias.get(alias);
- }
-
- @Implementation
- public void removeKey(String alias) throws InternalRecoveryServiceException {
- sKeyStatusesByAlias.remove(alias);
- sKeysByAlias.remove(alias);
- }
-
- @Implementation
- public int getRecoveryStatus(String alias) throws InternalRecoveryServiceException {
- maybeThrowError();
- return sKeyStatusesByAlias.getOrDefault(
- alias, RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
- }
-
- @Implementation
- public List<String> getAliases() throws InternalRecoveryServiceException {
- return ImmutableList.copyOf(sKeyStatusesByAlias.keySet());
- }
-
- private static void maybeThrowError() throws InternalRecoveryServiceException {
- if (sThrowsInternalError) {
- throw new InternalRecoveryServiceException("test error");
- }
- }
-
- /** Sets the recovery status of the key with {@code alias} to {@code status}. */
- public static void setRecoveryStatus(String alias, int status) {
- sKeyStatusesByAlias.put(alias, status);
- }
-
- /** Sets all existing keys to being synced. */
- public static void syncAllKeys() {
- for (String alias : sKeysByAlias.keySet()) {
- sKeyStatusesByAlias.put(alias, RecoveryController.RECOVERY_STATUS_SYNCED);
- }
- }
-
- public static void setThrowsInternalError(boolean throwsInternalError) {
- ShadowRecoveryController.sThrowsInternalError = throwsInternalError;
- }
-
- public static void setIsSupported(boolean isSupported) {
- ShadowRecoveryController.sIsSupported = isSupported;
- }
-
- @Resetter
- public static void reset() {
- sIsSupported = true;
- sThrowsInternalError = false;
- sKeysByAlias.clear();
- sKeyStatusesByAlias.clear();
- }
-}
diff --git a/packages/BackupEncryption/test/unittest/Android.bp b/packages/BackupEncryption/test/unittest/Android.bp
deleted file mode 100644
index f005170..0000000
--- a/packages/BackupEncryption/test/unittest/Android.bp
+++ /dev/null
@@ -1,31 +0,0 @@
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_test {
- name: "BackupEncryptionUnitTests",
- srcs: ["src/**/*.java"],
- static_libs: [
- "androidx.test.runner",
- "androidx.test.rules",
- "mockito-target-minus-junit4",
- "platform-test-annotations",
- "truth-prebuilt",
- "testables",
- "testng",
- ],
- libs: [
- "android.test.mock",
- "android.test.base",
- "android.test.runner",
- "BackupEncryption",
- ],
- test_suites: ["device-tests"],
- instrumentation_for: "BackupEncryption",
- certificate: "platform",
-}
diff --git a/packages/BackupEncryption/test/unittest/AndroidManifest.xml b/packages/BackupEncryption/test/unittest/AndroidManifest.xml
deleted file mode 100644
index 39ac8aa3..0000000
--- a/packages/BackupEncryption/test/unittest/AndroidManifest.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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.server.backup.encryption.unittests"
- android:sharedUserId="android.uid.system" >
- <application android:testOnly="true">
- <uses-library android:name="android.test.runner" />
- </application>
- <instrumentation
- android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.server.backup.encryption"
- android:label="Backup Encryption Unit Tests" />
-</manifest>
\ No newline at end of file
diff --git a/packages/BackupEncryption/test/unittest/AndroidTest.xml b/packages/BackupEncryption/test/unittest/AndroidTest.xml
deleted file mode 100644
index c9c812a..0000000
--- a/packages/BackupEncryption/test/unittest/AndroidTest.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
- -->
-<configuration description="Runs Backup Encryption Unit Tests.">
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="install-arg" value="-t" />
- <option name="test-file-name" value="BackupEncryptionUnitTests.apk" />
- </target_preparer>
-
- <option name="test-tag" value="BackupEncryptionUnitTests" />
- <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
- <option name="package" value="com.android.server.backup.encryption.unittests" />
- <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
- </test>
-</configuration>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 56715b4..a7e1a59 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -363,13 +363,6 @@
mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data);
}
- private void onAssociationCreated(@NonNull AssociationInfo association) {
- if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association);
-
- // Don't need to notify the app, CdmService has already done that. Just finish.
- setResultAndFinish(association, RESULT_OK);
- }
-
private void cancel(boolean discoveryTimeout, boolean userRejected) {
if (DEBUG) {
Log.i(TAG, "cancel(), discoveryTimeout="
@@ -413,7 +406,9 @@
}
private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) {
- if (DEBUG) Log.i(TAG, "setResultAndFinish(), association=" + association);
+ Log.i(TAG, "setResultAndFinish(), association="
+ + (association == null ? "null" : association)
+ + "resultCode=" + resultCode);
final Intent data = new Intent();
if (association != null) {
@@ -652,14 +647,14 @@
new ResultReceiver(Handler.getMain()) {
@Override
protected void onReceiveResult(int resultCode, Bundle data) {
- if (resultCode != RESULT_CODE_ASSOCIATION_CREATED) {
- throw new RuntimeException("Unknown result code: " + resultCode);
+ if (resultCode == RESULT_CODE_ASSOCIATION_CREATED) {
+ final AssociationInfo association = data.getParcelable(
+ EXTRA_ASSOCIATION, AssociationInfo.class);
+ requireNonNull(association);
+ setResultAndFinish(association, RESULT_OK);
+ } else {
+ setResultAndFinish(null, resultCode);
}
-
- final AssociationInfo association = data.getParcelable(EXTRA_ASSOCIATION);
- requireNonNull(association);
-
- onAssociationCreated(association);
}
};
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index e583138..0a4972f 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -18,12 +18,13 @@
package="com.android.settingslib.spa.gallery">
<application
+ android:name=".GalleryApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_label"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true">
<activity
- android:name=".MainActivity"
+ android:name=".GalleryMainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/MainActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt
similarity index 70%
copy from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/MainActivity.kt
copy to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt
index 5e859ce..8c9d42c 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/MainActivity.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt
@@ -16,6 +16,12 @@
package com.android.settingslib.spa.gallery
-import com.android.settingslib.spa.framework.BrowseActivity
+import android.app.Application
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-class MainActivity : BrowseActivity(GallerySpaEnvironment)
+class GalleryApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ SpaEnvironmentFactory.instance = GallerySpaEnvironment
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
index 332d5a8..23072a2 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
@@ -18,4 +18,4 @@
import com.android.settingslib.spa.framework.DebugActivity
-class GalleryDebugActivity : DebugActivity(GallerySpaEnvironment)
+class GalleryDebugActivity : DebugActivity()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
index 5e04861..817c209f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
@@ -18,4 +18,4 @@
import com.android.settingslib.spa.framework.EntryProvider
-class GalleryEntryProvider : EntryProvider(GallerySpaEnvironment)
+class GalleryEntryProvider : EntryProvider()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/MainActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryMainActivity.kt
similarity index 92%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/MainActivity.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryMainActivity.kt
index 5e859ce..08a9bf5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/MainActivity.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryMainActivity.kt
@@ -18,4 +18,4 @@
import com.android.settingslib.spa.framework.BrowseActivity
-class MainActivity : BrowseActivity(GallerySpaEnvironment)
+class GalleryMainActivity : BrowseActivity()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 33c4d77..aa457fe 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -87,7 +87,7 @@
)
}
- override val browseActivityClass = MainActivity::class.java
+ override val browseActivityClass = GalleryMainActivity::class.java
override val entryProviderAuthorities = "com.android.spa.gallery.provider"
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index 8ca1c37..d87c31b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -30,7 +30,7 @@
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.android.settingslib.spa.R
-import com.android.settingslib.spa.framework.common.SpaEnvironment
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.compose.LocalNavController
import com.android.settingslib.spa.framework.compose.NavControllerWrapperImpl
import com.android.settingslib.spa.framework.compose.localNavController
@@ -50,8 +50,8 @@
* $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination HOME
* $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination ARGUMENT/bar/5
*/
-open class BrowseActivity(spaEnvironment: SpaEnvironment) : ComponentActivity() {
- private val sppRepository by spaEnvironment.pageProviderRepository
+open class BrowseActivity : ComponentActivity() {
+ private val spaEnvironment get() = SpaEnvironmentFactory.instance
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.Theme_SpaLib_DayNight)
@@ -67,6 +67,7 @@
@Composable
private fun MainContent() {
+ val sppRepository by spaEnvironment.pageProviderRepository
val navController = rememberNavController()
CompositionLocalProvider(navController.localNavController()) {
NavHost(navController, NULL_PAGE_NAME) {
@@ -84,6 +85,7 @@
@Composable
private fun InitialDestinationNavigator() {
+ val sppRepository by spaEnvironment.pageProviderRepository
val destinationNavigated = rememberSaveable { mutableStateOf(false) }
if (destinationNavigated.value) return
destinationNavigated.value = true
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
index ab7c0fe1..b28da06 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
@@ -37,7 +37,7 @@
import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.SpaEnvironment
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.compose.localNavController
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.toState
@@ -63,8 +63,8 @@
* For gallery, Activity = com.android.settingslib.spa.gallery/.GalleryDebugActivity
* For SettingsGoogle, Activity = com.android.settings/.spa.SpaDebugActivity
*/
-open class DebugActivity(private val spaEnvironment: SpaEnvironment) : ComponentActivity() {
- private val entryRepository by spaEnvironment.entryRepository
+open class DebugActivity : ComponentActivity() {
+ private val spaEnvironment get() = SpaEnvironmentFactory.instance
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.Theme_SpaLib_DayNight)
@@ -128,6 +128,7 @@
@Composable
fun RootPage() {
+ val entryRepository by spaEnvironment.entryRepository
val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
val allEntry = remember { entryRepository.getAllEntries() }
HomeScaffold(title = "Settings Debug") {
@@ -141,6 +142,7 @@
})
Preference(object : PreferenceModel {
override val title = "Query EntryProvider"
+ override val enabled = isEntryProviderAvailable().toState()
override val onClick = { displayDebugMessage() }
})
}
@@ -148,6 +150,7 @@
@Composable
fun AllPages() {
+ val entryRepository by spaEnvironment.entryRepository
val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
RegularScaffold(title = "All Pages (${allPageWithEntry.size})") {
for (pageWithEntry in allPageWithEntry) {
@@ -164,6 +167,7 @@
@Composable
fun AllEntries() {
+ val entryRepository by spaEnvironment.entryRepository
val allEntry = remember { entryRepository.getAllEntries() }
RegularScaffold(title = "All Entries (${allEntry.size})") {
EntryList(allEntry)
@@ -172,6 +176,7 @@
@Composable
fun OnePage(arguments: Bundle?) {
+ val entryRepository by spaEnvironment.entryRepository
val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
val pageWithEntry = entryRepository.getPageWithEntry(id)!!
RegularScaffold(title = "Page - ${pageWithEntry.page.displayName}") {
@@ -180,7 +185,7 @@
Text(text = "Entry size: ${pageWithEntry.entries.size}")
Preference(model = object : PreferenceModel {
override val title = "open page"
- override val enabled = (!pageWithEntry.page.hasRuntimeParam()).toState()
+ override val enabled = isPageClickable(pageWithEntry.page).toState()
override val onClick = openPage(pageWithEntry.page)
})
EntryList(pageWithEntry.entries)
@@ -189,13 +194,14 @@
@Composable
fun OneEntry(arguments: Bundle?) {
+ val entryRepository by spaEnvironment.entryRepository
val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
val entry = entryRepository.getEntry(id)!!
val entryContent = remember { entry.formatContent() }
RegularScaffold(title = "Entry - ${entry.displayTitle()}") {
Preference(model = object : PreferenceModel {
override val title = "open entry"
- override val enabled = (!entry.containerPage().hasRuntimeParam()).toState()
+ override val enabled = isEntryClickable(entry).toState()
override val onClick = openEntry(entry)
})
Text(text = entryContent)
@@ -216,7 +222,7 @@
@Composable
private fun openPage(page: SettingsPage): (() -> Unit)? {
- if (page.hasRuntimeParam()) return null
+ if (!isPageClickable(page)) return null
val context = LocalContext.current
val route = page.buildRoute()
val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
@@ -230,7 +236,7 @@
@Composable
private fun openEntry(entry: SettingsEntry): (() -> Unit)? {
- if (entry.containerPage().hasRuntimeParam()) return null
+ if (!isEntryClickable(entry)) return null
val context = LocalContext.current
val route = entry.containerPage().buildRoute()
val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
@@ -242,4 +248,17 @@
context.startActivity(intent)
}
}
+
+ private fun isEntryProviderAvailable(): Boolean {
+ return spaEnvironment.entryProviderAuthorities != null
+ }
+
+ private fun isPageClickable(page: SettingsPage): Boolean {
+ return spaEnvironment.browseActivityClass != null && !page.hasRuntimeParam()
+ }
+
+ private fun isEntryClickable(entry: SettingsEntry): Boolean {
+ return spaEnvironment.browseActivityClass != null &&
+ !entry.containerPage().hasRuntimeParam()
+ }
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
index 50157fc..532f63b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
@@ -30,7 +30,7 @@
import android.util.Log
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.SpaEnvironment
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
private const val TAG = "EntryProvider"
@@ -49,9 +49,8 @@
* $ adb shell content query --uri content://<AuthorityPath>/search_static
* $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
*/
-open class EntryProvider(spaEnvironment: SpaEnvironment) : ContentProvider() {
- private val entryRepository by spaEnvironment.entryRepository
- private val browseActivityClass = spaEnvironment.browseActivityClass
+open class EntryProvider : ContentProvider() {
+ private val spaEnvironment get() = SpaEnvironmentFactory.instance
/**
* Enum to define all column names in provider.
@@ -221,6 +220,7 @@
}
private fun queryPageDebug(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
val command = createBrowsePageAdbCommand(pageWithEntry.page)
@@ -232,6 +232,7 @@
}
private fun queryEntryDebug(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
val command = createBrowsePageAdbCommand(entry.containerPage(), entry.id)
@@ -243,6 +244,7 @@
}
private fun queryPageInfo(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
val page = pageWithEntry.page
@@ -261,6 +263,7 @@
}
private fun queryEntryInfo(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
cursor.newRow()
@@ -276,6 +279,7 @@
}
private fun querySearchSitemap(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_SITEMAP_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
if (!entry.isAllowSearch) continue
@@ -287,6 +291,7 @@
}
private fun querySearchStaticData(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_DATA_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
if (!entry.isAllowSearch || entry.isSearchDataDynamic) continue
@@ -296,6 +301,7 @@
}
private fun querySearchDynamicData(): Cursor {
+ val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
if (!entry.isAllowSearch || !entry.isSearchDataDynamic) continue
@@ -317,26 +323,31 @@
}
private fun createBrowsePageIntent(page: SettingsPage, entryId: String? = null): Intent {
- if (context == null || page.hasRuntimeParam())
- return Intent()
-
- return Intent().setComponent(ComponentName(context!!, browseActivityClass)).apply {
- putExtra(BrowseActivity.KEY_DESTINATION, page.buildRoute())
- if (entryId != null) {
- putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
+ if (!isPageBrowsable(page)) return Intent()
+ return Intent().setComponent(ComponentName(context!!, spaEnvironment.browseActivityClass!!))
+ .apply {
+ putExtra(BrowseActivity.KEY_DESTINATION, page.buildRoute())
+ if (entryId != null) {
+ putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
+ }
}
- }
}
private fun createBrowsePageAdbCommand(page: SettingsPage, entryId: String? = null): String? {
- if (context == null || page.hasRuntimeParam()) return null
+ if (!isPageBrowsable(page)) return null
val packageName = context!!.packageName
- val activityName = browseActivityClass.name.replace(packageName, "")
+ val activityName = spaEnvironment.browseActivityClass!!.name.replace(packageName, "")
val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}"
val highlightParam =
if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
}
+
+ private fun isPageBrowsable(page: SettingsPage): Boolean {
+ return context != null &&
+ spaEnvironment.browseActivityClass != null &&
+ !page.hasRuntimeParam()
+ }
}
fun EntryProvider.QueryEnum.getColumns(): Array<String> {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index 111555b..3885025 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -18,12 +18,28 @@
import android.app.Activity
+object SpaEnvironmentFactory {
+ private var spaEnvironment: SpaEnvironment? = null
+
+ var instance: SpaEnvironment
+ get() {
+ if (spaEnvironment == null)
+ throw UnsupportedOperationException("Spa environment is not set")
+ return spaEnvironment!!
+ }
+ set(env: SpaEnvironment) {
+ if (spaEnvironment != null)
+ throw UnsupportedOperationException("Spa environment is already set")
+ spaEnvironment = env
+ }
+}
+
abstract class SpaEnvironment {
abstract val pageProviderRepository: Lazy<SettingsPageProviderRepository>
val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) }
- abstract val browseActivityClass: Class<out Activity>
+ open val browseActivityClass: Class<out Activity>? = null
open val entryProviderAuthorities: String? = null
diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
index 6940c39..33c2e4855 100644
--- a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
+++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
@@ -13,47 +13,62 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
+
+<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:baselineAligned="false"
- android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/user_info_scroll"
android:padding="16dp">
- <FrameLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center">
- <ImageView
- android:id="@+id/user_photo"
- android:layout_width="@dimen/user_photo_size_in_user_info_dialog"
- android:layout_height="@dimen/user_photo_size_in_user_info_dialog"
- android:contentDescription="@string/user_image_photo_selector"
- android:scaleType="fitCenter"/>
- <ImageView
- android:id="@+id/add_a_photo_icon"
- android:layout_width="@dimen/add_a_photo_icon_size_in_user_info_dialog"
- android:layout_height="@dimen/add_a_photo_icon_size_in_user_info_dialog"
- android:src="@drawable/add_a_photo_circled"
- android:layout_gravity="bottom|right" />
- </FrameLayout>
-
- <EditText
- android:id="@+id/user_name"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="@dimen/user_name_height_in_user_info_dialog"
- android:layout_gravity="center"
- android:minWidth="200dp"
- android:layout_marginStart="6dp"
- android:minHeight="@dimen/min_tap_target_size"
- android:ellipsize="end"
- android:singleLine="true"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textAlignment="viewStart"
- android:inputType="text|textCapWords"
- android:selectAllOnFocus="true"
- android:hint="@string/user_nickname"
- android:maxLength="100"/>
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/user_info_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/EditUserDialogTitle"
+ android:text="@string/user_info_settings_title"
+ android:textDirection="locale"/>
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+ <ImageView
+ android:id="@+id/user_photo"
+ android:layout_width="@dimen/user_photo_size_in_user_info_dialog"
+ android:layout_height="@dimen/user_photo_size_in_user_info_dialog"
+ android:contentDescription="@string/user_image_photo_selector"
+ android:scaleType="fitCenter"/>
+ <ImageView
+ android:id="@+id/add_a_photo_icon"
+ android:layout_width="@dimen/add_a_photo_icon_size_in_user_info_dialog"
+ android:layout_height="@dimen/add_a_photo_icon_size_in_user_info_dialog"
+ android:src="@drawable/add_a_photo_circled"
+ android:layout_gravity="bottom|right"/>
+ </FrameLayout>
-</LinearLayout>
+ <EditText
+ android:id="@+id/user_name"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/user_name_height_in_user_info_dialog"
+ android:layout_gravity="center"
+ android:minWidth="200dp"
+ android:layout_marginStart="6dp"
+ android:minHeight="@dimen/min_tap_target_size"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textAlignment="viewStart"
+ android:inputType="text|textCapWords"
+ android:selectAllOnFocus="true"
+ android:hint="@string/user_nickname"
+ android:maxLength="100"/>
+
+ </LinearLayout>
+
+</ScrollView>
+
diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml
index 5237b4f..5a9e780 100644
--- a/packages/SettingsLib/res/values/styles.xml
+++ b/packages/SettingsLib/res/values/styles.xml
@@ -77,4 +77,10 @@
<item name="android:textSize">@dimen/broadcast_dialog_btn_text_size</item>
</style>
+ <style name="EditUserDialogTitle" parent="@android:TextAppearance.DeviceDefault.Headline">
+ <item name="android:textSize">@dimen/broadcast_dialog_title_text_size</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textDirection">locale</item>
+ <item name="android:ellipsize">end</item>
+ </style>
</resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
index 3b542cc..e55d7ea 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
@@ -31,6 +31,7 @@
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageView;
+import android.widget.ScrollView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -159,7 +160,8 @@
userPhotoView);
}
}
-
+ ScrollView scrollView = content.findViewById(R.id.user_info_scroll);
+ scrollView.setClipToOutline(true);
mEditUserInfoDialog = buildDialog(activity, content, userNameView, oldUserIcon,
defaultUserName, title, successCallback, cancelCallback);
@@ -182,7 +184,6 @@
@Nullable Drawable oldUserIcon, String defaultUserName, String title,
BiConsumer<String, Drawable> successCallback, Runnable cancelCallback) {
return new AlertDialog.Builder(activity)
- .setTitle(title)
.setView(content)
.setCancelable(true)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index fab85b7..def7ddc 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -213,7 +213,6 @@
Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
Settings.Secure.WEAR_TALKBACK_ENABLED,
Settings.Secure.HBM_SETTING_KEY,
- Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED,
Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED,
Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED,
Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 1454239..cde4bc4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -346,7 +346,6 @@
VALIDATORS.put(Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.WEAR_TALKBACK_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.HBM_SETTING_KEY, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_CODE, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, ANY_STRING_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index c3b645e..4e2bce2 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1825,9 +1825,6 @@
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED,
SecureSettingsProto.Accessibility
.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED);
- dumpSetting(s, p,
- Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED,
- SecureSettingsProto.Accessibility.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED);
p.end(accessibilityToken);
final long adaptiveSleepToken = p.start(SecureSettingsProto.ADAPTIVE_SLEEP);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 528af2e..cd667ca 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -48,6 +48,7 @@
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FrameworkStatsLog;
@@ -376,8 +377,8 @@
Setting newSetting = new Setting(name, oldSetting.getValue(), null,
oldSetting.getPackageName(), oldSetting.getTag(), false,
oldSetting.getId());
- int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue,
- newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue());
+ int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), 0,
+ oldValue, newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue());
checkNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
mSettings.put(name, newSetting);
updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
@@ -414,8 +415,9 @@
String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null;
String newDefaultValue = makeDefault ? value : oldDefaultValue;
- int newSize = getNewMemoryUsagePerPackageLocked(packageName, oldValue, value,
- oldDefaultValue, newDefaultValue);
+ int newSize = getNewMemoryUsagePerPackageLocked(packageName,
+ oldValue == null ? name.length() : 0 /* deltaKeySize */,
+ oldValue, value, oldDefaultValue, newDefaultValue);
checkNewMemoryUsagePerPackageLocked(packageName, newSize);
Setting newState;
@@ -559,8 +561,12 @@
}
Setting oldState = mSettings.remove(name);
- int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName, oldState.value,
- null, oldState.defaultValue, null);
+ if (oldState == null) {
+ return false;
+ }
+ int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName,
+ -name.length() /* deltaKeySize */,
+ oldState.value, null, oldState.defaultValue, null);
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, /* value= */ "",
/* newValue= */ "", oldState.value, /* tag */ "", false, getUserIdFromKey(mKey),
@@ -583,15 +589,16 @@
}
Setting setting = mSettings.get(name);
+ if (setting == null) {
+ return false;
+ }
Setting oldSetting = new Setting(setting);
String oldValue = setting.getValue();
String oldDefaultValue = setting.getDefaultValue();
- String newValue = oldDefaultValue;
- String newDefaultValue = oldDefaultValue;
- int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, oldValue,
- newValue, oldDefaultValue, newDefaultValue);
+ int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, 0, oldValue,
+ oldDefaultValue, oldDefaultValue, oldDefaultValue);
checkNewMemoryUsagePerPackageLocked(setting.packageName, newSize);
if (!setting.reset()) {
@@ -725,8 +732,8 @@
}
@GuardedBy("mLock")
- private int getNewMemoryUsagePerPackageLocked(String packageName, String oldValue,
- String newValue, String oldDefaultValue, String newDefaultValue) {
+ private int getNewMemoryUsagePerPackageLocked(String packageName, int deltaKeySize,
+ String oldValue, String newValue, String oldDefaultValue, String newDefaultValue) {
if (isExemptFromMemoryUsageCap(packageName)) {
return 0;
}
@@ -735,7 +742,7 @@
final int newValueSize = (newValue != null) ? newValue.length() : 0;
final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0;
- final int deltaSize = newValueSize + newDefaultValueSize
+ final int deltaSize = deltaKeySize + newValueSize + newDefaultValueSize
- oldValueSize - oldDefaultValueSize;
return Math.max((currentSize != null) ? currentSize + deltaSize : deltaSize, 0);
}
@@ -1577,4 +1584,11 @@
}
return false;
}
+
+ @VisibleForTesting
+ public int getMemoryUsage(String packageName) {
+ synchronized (mLock) {
+ return mPackageToMemoryUsage.getOrDefault(packageName, 0);
+ }
+ }
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index a637efa..5a534b9 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -294,7 +294,7 @@
settingsState.deleteSettingLocked(SETTING_NAME);
// Should not throw if usage is under the cap
- settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 19999),
+ settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 19975),
null, false, "p1");
settingsState.deleteSettingLocked(SETTING_NAME);
try {
@@ -312,5 +312,97 @@
assertTrue(ex.getMessage().contains("p1"));
}
assertTrue(settingsState.getSettingLocked(SETTING_NAME).isNull());
+ try {
+ settingsState.insertSettingLocked(Strings.repeat("A", 20001), "",
+ null, false, "p1");
+ fail("Should throw because it exceeded per package memory usage");
+ } catch (IllegalStateException ex) {
+ assertTrue(ex.getMessage().contains("You are adding too many system settings"));
+ }
+ }
+
+ public void testMemoryUsagePerPackage() {
+ SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1,
+ SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper());
+
+ // Test inserting one key with default
+ final String testKey1 = SETTING_NAME;
+ final String testValue1 = Strings.repeat("A", 100);
+ settingsState.insertSettingLocked(testKey1, testValue1, null, true, TEST_PACKAGE);
+ int expectedMemUsage = testKey1.length() + testValue1.length()
+ + testValue1.length() /* size for default */;
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test inserting another key
+ final String testKey2 = SETTING_NAME + "2";
+ settingsState.insertSettingLocked(testKey2, testValue1, null, false, TEST_PACKAGE);
+ expectedMemUsage += testKey2.length() + testValue1.length();
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test updating first key with new default
+ final String testValue2 = Strings.repeat("A", 300);
+ settingsState.insertSettingLocked(testKey1, testValue2, null, true, TEST_PACKAGE);
+ expectedMemUsage += (testValue2.length() - testValue1.length()) * 2;
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test updating first key without new default
+ final String testValue3 = Strings.repeat("A", 50);
+ settingsState.insertSettingLocked(testKey1, testValue3, null, false, TEST_PACKAGE);
+ expectedMemUsage -= testValue2.length() - testValue3.length();
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test updating second key
+ settingsState.insertSettingLocked(testKey2, testValue2, null, false, TEST_PACKAGE);
+ expectedMemUsage -= testValue1.length() - testValue2.length();
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test resetting key
+ settingsState.resetSettingLocked(testKey1);
+ expectedMemUsage += testValue2.length() - testValue3.length();
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test resetting default value
+ settingsState.resetSettingDefaultValueLocked(testKey1);
+ expectedMemUsage -= testValue2.length();
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test deletion
+ settingsState.deleteSettingLocked(testKey2);
+ expectedMemUsage -= testValue2.length() + testKey2.length() /* key is deleted too */;
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test another package with a different key
+ final String testPackage2 = TEST_PACKAGE + "2";
+ final String testKey3 = SETTING_NAME + "3";
+ settingsState.insertSettingLocked(testKey3, testValue1, null, true, testPackage2);
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+ final int expectedMemUsage2 = testKey3.length() + testValue1.length() * 2;
+ assertEquals(expectedMemUsage2, settingsState.getMemoryUsage(testPackage2));
+
+ // Test system package
+ settingsState.insertSettingLocked(testKey1, testValue1, null, true, SYSTEM_PACKAGE);
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+ assertEquals(expectedMemUsage2, settingsState.getMemoryUsage(testPackage2));
+ assertEquals(0, settingsState.getMemoryUsage(SYSTEM_PACKAGE));
+
+ // Test invalid value
+ try {
+ settingsState.insertSettingLocked(testKey1, Strings.repeat("A", 20001), null, false,
+ TEST_PACKAGE);
+ fail("Should throw because it exceeded per package memory usage");
+ } catch (IllegalStateException ex) {
+ assertTrue(ex.getMessage().contains("You are adding too many system settings"));
+ }
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+
+ // Test invalid key
+ try {
+ settingsState.insertSettingLocked(Strings.repeat("A", 20001), "", null, false,
+ TEST_PACKAGE);
+ fail("Should throw because it exceeded per package memory usage");
+ } catch (IllegalStateException ex) {
+ assertTrue(ex.getMessage().contains("You are adding too many system settings"));
+ }
+ assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
}
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index ddfac36..1a2ac83 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -141,6 +141,7 @@
<uses-permission android:name="android.permission.SET_PREFERRED_APPLICATIONS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.LOCATION_BYPASS" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
new file mode 100644
index 0000000..1d808ba
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.SdkConstants.CLASS_CONTEXT
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiModifierListOwner
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Warns if {@code Context.bindService}, {@code Context.bindServiceAsUser}, or {@code
+ * Context.unbindService} is not called on a {@code WorkerThread}
+ */
+@Suppress("UnstableApiUsage")
+class BindServiceOnMainThreadDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("bindService", "bindServiceAsUser", "unbindService")
+ }
+
+ private fun hasWorkerThreadAnnotation(
+ context: JavaContext,
+ annotated: PsiModifierListOwner?
+ ): Boolean {
+ return context.evaluator.getAnnotations(annotated, inHierarchy = true).any { uAnnotation ->
+ uAnnotation.qualifiedName == "androidx.annotation.WorkerThread"
+ }
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
+ if (
+ !hasWorkerThreadAnnotation(context, node.getParentOfType(UMethod::class.java)) &&
+ !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java))
+ ) {
+ context.report(
+ ISSUE,
+ method,
+ context.getLocation(node),
+ "This method should be annotated with `@WorkerThread` because " +
+ "it calls ${method.name}",
+ )
+ }
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "BindServiceOnMainThread",
+ briefDescription = "Service bound or unbound on main thread",
+ explanation =
+ """
+ Binding and unbinding services are synchronous calls to `ActivityManager`. \
+ They usually take multiple milliseconds to complete. If called on the main \
+ thread, it will likely cause missed frames. To fix it, use a `@Background \
+ Executor` and annotate the calling method with `@WorkerThread`.
+ """,
+ category = Category.PERFORMANCE,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(
+ BindServiceOnMainThreadDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
deleted file mode 100644
index 925fae0e..0000000
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.systemui.lint
-
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UCallExpression
-
-@Suppress("UnstableApiUsage")
-class BindServiceViaContextDetector : Detector(), SourceCodeScanner {
-
- override fun getApplicableMethodNames(): List<String> {
- return listOf("bindService", "bindServiceAsUser", "unbindService")
- }
-
- override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
- context.report(
- ISSUE,
- method,
- context.getNameLocation(node),
- "Binding or unbinding services are synchronous calls, please make " +
- "sure you're on a @Background Executor."
- )
- }
- }
-
- companion object {
- @JvmField
- val ISSUE: Issue =
- Issue.create(
- id = "BindServiceViaContextDetector",
- briefDescription = "Service bound/unbound via Context, please make sure " +
- "you're on a background thread.",
- explanation =
- "Binding or unbinding services are synchronous calls to ActivityManager, " +
- "they usually take multiple milliseconds to complete and will make" +
- "the caller drop frames. Make sure you're on a @Background Executor.",
- category = Category.PERFORMANCE,
- priority = 8,
- severity = Severity.WARNING,
- implementation =
- Implementation(BindServiceViaContextDetector::class.java, Scope.JAVA_FILE_SCOPE)
- )
- }
-}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
index 8d48f09..1129929 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
@@ -16,6 +16,7 @@
package com.android.internal.systemui.lint
+import com.android.SdkConstants.CLASS_CONTEXT
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
@@ -48,14 +49,14 @@
return
}
- val evaulator = context.evaluator
- if (evaulator.isMemberInSubClassOf(method, "android.content.Context")) {
+ val evaluator = context.evaluator
+ if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
context.report(
ISSUE,
method,
context.getNameLocation(node),
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ "`Context.${method.name}()` should be replaced with " +
+ "`BroadcastSender.${method.name}()`"
)
}
}
@@ -65,14 +66,14 @@
val ISSUE: Issue =
Issue.create(
id = "BroadcastSentViaContext",
- briefDescription = "Broadcast sent via Context instead of BroadcastSender.",
- explanation =
- "Broadcast was sent via " +
- "Context.sendBroadcast/Context.sendBroadcastAsUser. Please use " +
- "BroadcastSender.sendBroadcast/BroadcastSender.sendBroadcastAsUser " +
- "which will schedule dispatch of broadcasts on background thread. " +
- "Sending broadcasts on main thread causes jank due to synchronous " +
- "Binder calls.",
+ briefDescription = "Broadcast sent via `Context` instead of `BroadcastSender`",
+ // lint trims indents and converts \ to line continuations
+ explanation = """
+ Broadcasts sent via `Context.sendBroadcast()` or \
+ `Context.sendBroadcastAsUser()` will block the main thread and may cause \
+ missed frames. Instead, use `BroadcastSender.sendBroadcast()` or \
+ `BroadcastSender.sendBroadcastAsUser()` which will schedule and dispatch \
+ broadcasts on a background worker thread.""",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt
deleted file mode 100644
index a629eee..0000000
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.systemui.lint
-
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UCallExpression
-
-@Suppress("UnstableApiUsage")
-class GetMainLooperViaContextDetector : Detector(), SourceCodeScanner {
-
- override fun getApplicableMethodNames(): List<String> {
- return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor")
- }
-
- override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
- context.report(
- ISSUE,
- method,
- context.getNameLocation(node),
- "Please inject a @Main Executor instead."
- )
- }
- }
-
- companion object {
- @JvmField
- val ISSUE: Issue =
- Issue.create(
- id = "GetMainLooperViaContextDetector",
- briefDescription = "Please use idiomatic SystemUI executors, injecting " +
- "them via Dagger.",
- explanation = "Injecting the @Main Executor is preferred in order to make" +
- "dependencies explicit and increase testability. It's much " +
- "easier to pass a FakeExecutor on your test ctor than to " +
- "deal with loopers in unit tests.",
- category = Category.LINT,
- priority = 8,
- severity = Severity.WARNING,
- implementation = Implementation(GetMainLooperViaContextDetector::class.java,
- Scope.JAVA_FILE_SCOPE)
- )
- }
-}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
new file mode 100644
index 0000000..bab76ab
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.SdkConstants.CLASS_CONTEXT
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+@Suppress("UnstableApiUsage")
+class NonInjectedMainThreadDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor")
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
+ context.report(
+ ISSUE,
+ method,
+ context.getNameLocation(node),
+ "Replace with injected `@Main Executor`."
+ )
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "NonInjectedMainThread",
+ briefDescription = "Main thread usage without dependency injection",
+ explanation =
+ """
+ Main thread should be injected using the `@Main Executor` instead \
+ of using the accessors in `Context`. This is to make the \
+ dependencies explicit and increase testability. It's much easier \
+ to pass a `FakeExecutor` on test constructors than it is to deal \
+ with loopers in unit tests.""",
+ category = Category.LINT,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(NonInjectedMainThreadDetector::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
index 4eb7c7d..b622900 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
@@ -16,6 +16,7 @@
package com.android.internal.systemui.lint
+import com.android.SdkConstants.CLASS_CONTEXT
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
@@ -32,7 +33,7 @@
class NonInjectedServiceDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List<String> {
- return listOf("getSystemService")
+ return listOf("getSystemService", "get")
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
@@ -40,14 +41,25 @@
if (
!evaluator.isStatic(method) &&
method.name == "getSystemService" &&
- method.containingClass?.qualifiedName == "android.content.Context"
+ method.containingClass?.qualifiedName == CLASS_CONTEXT
) {
context.report(
ISSUE,
method,
context.getNameLocation(node),
- "Use @Inject to get the handle to a system-level services instead of using " +
- "Context.getSystemService()"
+ "Use `@Inject` to get system-level service handles instead of " +
+ "`Context.getSystemService()`"
+ )
+ } else if (
+ evaluator.isStatic(method) &&
+ method.name == "get" &&
+ method.containingClass?.qualifiedName == "android.accounts.AccountManager"
+ ) {
+ context.report(
+ ISSUE,
+ method,
+ context.getNameLocation(node),
+ "Replace `AccountManager.get()` with an injected instance of `AccountManager`"
)
}
}
@@ -57,14 +69,14 @@
val ISSUE: Issue =
Issue.create(
id = "NonInjectedService",
- briefDescription =
- "System-level services should be retrieved using " +
- "@Inject instead of Context.getSystemService().",
+ briefDescription = "System service not injected",
explanation =
- "Context.getSystemService() should be avoided because it makes testing " +
- "difficult. Instead, use an injected service. For example, " +
- "instead of calling Context.getSystemService(UserManager.class), " +
- "use @Inject and add UserManager to the constructor",
+ """
+ `Context.getSystemService()` should be avoided because it makes testing \
+ difficult. Instead, use an injected service. For example, instead of calling \
+ `Context.getSystemService(UserManager.class)` in a class, annotate the class' \
+ constructor with `@Inject` and add `UserManager` to the parameters.
+ """,
category = Category.CORRECTNESS,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
index eb71d32..4ba3afc 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
@@ -16,6 +16,7 @@
package com.android.internal.systemui.lint
+import com.android.SdkConstants.CLASS_CONTEXT
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
@@ -35,12 +36,12 @@
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
- if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
+ if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
context.report(
ISSUE,
method,
context.getNameLocation(node),
- "BroadcastReceivers should be registered via BroadcastDispatcher."
+ "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`"
)
}
}
@@ -49,14 +50,16 @@
@JvmField
val ISSUE: Issue =
Issue.create(
- id = "RegisterReceiverViaContextDetector",
- briefDescription = "Broadcast registrations via Context are blocking " +
- "calls. Please use BroadcastDispatcher.",
- explanation =
- "Context#registerReceiver is a blocking call to the system server, " +
- "making it very likely that you'll drop a frame. Please use " +
- "BroadcastDispatcher instead (or move this call to a " +
- "@Background Executor.)",
+ id = "RegisterReceiverViaContext",
+ briefDescription = "Blocking broadcast registration",
+ // lint trims indents and converts \ to line continuations
+ explanation = """
+ `Context.registerReceiver()` is a blocking call to the system server, \
+ making it very likely that you'll drop a frame. Please use \
+ `BroadcastDispatcher` instead, which registers the receiver on a \
+ background thread. `BroadcastDispatcher` also improves our visibility \
+ into ANRs.""",
+ moreInfo = "go/identifying-broadcast-threads",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
index b006615..7be21a5 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
@@ -49,8 +49,7 @@
ISSUE_SLOW_USER_ID_QUERY,
method,
context.getNameLocation(node),
- "ActivityManager.getCurrentUser() is slow. " +
- "Use UserTracker.getUserId() instead."
+ "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`"
)
}
if (
@@ -62,7 +61,7 @@
ISSUE_SLOW_USER_INFO_QUERY,
method,
context.getNameLocation(node),
- "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead."
+ "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
)
}
}
@@ -72,11 +71,13 @@
val ISSUE_SLOW_USER_ID_QUERY: Issue =
Issue.create(
id = "SlowUserIdQuery",
- briefDescription = "User ID queried using ActivityManager instead of UserTracker.",
+ briefDescription = "User ID queried using ActivityManager",
explanation =
- "ActivityManager.getCurrentUser() makes a binder call and is slow. " +
- "Instead, inject a UserTracker and call UserTracker.getUserId(). For " +
- "more info, see: http://go/multi-user-in-systemui-slides",
+ """
+ `ActivityManager.getCurrentUser()` uses a blocking binder call and is slow. \
+ Instead, inject a `UserTracker` and call `UserTracker.getUserId()`.
+ """,
+ moreInfo = "http://go/multi-user-in-systemui-slides",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
@@ -88,11 +89,13 @@
val ISSUE_SLOW_USER_INFO_QUERY: Issue =
Issue.create(
id = "SlowUserInfoQuery",
- briefDescription = "User info queried using UserManager instead of UserTracker.",
+ briefDescription = "User info queried using UserManager",
explanation =
- "UserManager.getUserInfo() makes a binder call and is slow. " +
- "Instead, inject a UserTracker and call UserTracker.getUserInfo(). For " +
- "more info, see: http://go/multi-user-in-systemui-slides",
+ """
+ `UserManager.getUserInfo()` uses a blocking binder call and is slow. \
+ Instead, inject a `UserTracker` and call `UserTracker.getUserInfo()`.
+ """,
+ moreInfo = "http://go/multi-user-in-systemui-slides",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
index a584894..4eeeb85 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
@@ -47,7 +47,7 @@
ISSUE,
referenced,
context.getNameLocation(referenced),
- "Usage of Config.HARDWARE is highly encouraged."
+ "Replace software bitmap with `Config.HARDWARE`"
)
}
}
@@ -56,12 +56,12 @@
@JvmField
val ISSUE: Issue =
Issue.create(
- id = "SoftwareBitmapDetector",
- briefDescription = "Software bitmap detected. Please use Config.HARDWARE instead.",
- explanation =
- "Software bitmaps occupy twice as much memory, when compared to Config.HARDWARE. " +
- "In case you need to manipulate the pixels, please consider to either use" +
- "a shader (encouraged), or a short lived software bitmap.",
+ id = "SoftwareBitmap",
+ briefDescription = "Software bitmap",
+ explanation = """
+ Software bitmaps occupy twice as much memory as `Config.HARDWARE` bitmaps \
+ do. However, hardware bitmaps are read-only. If you need to manipulate the \
+ pixels, use a shader (preferably) or a short lived software bitmap.""",
category = Category.PERFORMANCE,
priority = 8,
severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 312810b..cf7c1b5 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -28,11 +28,11 @@
override val issues: List<Issue>
get() = listOf(
- BindServiceViaContextDetector.ISSUE,
+ BindServiceOnMainThreadDetector.ISSUE,
BroadcastSentViaContextDetector.ISSUE,
SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY,
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY,
- GetMainLooperViaContextDetector.ISSUE,
+ NonInjectedMainThreadDetector.ISSUE,
RegisterReceiverViaContextDetector.ISSUE,
SoftwareBitmapDetector.ISSUE,
NonInjectedServiceDetector.ISSUE,
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 26bd8d0..486af9d 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -16,16 +16,21 @@
package com.android.internal.systemui.lint
+import com.android.annotations.NonNull
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import org.intellij.lang.annotations.Language
+
+@Suppress("UnstableApiUsage")
+@NonNull
+private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented()
/*
* This file contains stubs of framework APIs and System UI classes for testing purposes only. The
* stubs are not used in the lint detectors themselves.
*/
-@Suppress("UnstableApiUsage")
internal val androidStubs =
arrayOf(
- java(
+ indentedJava(
"""
package android.app;
@@ -34,7 +39,16 @@
}
"""
),
- java(
+ indentedJava(
+ """
+package android.accounts;
+
+public class AccountManager {
+ public static AccountManager get(Context context) { return null; }
+}
+"""
+ ),
+ indentedJava(
"""
package android.os;
import android.content.pm.UserInfo;
@@ -45,39 +59,39 @@
}
"""
),
- java("""
+ indentedJava("""
package android.annotation;
public @interface UserIdInt {}
"""),
- java("""
+ indentedJava("""
package android.content.pm;
public class UserInfo {}
"""),
- java("""
+ indentedJava("""
package android.os;
public class Looper {}
"""),
- java("""
+ indentedJava("""
package android.os;
public class Handler {}
"""),
- java("""
+ indentedJava("""
package android.content;
public class ServiceConnection {}
"""),
- java("""
+ indentedJava("""
package android.os;
public enum UserHandle {
ALL
}
"""),
- java(
+ indentedJava(
"""
package android.content;
import android.os.UserHandle;
@@ -108,7 +122,7 @@
}
"""
),
- java(
+ indentedJava(
"""
package android.app;
import android.content.Context;
@@ -116,7 +130,7 @@
public class Activity extends Context {}
"""
),
- java(
+ indentedJava(
"""
package android.graphics;
@@ -132,17 +146,17 @@
}
"""
),
- java("""
+ indentedJava("""
package android.content;
public class BroadcastReceiver {}
"""),
- java("""
+ indentedJava("""
package android.content;
public class IntentFilter {}
"""),
- java(
+ indentedJava(
"""
package com.android.systemui.settings;
import android.content.pm.UserInfo;
@@ -153,4 +167,23 @@
}
"""
),
+ indentedJava(
+ """
+package androidx.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+@Retention(SOURCE)
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
+public @interface WorkerThread {
+}
+"""
+ ),
)
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
new file mode 100644
index 0000000..6ae8fd3
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class BindServiceOnMainThreadDetectorTest : LintDetectorTest() {
+
+ override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
+
+ @Test
+ fun testBindService() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void bind(Context context) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ context.bindService(intent, null, 0);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls bindService [BindServiceOnMainThread]
+ context.bindService(intent, null, 0);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testBindServiceAsUser() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.os.UserHandle;
+
+ public class TestClass {
+ public void bind(Context context) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: This method should be annotated with @WorkerThread because it calls bindServiceAsUser [BindServiceOnMainThread]
+ context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testUnbindService() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.content.ServiceConnection;
+
+ public class TestClass {
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls unbindService [BindServiceOnMainThread]
+ context.unbindService(connection);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testWorkerMethod() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.content.ServiceConnection;
+ import androidx.annotation.WorkerThread;
+
+ public class TestClass {
+ @WorkerThread
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+
+ public class ChildTestClass extends TestClass {
+ @Override
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testWorkerClass() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.content.ServiceConnection;
+ import androidx.annotation.WorkerThread;
+
+ @WorkerThread
+ public class TestClass {
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+ }
+
+ public class ChildTestClass extends TestClass {
+ @Override
+ public void unbind(Context context, ServiceConnection connection) {
+ context.unbindService(connection);
+ }
+
+ public void bind(Context context, ServiceConnection connection) {
+ context.bind(connection);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceOnMainThreadDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
deleted file mode 100644
index 564afcb..0000000
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.systemui.lint
-
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-import org.junit.Test
-
-@Suppress("UnstableApiUsage")
-class BindServiceViaContextDetectorTest : LintDetectorTest() {
-
- override fun getDetector(): Detector = BindServiceViaContextDetector()
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
-
- override fun getIssues(): List<Issue> = listOf(BindServiceViaContextDetector.ISSUE)
-
- private val explanation = "Binding or unbinding services are synchronous calls"
-
- @Test
- fun testBindService() {
- lint()
- .files(
- TestFiles.java(
- """
- package test.pkg;
- import android.content.Context;
-
- public class TestClass1 {
- public void bind(Context context) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- context.bindService(intent, null, 0);
- }
- }
- """
- )
- .indented(),
- *stubs
- )
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
- }
-
- @Test
- fun testBindServiceAsUser() {
- lint()
- .files(
- TestFiles.java(
- """
- package test.pkg;
- import android.content.Context;
- import android.os.UserHandle;
-
- public class TestClass1 {
- public void bind(Context context) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
- }
- }
- """
- )
- .indented(),
- *stubs
- )
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
- }
-
- @Test
- fun testUnbindService() {
- lint()
- .files(
- TestFiles.java(
- """
- package test.pkg;
- import android.content.Context;
- import android.content.ServiceConnection;
-
- public class TestClass1 {
- public void unbind(Context context, ServiceConnection connection) {
- context.unbindService(connection);
- }
- }
- """
- )
- .indented(),
- *stubs
- )
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
- }
-
- private val stubs = androidStubs
-}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 06aee8e..7d42280 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -41,7 +41,7 @@
package test.pkg;
import android.content.Context;
- public class TestClass1 {
+ public class TestClass {
public void send(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
context.sendBroadcast(intent);
@@ -54,10 +54,13 @@
)
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Context.sendBroadcast() should be replaced with BroadcastSender.sendBroadcast() [BroadcastSentViaContext]
+ context.sendBroadcast(intent);
+ ~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -71,7 +74,7 @@
import android.content.Context;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void send(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
@@ -84,10 +87,13 @@
)
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext]
+ context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+ ~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -101,7 +107,7 @@
import android.app.Activity;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void send(Activity activity) {
Intent intent = new Intent(Intent.ACTION_VIEW);
activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
@@ -115,14 +121,44 @@
)
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext]
+ activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+ ~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@Test
+ fun testSendBroadcastInBroadcastSender() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package com.android.systemui.broadcast;
+ import android.app.Activity;
+ import android.os.UserHandle;
+
+ public class BroadcastSender {
+ public void send(Activity activity) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+ }
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BroadcastSentViaContextDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
fun testNoopIfNoCall() {
lint()
.files(
@@ -131,7 +167,7 @@
package test.pkg;
import android.content.Context;
- public class TestClass1 {
+ public class TestClass {
public void sendBroadcast() {
Intent intent = new Intent(Intent.ACTION_VIEW);
context.startActivity(intent);
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
similarity index 64%
rename from packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c55f399..c468af8 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -24,14 +24,12 @@
import org.junit.Test
@Suppress("UnstableApiUsage")
-class GetMainLooperViaContextDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : LintDetectorTest() {
- override fun getDetector(): Detector = GetMainLooperViaContextDetector()
+ override fun getDetector(): Detector = NonInjectedMainThreadDetector()
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
- override fun getIssues(): List<Issue> = listOf(GetMainLooperViaContextDetector.ISSUE)
-
- private val explanation = "Please inject a @Main Executor instead."
+ override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
@Test
fun testGetMainThreadHandler() {
@@ -43,7 +41,7 @@
import android.content.Context;
import android.os.Handler;
- public class TestClass1 {
+ public class TestClass {
public void test(Context context) {
Handler mainThreadHandler = context.getMainThreadHandler();
}
@@ -53,10 +51,16 @@
.indented(),
*stubs
)
- .issues(GetMainLooperViaContextDetector.ISSUE)
+ .issues(NonInjectedMainThreadDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+ Handler mainThreadHandler = context.getMainThreadHandler();
+ ~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -69,7 +73,7 @@
import android.content.Context;
import android.os.Looper;
- public class TestClass1 {
+ public class TestClass {
public void test(Context context) {
Looper mainLooper = context.getMainLooper();
}
@@ -79,10 +83,16 @@
.indented(),
*stubs
)
- .issues(GetMainLooperViaContextDetector.ISSUE)
+ .issues(NonInjectedMainThreadDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+ Looper mainLooper = context.getMainLooper();
+ ~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -95,7 +105,7 @@
import android.content.Context;
import java.util.concurrent.Executor;
- public class TestClass1 {
+ public class TestClass {
public void test(Context context) {
Executor mainExecutor = context.getMainExecutor();
}
@@ -105,10 +115,16 @@
.indented(),
*stubs
)
- .issues(GetMainLooperViaContextDetector.ISSUE)
+ .issues(NonInjectedMainThreadDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+ Executor mainExecutor = context.getMainExecutor();
+ ~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index 6b9f88f..c83a35b 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -39,7 +39,7 @@
package test.pkg;
import android.content.Context;
- public class TestClass1 {
+ public class TestClass {
public void getSystemServiceWithoutDagger(Context context) {
context.getSystemService("user");
}
@@ -51,8 +51,14 @@
)
.issues(NonInjectedServiceDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains("Use @Inject to get the handle")
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService]
+ context.getSystemService("user");
+ ~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -65,7 +71,7 @@
import android.content.Context;
import android.os.UserManager;
- public class TestClass2 {
+ public class TestClass {
public void getSystemServiceWithoutDagger(Context context) {
context.getSystemService(UserManager.class);
}
@@ -77,8 +83,46 @@
)
.issues(NonInjectedServiceDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains("Use @Inject to get the handle")
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService]
+ context.getSystemService(UserManager.class);
+ ~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testGetAccountManager() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.accounts.AccountManager;
+
+ public class TestClass {
+ public void getSystemServiceWithoutDagger(Context context) {
+ AccountManager.get(context);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(NonInjectedServiceDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Replace AccountManager.get() with an injected instance of AccountManager [NonInjectedService]
+ AccountManager.get(context);
+ ~~~
+ 0 errors, 1 warnings
+ """
+ )
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index 802ceba..ebcddeb 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -31,8 +31,6 @@
override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
- private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher."
-
@Test
fun testRegisterReceiver() {
lint()
@@ -44,7 +42,7 @@
import android.content.Context;
import android.content.IntentFilter;
- public class TestClass1 {
+ public class TestClass {
public void bind(Context context, BroadcastReceiver receiver,
IntentFilter filter) {
context.registerReceiver(receiver, filter, 0);
@@ -57,8 +55,14 @@
)
.issues(RegisterReceiverViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:9: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+ context.registerReceiver(receiver, filter, 0);
+ ~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -74,7 +78,7 @@
import android.os.Handler;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void bind(Context context, BroadcastReceiver receiver,
IntentFilter filter, Handler handler) {
context.registerReceiverAsUser(receiver, UserHandle.ALL, filter,
@@ -88,8 +92,14 @@
)
.issues(RegisterReceiverViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+ context.registerReceiverAsUser(receiver, UserHandle.ALL, filter,
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
@Test
@@ -105,7 +115,7 @@
import android.os.Handler;
import android.os.UserHandle;
- public class TestClass1 {
+ public class TestClass {
public void bind(Context context, BroadcastReceiver receiver,
IntentFilter filter, Handler handler) {
context.registerReceiverForAllUsers(receiver, filter, "permission",
@@ -119,8 +129,14 @@
)
.issues(RegisterReceiverViaContextDetector.ISSUE)
.run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ .expect(
+ """
+ src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+ context.registerReceiverForAllUsers(receiver, filter, "permission",
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index e265837..b03a11c 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -44,7 +44,7 @@
package test.pkg;
import android.app.ActivityManager;
- public class TestClass1 {
+ public class TestClass {
public void slewlyGetCurrentUser() {
ActivityManager.getCurrentUser();
}
@@ -59,10 +59,13 @@
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
)
.run()
- .expectWarningCount(1)
- .expectContains(
- "ActivityManager.getCurrentUser() is slow. " +
- "Use UserTracker.getUserId() instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserId() instead of ActivityManager.getCurrentUser() [SlowUserIdQuery]
+ ActivityManager.getCurrentUser();
+ ~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -75,7 +78,7 @@
package test.pkg;
import android.os.UserManager;
- public class TestClass2 {
+ public class TestClass {
public void slewlyGetUserInfo(UserManager userManager) {
userManager.getUserInfo();
}
@@ -90,9 +93,13 @@
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
)
.run()
- .expectWarningCount(1)
- .expectContains(
- "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead."
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserInfo() instead of UserManager.getUserInfo() [SlowUserInfoQuery]
+ userManager.getUserInfo();
+ ~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
)
}
@@ -105,7 +112,7 @@
package test.pkg;
import com.android.systemui.settings.UserTracker;
- public class TestClass3 {
+ public class TestClass {
public void quicklyGetUserId(UserTracker userTracker) {
userTracker.getUserId();
}
@@ -132,7 +139,7 @@
package test.pkg;
import com.android.systemui.settings.UserTracker;
- public class TestClass4 {
+ public class TestClass {
public void quicklyGetUserId(UserTracker userTracker) {
userTracker.getUserInfo();
}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index fd6ab09..fb6537e 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -31,8 +31,6 @@
override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
- private val explanation = "Usage of Config.HARDWARE is highly encouraged."
-
@Test
fun testSoftwareBitmap() {
lint()
@@ -41,7 +39,7 @@
"""
import android.graphics.Bitmap;
- public class TestClass1 {
+ public class TestClass {
public void test() {
Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
@@ -54,8 +52,17 @@
)
.issues(SoftwareBitmapDetector.ISSUE)
.run()
- .expectWarningCount(2)
- .expectContains(explanation)
+ .expect(
+ """
+ src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+ ARGB_8888,
+ ~~~~~~~~~
+ src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+ RGB_565,
+ ~~~~~~~
+ 0 errors, 2 warnings
+ """
+ )
}
@Test
@@ -66,7 +73,7 @@
"""
import android.graphics.Bitmap;
- public class TestClass1 {
+ public class TestClass {
public void test() {
Bitmap.createBitmap(300, 300, Bitmap.Config.HARDWARE);
}
@@ -78,7 +85,7 @@
)
.issues(SoftwareBitmapDetector.ISSUE)
.run()
- .expectWarningCount(0)
+ .expectClean()
}
private val stubs = androidStubs
diff --git a/packages/SystemUI/docs/device-entry/doze.md b/packages/SystemUI/docs/device-entry/doze.md
index 6b6dce5..10bd367 100644
--- a/packages/SystemUI/docs/device-entry/doze.md
+++ b/packages/SystemUI/docs/device-entry/doze.md
@@ -1,5 +1,7 @@
# Doze
+`Dozing` is a low-powered state of the device. If Always-on Display (AOD), pulsing, or wake-gestures are enabled, then the device will enter the `dozing` state after a user intent to turn off the screen (ie: power button) or the screen times out.
+
Always-on Display (AOD) provides an alternative 'screen-off' experience. Instead, of completely turning the display off, it provides a distraction-free, glanceable experience for the phone in a low-powered mode. In this low-powered mode, the display will have a lower refresh rate and the UI should frequently shift its displayed contents in order to prevent burn-in. The recommended max on-pixel-ratio (OPR) is 5% to reduce battery consumption.

@@ -58,7 +60,7 @@
Refer to the documentation in [DozeSuppressors][15] for more information.
## AOD burn-in and image retention
-Because AOD will show an image on the screen for an elogated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention.
+Because AOD will show an image on the screen for an elongated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention.
To prevent burn-in, it is recommended to often shift UI on the screen. [DozeUi][17] schedules a call to dozeTimeTick every minute to request a shift in UI for all elements on AOD. The amount of shift can be determined by undergoing simulated AOD testing since this may vary depending on the display.
diff --git a/packages/SystemUI/docs/device-entry/glossary.md b/packages/SystemUI/docs/device-entry/glossary.md
index f3d12c2..7f19b16 100644
--- a/packages/SystemUI/docs/device-entry/glossary.md
+++ b/packages/SystemUI/docs/device-entry/glossary.md
@@ -2,38 +2,38 @@
## Keyguard
-| Term | Description |
-| :-----------: | ----------- |
-| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer.|
-| Lock screen<br><br>| The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10].|
-| Bouncer, [bouncer.md][2]<br><br>| The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM.|
-| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5]|
-| Ambient display (AOD), [doze.md][6]<br><br>| UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices.|
+| Term | Description |
+|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer. |
+| Lock screen<br><br> | The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10]. |
+| Bouncer, [bouncer.md][2]<br><br> | The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM. |
+| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5] |
+| Ambient display (AOD), [doze.md][6]<br><br> | UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices. |
## General Authentication Terms
-| Term | Description |
-| ----------- | ----------- |
-| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input.|
-| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7].|
+| Term | Description |
+|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input. |
+| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7]. |
## Face Authentication Terms
-| Term | Description |
-| ----------- | ----------- |
-| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry.|
-| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above).|
-| Bypass User Journey <br><br>| Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen.|
-| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility.|
+| Term | Description |
+|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry. |
+| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above). |
+| Bypass User Journey <br><br> | Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen. |
+| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility. |
## Fingerprint Authentication Terms
-| Term | Description |
-| ----------- | ----------- |
-| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after two hard-fingerprint-failures, the primary authentication bouncer is shown</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul>|
-| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade.|
+| Term | Description |
+|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after multiple consecutive hard-fingerprint-failures, the primary authentication bouncer is shown. The exact number of attempts is defined in: [BiometricUnlockController#UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER][4]</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul> |
+| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade. |
## Other Authentication Terms
-| Term | Description |
-| ---------- | ----------- |
-| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently.|
+| Term | Description |
+|--------------|-----------------------------------------------------------------------|
+| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently. |
[1]: /frameworks/base/packages/SystemUI/docs/device-entry/keyguard.md
@@ -46,3 +46,4 @@
[8]: /frameworks/base/packages/SystemUI/res-keyguard/font/clock.xml
[9]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
[10]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java
+[11]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
diff --git a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
index 6939084..33c68bf1 100644
--- a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
+++ b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
@@ -16,6 +16,6 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
- <solid android:color="@android:color/white" />
+ <solid android:color="@android:color/transparent" />
<corners android:radius="@dimen/qs_media_album_radius" />
</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
index d7a0b47..3efdc5a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
@@ -17,6 +17,7 @@
import android.graphics.Point
import android.view.Surface
+import android.view.Surface.Rotation
import android.view.View
import android.view.WindowManager
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
@@ -58,14 +59,14 @@
* Updates display properties in order to calculate the initial position for the views
* Must be called before [registerViewForAnimation]
*/
- fun updateDisplayProperties() {
+ @JvmOverloads
+ fun updateDisplayProperties(@Rotation rotation: Int = windowManager.defaultDisplay.rotation) {
windowManager.defaultDisplay.getSize(screenSize)
// Simple implementation to get current fold orientation,
// this might not be correct on all devices
// TODO: use JetPack WindowManager library to get the fold orientation
- isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
- windowManager.defaultDisplay.rotation == Surface.ROTATION_180
+ isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
}
/**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index 22bffda..6087655 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -132,8 +132,11 @@
mMainThreadHandler.postAtFrontOfQueue(() -> {
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
- if (isRotationLocked()) {
- if (shouldOverrideUserLockPrefs(rotation)) {
+ boolean rotationLocked = isRotationLocked();
+ // The isVisible check makes the rotation button disappear when we are not locked
+ // (e.g. for tabletop auto-rotate).
+ if (rotationLocked || mRotationButton.isVisible()) {
+ if (shouldOverrideUserLockPrefs(rotation) && rotationLocked) {
setRotationLockedAtAngle(rotation);
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
index ec938b2..aca9907 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
@@ -15,12 +15,11 @@
package com.android.systemui.unfold.util
import android.content.Context
-import android.os.RemoteException
-import android.view.IRotationWatcher
-import android.view.IWindowManager
import android.view.Surface
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
/**
* [UnfoldTransitionProgressProvider] that emits transition progress only when the display has
@@ -29,27 +28,21 @@
*/
class NaturalRotationUnfoldProgressProvider(
private val context: Context,
- private val windowManagerInterface: IWindowManager,
+ private val rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider
) : UnfoldTransitionProgressProvider {
private val scopedUnfoldTransitionProgressProvider =
ScopedUnfoldTransitionProgressProvider(unfoldTransitionProgressProvider)
- private val rotationWatcher = RotationWatcher()
private var isNaturalRotation: Boolean = false
fun init() {
- try {
- windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
- } catch (e: RemoteException) {
- throw e.rethrowFromSystemServer()
- }
-
- onRotationChanged(context.display.rotation)
+ rotationChangeProvider.addCallback(rotationListener)
+ rotationListener.onRotationChanged(context.display.rotation)
}
- private fun onRotationChanged(rotation: Int) {
+ private val rotationListener = RotationListener { rotation ->
val isNewRotationNatural =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
@@ -60,12 +53,7 @@
}
override fun destroy() {
- try {
- windowManagerInterface.removeRotationWatcher(rotationWatcher)
- } catch (e: RemoteException) {
- e.rethrowFromSystemServer()
- }
-
+ rotationChangeProvider.removeCallback(rotationListener)
scopedUnfoldTransitionProgressProvider.destroy()
}
@@ -76,10 +64,4 @@
override fun removeCallback(listener: TransitionProgressListener) {
scopedUnfoldTransitionProgressProvider.removeCallback(listener)
}
-
- private inner class RotationWatcher : IRotationWatcher.Stub() {
- override fun onRotationChanged(rotation: Int) {
- this@NaturalRotationUnfoldProgressProvider.onRotationChanged(rotation)
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
index 692fe83..e6a2bfa 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
@@ -17,7 +17,6 @@
package com.android.keyguard
import android.app.StatusBarManager.SESSION_KEYGUARD
-import android.content.Context
import android.hardware.biometrics.BiometricSourceType
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.UiEvent
@@ -41,11 +40,10 @@
*/
@SysUISingleton
class KeyguardBiometricLockoutLogger @Inject constructor(
- context: Context?,
private val uiEventLogger: UiEventLogger,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val sessionTracker: SessionTracker
-) : CoreStartable(context) {
+) : CoreStartable {
private var fingerprintLockedOut = false
private var faceLockedOut = false
private var encryptedOrLockdown = false
@@ -169,4 +167,4 @@
return strongAuthFlags and flagCheck != 0
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
index 37829f2..a89cbf5 100644
--- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -19,11 +19,11 @@
@SysUISingleton
class ChooserSelector @Inject constructor(
- context: Context,
+ private val context: Context,
private val featureFlags: FeatureFlags,
@Application private val coroutineScope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher
-) : CoreStartable(context) {
+) : CoreStartable {
private val packageManager = context.packageManager
private val chooserComponent = ComponentName.unflattenFromString(
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 0201cdc..929ebea 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -16,39 +16,41 @@
package com.android.systemui;
-import android.content.Context;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
-import com.android.internal.annotations.VisibleForTesting;
-
import java.io.PrintWriter;
/**
- * A top-level module of system UI code (sometimes called "system UI services" elsewhere in code).
- * Which CoreStartable modules are loaded can be controlled via a config resource.
+ * Code that needs to be run when SystemUI is started.
+ *
+ * Which CoreStartable modules are loaded is controlled via the dagger graph. Bind them into the
+ * CoreStartable map with code such as:
+ *
+ * <pre>
+ * @Binds
+ * @IntoMap
+ * @ClassKey(FoobarStartable::class)
+ * abstract fun bind(impl: FoobarStartable): CoreStartable
+ * </pre>
*
* @see SystemUIApplication#startServicesIfNeeded()
*/
-public abstract class CoreStartable implements Dumpable {
- protected final Context mContext;
-
- public CoreStartable(Context context) {
- mContext = context;
- }
+public interface CoreStartable extends Dumpable {
/** Main entry point for implementations. Called shortly after app startup. */
- public abstract void start();
+ void start();
- protected void onConfigurationChanged(Configuration newConfig) {
+ /** */
+ default void onConfigurationChanged(Configuration newConfig) {
}
@Override
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ default void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
}
- @VisibleForTesting
- protected void onBootCompleted() {
+ /** Called when the device reports BOOT_COMPLETED. */
+ default void onBootCompleted() {
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
index 9cdce64..8f41956 100644
--- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java
+++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
@@ -46,7 +46,7 @@
* system that are used for testing the latency.
*/
@SysUISingleton
-public class LatencyTester extends CoreStartable {
+public class LatencyTester implements CoreStartable {
private static final boolean DEFAULT_ENABLED = Build.IS_ENG;
private static final String
ACTION_FINGERPRINT_WAKE =
@@ -62,13 +62,11 @@
@Inject
public LatencyTester(
- Context context,
BiometricUnlockController biometricUnlockController,
BroadcastDispatcher broadcastDispatcher,
DeviceConfigProxy deviceConfigProxy,
@Main DelayableExecutor mainExecutor
) {
- super(context);
mBiometricUnlockController = biometricUnlockController;
mBroadcastDispatcher = broadcastDispatcher;
mDeviceConfigProxy = deviceConfigProxy;
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 2e13903..b5f42a1 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -105,7 +105,7 @@
* for antialiasing and emulation purposes.
*/
@SysUISingleton
-public class ScreenDecorations extends CoreStartable implements Tunable , Dumpable {
+public class ScreenDecorations implements CoreStartable, Tunable , Dumpable {
private static final boolean DEBUG = false;
private static final String TAG = "ScreenDecorations";
@@ -130,6 +130,7 @@
@VisibleForTesting
protected boolean mIsRegistered;
private final BroadcastDispatcher mBroadcastDispatcher;
+ private final Context mContext;
private final Executor mMainExecutor;
private final TunerService mTunerService;
private final SecureSettings mSecureSettings;
@@ -308,7 +309,7 @@
ThreadFactory threadFactory,
PrivacyDotDecorProviderFactory dotFactory,
FaceScanningProviderFactory faceScanningFactory) {
- super(context);
+ mContext = context;
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
mBroadcastDispatcher = broadcastDispatcher;
@@ -973,7 +974,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
Log.i(TAG, "ScreenDecorations is disabled");
return;
diff --git a/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java b/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java
index 1f2de4c..5bd85a7 100644
--- a/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java
@@ -38,16 +38,17 @@
* @see SliceBroadcastRelay
*/
@SysUISingleton
-public class SliceBroadcastRelayHandler extends CoreStartable {
+public class SliceBroadcastRelayHandler implements CoreStartable {
private static final String TAG = "SliceBroadcastRelay";
private static final boolean DEBUG = false;
private final ArrayMap<Uri, BroadcastRelay> mRelays = new ArrayMap<>();
+ private final Context mContext;
private final BroadcastDispatcher mBroadcastDispatcher;
@Inject
public SliceBroadcastRelayHandler(Context context, BroadcastDispatcher broadcastDispatcher) {
- super(context);
+ mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 9cfd399..d9f44cd 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -45,8 +45,6 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.util.NotificationChannels;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
@@ -287,14 +285,10 @@
CoreStartable startable;
if (DEBUG) Log.d(TAG, "loading: " + clsName);
try {
- Constructor<?> constructor = Class.forName(clsName).getConstructor(
- Context.class);
- startable = (CoreStartable) constructor.newInstance(this);
+ startable = (CoreStartable) Class.forName(clsName).newInstance();
} catch (ClassNotFoundException
- | NoSuchMethodException
| IllegalAccessException
- | InstantiationException
- | InvocationTargetException ex) {
+ | InstantiationException ex) {
throw new RuntimeException(ex);
}
diff --git a/packages/SystemUI/src/com/android/systemui/VendorServices.java b/packages/SystemUI/src/com/android/systemui/VendorServices.java
index 139448c0..a320939 100644
--- a/packages/SystemUI/src/com/android/systemui/VendorServices.java
+++ b/packages/SystemUI/src/com/android/systemui/VendorServices.java
@@ -16,15 +16,12 @@
package com.android.systemui;
-import android.content.Context;
-
/**
* Placeholder for any vendor-specific services.
*/
-public class VendorServices extends CoreStartable {
+public class VendorServices implements CoreStartable {
- public VendorServices(Context context) {
- super(context);
+ public VendorServices() {
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index a1288b5..9f1c9b4 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -69,7 +69,7 @@
* Class to register system actions with accessibility framework.
*/
@SysUISingleton
-public class SystemActions extends CoreStartable {
+public class SystemActions implements CoreStartable {
private static final String TAG = "SystemActions";
/**
@@ -177,6 +177,7 @@
private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
private final SystemActionsBroadcastReceiver mReceiver;
+ private final Context mContext;
private final Optional<Recents> mRecentsOptional;
private Locale mLocale;
private final AccessibilityManager mA11yManager;
@@ -190,7 +191,7 @@
NotificationShadeWindowController notificationShadeController,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
Optional<Recents> recentsOptional) {
- super(context);
+ mContext = context;
mRecentsOptional = recentsOptional;
mReceiver = new SystemActionsBroadcastReceiver();
mLocale = mContext.getResources().getConfiguration().getLocales().get(0);
@@ -219,7 +220,6 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
final Locale locale = mContext.getResources().getConfiguration().getLocales().get(0);
if (!locale.equals(mLocale)) {
mLocale = locale;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index f4701ed..4f03b63 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -53,11 +53,12 @@
* when {@code IStatusBar#requestWindowMagnificationConnection(boolean)} is called.
*/
@SysUISingleton
-public class WindowMagnification extends CoreStartable implements WindowMagnifierCallback,
+public class WindowMagnification implements CoreStartable, WindowMagnifierCallback,
CommandQueue.Callbacks {
private static final String TAG = "WindowMagnification";
private final ModeSwitchesController mModeSwitchesController;
+ private final Context mContext;
private final Handler mHandler;
private final AccessibilityManager mAccessibilityManager;
private final CommandQueue mCommandQueue;
@@ -108,7 +109,7 @@
public WindowMagnification(Context context, @Main Handler mainHandler,
CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
SysUiState sysUiState, OverviewProxyService overviewProxyService) {
- super(context);
+ mContext = context;
mHandler = mainHandler;
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mCommandQueue = commandQueue;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index d1bc968..242a598 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -104,7 +104,7 @@
* {@link com.android.keyguard.KeyguardUpdateMonitor}
*/
@SysUISingleton
-public class AuthController extends CoreStartable implements CommandQueue.Callbacks,
+public class AuthController implements CoreStartable, CommandQueue.Callbacks,
AuthDialogCallback, DozeReceiver {
private static final String TAG = "AuthController";
@@ -112,6 +112,7 @@
private static final int SENSOR_PRIVACY_DELAY = 500;
private final Handler mHandler;
+ private final Context mContext;
private final Execution mExecution;
private final CommandQueue mCommandQueue;
private final StatusBarStateController mStatusBarStateController;
@@ -697,7 +698,7 @@
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibrator) {
- super(context);
+ mContext = context;
mExecution = execution;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
@@ -1152,8 +1153,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
+ public void onConfigurationChanged(Configuration newConfig) {
updateSensorLocations();
// Save the state of the current dialog (buttons showing, etc)
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt
index d7b263a..c536e81 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt
@@ -16,16 +16,14 @@
package com.android.systemui.broadcast
-import android.content.Context
import com.android.systemui.CoreStartable
import javax.inject.Inject
class BroadcastDispatcherStartable @Inject constructor(
- context: Context,
val broadcastDispatcher: BroadcastDispatcher
-) : CoreStartable(context) {
+) : CoreStartable {
override fun start() {
broadcastDispatcher.initialize()
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index f526277..05e3f1c 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -39,8 +39,8 @@
* ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
*/
@SysUISingleton
-public class ClipboardListener extends CoreStartable
- implements ClipboardManager.OnPrimaryClipChangedListener {
+public class ClipboardListener implements
+ CoreStartable, ClipboardManager.OnPrimaryClipChangedListener {
private static final String TAG = "ClipboardListener";
@VisibleForTesting
@@ -49,6 +49,7 @@
static final String EXTRA_SUPPRESS_OVERLAY =
"com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
+ private final Context mContext;
private final DeviceConfigProxy mDeviceConfig;
private final ClipboardOverlayControllerFactory mOverlayFactory;
private final ClipboardManager mClipboardManager;
@@ -59,7 +60,7 @@
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager,
UiEventLogger uiEventLogger) {
- super(context);
+ mContext = context;
mDeviceConfig = deviceConfigProxy;
mOverlayFactory = overlayFactory;
mClipboardManager = clipboardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index c2dffe8..d05bd51 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -28,6 +28,7 @@
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.unfold.FoldStateLogger;
import com.android.systemui.unfold.FoldStateLoggingProvider;
@@ -63,6 +64,7 @@
@Subcomponent(modules = {
DefaultComponentBinder.class,
DependencyProvider.class,
+ QsFrameTranslateModule.class,
SystemUIBinder.class,
SystemUIModule.class,
SystemUICoreStartableModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d70b971..dc3dadb 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -61,7 +61,6 @@
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
@@ -133,7 +132,6 @@
PeopleModule.class,
PluginModule.class,
PrivacyModule.class,
- QsFrameTranslateModule.class,
ScreenshotModule.class,
SensorModule.class,
MultiUserUtilsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
index 99ca3c7..d145f5c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
@@ -40,11 +40,12 @@
* {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be
* the designated dream overlay component.
*/
-public class DreamOverlayRegistrant extends CoreStartable {
+public class DreamOverlayRegistrant implements CoreStartable {
private static final String TAG = "DreamOverlayRegistrant";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final IDreamManager mDreamManager;
private final ComponentName mOverlayServiceComponent;
+ private final Context mContext;
private final Resources mResources;
private boolean mCurrentRegisteredState = false;
@@ -98,7 +99,7 @@
@Inject
public DreamOverlayRegistrant(Context context, @Main Resources resources) {
- super(context);
+ mContext = context;
mResources = resources;
mDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.getService(DreamService.DREAM_SERVICE));
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
index bbcab60..ee2f1af 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java
@@ -16,7 +16,6 @@
package com.android.systemui.dreams.complication;
-import android.content.Context;
import android.database.ContentObserver;
import android.os.UserHandle;
import android.provider.Settings;
@@ -37,7 +36,7 @@
* user, and pushes updates to {@link DreamOverlayStateController}.
*/
@SysUISingleton
-public class ComplicationTypesUpdater extends CoreStartable {
+public class ComplicationTypesUpdater implements CoreStartable {
private final DreamBackend mDreamBackend;
private final Executor mExecutor;
private final SecureSettings mSecureSettings;
@@ -45,13 +44,11 @@
private final DreamOverlayStateController mDreamOverlayStateController;
@Inject
- ComplicationTypesUpdater(Context context,
+ ComplicationTypesUpdater(
DreamBackend dreamBackend,
@Main Executor executor,
SecureSettings secureSettings,
DreamOverlayStateController dreamOverlayStateController) {
- super(context);
-
mDreamBackend = dreamBackend;
mExecutor = executor;
mSecureSettings = secureSettings;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
index 675a2f4..77e1fc9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
@@ -19,7 +19,6 @@
import static com.android.systemui.dreams.complication.dagger.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW;
import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS;
-import android.content.Context;
import android.view.View;
import com.android.systemui.CoreStartable;
@@ -61,7 +60,7 @@
* {@link CoreStartable} responsible for registering {@link DreamClockTimeComplication} with
* SystemUI.
*/
- public static class Registrant extends CoreStartable {
+ public static class Registrant implements CoreStartable {
private final DreamOverlayStateController mDreamOverlayStateController;
private final DreamClockTimeComplication mComplication;
@@ -69,10 +68,9 @@
* Default constructor to register {@link DreamClockTimeComplication}.
*/
@Inject
- public Registrant(Context context,
+ public Registrant(
DreamOverlayStateController dreamOverlayStateController,
DreamClockTimeComplication dreamClockTimeComplication) {
- super(context);
mDreamOverlayStateController = dreamOverlayStateController;
mComplication = dreamClockTimeComplication;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 821e13e..0ccb222 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -71,7 +71,7 @@
/**
* {@link CoreStartable} for registering the complication with SystemUI on startup.
*/
- public static class Registrant extends CoreStartable {
+ public static class Registrant implements CoreStartable {
private final DreamHomeControlsComplication mComplication;
private final DreamOverlayStateController mDreamOverlayStateController;
private final ControlsComponent mControlsComponent;
@@ -90,11 +90,9 @@
};
@Inject
- public Registrant(Context context, DreamHomeControlsComplication complication,
+ public Registrant(DreamHomeControlsComplication complication,
DreamOverlayStateController dreamOverlayStateController,
ControlsComponent controlsComponent) {
- super(context);
-
mComplication = complication;
mControlsComponent = controlsComponent;
mDreamOverlayStateController = dreamOverlayStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
index a981f25..c3aaf0c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
@@ -61,7 +61,7 @@
* {@link CoreStartable} responsbile for registering {@link SmartSpaceComplication} with
* SystemUI.
*/
- public static class Registrant extends CoreStartable {
+ public static class Registrant implements CoreStartable {
private final DreamSmartspaceController mSmartSpaceController;
private final DreamOverlayStateController mDreamOverlayStateController;
private final SmartSpaceComplication mComplication;
@@ -78,11 +78,10 @@
* Default constructor for {@link SmartSpaceComplication}.
*/
@Inject
- public Registrant(Context context,
+ public Registrant(
DreamOverlayStateController dreamOverlayStateController,
SmartSpaceComplication smartSpaceComplication,
DreamSmartspaceController smartSpaceController) {
- super(context);
mDreamOverlayStateController = dreamOverlayStateController;
mComplication = smartSpaceComplication;
mSmartSpaceController = smartSpaceController;
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index c0e3021..560dcbd 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -16,9 +16,7 @@
package com.android.systemui.flags
-import android.content.Context
import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
import dagger.Binds
@@ -30,12 +28,11 @@
class FeatureFlagsDebugStartable
@Inject
constructor(
- @Application context: Context,
dumpManager: DumpManager,
private val commandRegistry: CommandRegistry,
private val flagCommand: FlagCommand,
featureFlags: FeatureFlags
-) : CoreStartable(context) {
+) : CoreStartable {
init {
dumpManager.registerDumpable(FeatureFlagsDebug.TAG) { pw, args ->
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
index f138f1e..e7d8cc3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt
@@ -16,9 +16,7 @@
package com.android.systemui.flags
-import android.content.Context
import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
import dagger.Binds
import dagger.Module
@@ -28,8 +26,7 @@
class FeatureFlagsReleaseStartable
@Inject
-constructor(@Application context: Context, dumpManager: DumpManager, featureFlags: FeatureFlags) :
- CoreStartable(context) {
+constructor(dumpManager: DumpManager, featureFlags: FeatureFlags) : CoreStartable {
init {
dumpManager.registerDumpable(FeatureFlagsRelease.TAG) { pw, args ->
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
index 74d5bd5..9f321d8 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
@@ -36,8 +36,7 @@
* Manages power menu plugins and communicates power menu actions to the CentralSurfaces.
*/
@SysUISingleton
-public class GlobalActionsComponent extends CoreStartable
- implements Callbacks, GlobalActionsManager {
+public class GlobalActionsComponent implements CoreStartable, Callbacks, GlobalActionsManager {
private final CommandQueue mCommandQueue;
private final ExtensionController mExtensionController;
@@ -48,11 +47,10 @@
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Inject
- public GlobalActionsComponent(Context context, CommandQueue commandQueue,
+ public GlobalActionsComponent(CommandQueue commandQueue,
ExtensionController extensionController,
Provider<GlobalActions> globalActionsProvider,
StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
- super(context);
mCommandQueue = commandQueue;
mExtensionController = extensionController;
mGlobalActionsProvider = globalActionsProvider;
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index 568143c..4f1a2b3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -27,7 +27,6 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.res.Configuration;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.HandlerThread;
@@ -66,7 +65,7 @@
/** */
@SysUISingleton
-public class KeyboardUI extends CoreStartable implements InputManager.OnTabletModeChangedListener {
+public class KeyboardUI implements CoreStartable, InputManager.OnTabletModeChangedListener {
private static final String TAG = "KeyboardUI";
private static final boolean DEBUG = false;
@@ -127,13 +126,12 @@
@Inject
public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider) {
- super(context);
+ mContext = context;
this.mBluetoothManagerProvider = bluetoothManagerProvider;
}
@Override
public void start() {
- mContext = super.mContext;
HandlerThread thread = new HandlerThread("Keyboard", Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
mHandler = new KeyboardHandler(thread.getLooper());
@@ -141,10 +139,6 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
- }
-
- @Override
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyboardUI:");
pw.println(" mEnabled=" + mEnabled);
@@ -156,7 +150,7 @@
}
@Override
- protected void onBootCompleted() {
+ public void onBootCompleted() {
mHandler.sendEmptyMessage(MSG_ON_BOOT_COMPLETED);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index aee70ee..99de9eb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -186,7 +186,7 @@
* directly to the keyguard UI is posted to a {@link android.os.Handler} to ensure it is taken on the UI
* thread of the keyguard.
*/
-public class KeyguardViewMediator extends CoreStartable implements Dumpable,
+public class KeyguardViewMediator implements CoreStartable, Dumpable,
StatusBarStateController.StateListener {
private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000;
@@ -272,6 +272,7 @@
private boolean mShuttingDown;
private boolean mDozing;
private boolean mAnimatingScreenOff;
+ private final Context mContext;
private final FalsingCollector mFalsingCollector;
/** High level access to the power manager for WakeLocks */
@@ -1128,7 +1129,7 @@
DreamOverlayStateController dreamOverlayStateController,
Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator) {
- super(context);
+ mContext = context;
mFalsingCollector = falsingCollector;
mLockPatternUtils = lockPatternUtils;
mBroadcastDispatcher = broadcastDispatcher;
diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
index 8f9357a..c7e4c5e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
@@ -21,7 +21,6 @@
import static android.app.StatusBarManager.SESSION_KEYGUARD;
import android.annotation.Nullable;
-import android.content.Context;
import android.os.RemoteException;
import android.util.Log;
@@ -48,7 +47,7 @@
* session. Can be used across processes via StatusBarManagerService#registerSessionListener
*/
@SysUISingleton
-public class SessionTracker extends CoreStartable {
+public class SessionTracker implements CoreStartable {
private static final String TAG = "SessionTracker";
private static final boolean DEBUG = false;
@@ -65,13 +64,11 @@
@Inject
public SessionTracker(
- Context context,
IStatusBarService statusBarService,
AuthController authController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
KeyguardStateController keyguardStateController
) {
- super(context);
mStatusBarManagerService = statusBarService;
mAuthController = authController;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 9dd18b2..80bff83 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -11,6 +11,7 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.PathInterpolator
import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.InstanceId
@@ -95,7 +96,8 @@
* finished
*/
@MediaLocation
- private var currentEndLocation: Int = -1
+ @VisibleForTesting
+ var currentEndLocation: Int = -1
/**
* The ending location of the view where it ends when all animations and transitions have
@@ -126,7 +128,8 @@
lateinit var settingsButton: View
private set
private val mediaContent: ViewGroup
- private val pageIndicator: PageIndicator
+ @VisibleForTesting
+ val pageIndicator: PageIndicator
private val visualStabilityCallback: OnReorderingAllowedListener
private var needsReordering: Boolean = false
private var keysNeedRemoval = mutableSetOf<String>()
@@ -149,6 +152,27 @@
}
}
}
+
+ companion object {
+ const val ANIMATION_BASE_DURATION = 2200f
+ const val DURATION = 167f
+ const val DETAILS_DELAY = 1067f
+ const val CONTROLS_DELAY = 1400f
+ const val PAGINATION_DELAY = 1900f
+ const val MEDIATITLES_DELAY = 1000f
+ const val MEDIACONTAINERS_DELAY = 967f
+ val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F)
+ val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F)
+
+ fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
+ val transformStartFraction = delay / ANIMATION_BASE_DURATION
+ val transformDurationFraction = duration / ANIMATION_BASE_DURATION
+ val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
+ return MathUtils.constrain((squishinessToTime - transformStartFraction) /
+ transformDurationFraction, 0F, 1F)
+ }
+ }
+
private val configListener = object : ConfigurationController.ConfigurationListener {
override fun onDensityOrFontScaleChanged() {
// System font changes should only happen when UMO is offscreen or a flicker may occur
@@ -633,12 +657,17 @@
}
}
- private fun updatePageIndicatorAlpha() {
+ @VisibleForTesting
+ fun updatePageIndicatorAlpha() {
val hostStates = mediaHostStatesManager.mediaHostStates
val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
val startAlpha = if (startIsVisible) 1.0f else 0.0f
- val endAlpha = if (endIsVisible) 1.0f else 0.0f
+ // when squishing in split shade, only use endState, which keeps changing
+ // to provide squishFraction
+ val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
+ val endAlpha = (if (endIsVisible) 1.0f else 0.0f) *
+ calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
var alpha = 1.0f
if (!endIsVisible || !startIsVisible) {
var progress = currentTransitionProgress
@@ -687,6 +716,7 @@
mediaCarouselScrollHandler.setCarouselBounds(
currentCarouselWidth, currentCarouselHeight)
updatePageIndicatorLocation()
+ updatePageIndicatorAlpha()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
index ef49fd3..a776897 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
@@ -47,7 +47,7 @@
* were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
*/
private val translationConfig = PhysicsAnimator.SpringConfig(
- SpringForce.STIFFNESS_MEDIUM,
+ SpringForce.STIFFNESS_LOW,
SpringForce.DAMPING_RATIO_LOW_BOUNCY)
/**
@@ -289,7 +289,10 @@
return false
}
}
- if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
+ if (motionEvent.action == MotionEvent.ACTION_MOVE) {
+ // cancel on going animation if there is any.
+ PhysicsAnimator.getInstance(this).cancel()
+ } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
// It's an up and the fling didn't take it above
val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
val scrollXAmount: Int
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index bffb0fd..8645922 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -203,6 +203,14 @@
}
}
+ override var squishFraction: Float = 1.0f
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
override var showsOnlyActiveMedia: Boolean = false
set(value) {
if (!value.equals(field)) {
@@ -253,6 +261,7 @@
override fun copy(): MediaHostState {
val mediaHostState = MediaHostStateHolder()
mediaHostState.expansion = expansion
+ mediaHostState.squishFraction = squishFraction
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible
@@ -271,6 +280,9 @@
if (expansion != other.expansion) {
return false
}
+ if (squishFraction != other.squishFraction) {
+ return false
+ }
if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
return false
}
@@ -289,6 +301,7 @@
override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode()
+ result = 31 * result + squishFraction.hashCode()
result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2
@@ -329,6 +342,11 @@
var expansion: Float
/**
+ * Fraction of the height animation.
+ */
+ var squishFraction: Float
+
+ /**
* Is this host only showing active media or is it showing all of them including resumption?
*/
var showsOnlyActiveMedia: Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
index ac59175..faa7aae 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -18,8 +18,15 @@
import android.content.Context
import android.content.res.Configuration
+import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.R
+import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.TransitionLayout
@@ -50,6 +57,24 @@
companion object {
@JvmField
val GUTS_ANIMATION_DURATION = 500L
+ val controlIds = setOf(
+ R.id.media_progress_bar,
+ R.id.actionNext,
+ R.id.actionPrev,
+ R.id.action0,
+ R.id.action1,
+ R.id.action2,
+ R.id.action3,
+ R.id.action4,
+ R.id.media_scrubbing_elapsed_time,
+ R.id.media_scrubbing_total_time
+ )
+
+ val detailIds = setOf(
+ R.id.header_title,
+ R.id.header_artist,
+ R.id.actionPlayPause,
+ )
}
/**
@@ -57,6 +82,7 @@
*/
lateinit var sizeChangedListener: () -> Unit
private var firstRefresh: Boolean = true
+ @VisibleForTesting
private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController()
private var animationDelay: Long = 0
@@ -279,10 +305,47 @@
}
/**
+ * Apply squishFraction to a copy of viewState such that the cached version is untouched.
+ */
+ internal fun squishViewState(
+ viewState: TransitionViewState,
+ squishFraction: Float
+ ): TransitionViewState {
+ val squishedViewState = viewState.copy()
+ squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
+ controlIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
+ }
+ }
+
+ detailIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
+ }
+ }
+
+ RecommendationViewHolder.mediaContainersIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
+ }
+ }
+
+ RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
+ }
+ }
+
+ return squishedViewState
+ }
+
+ /**
* Obtain a new viewState for a given media state. This usually returns a cached state, but if
* it's not available, it will recreate one by measuring, which may be expensive.
*/
- private fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+ @VisibleForTesting
+ fun obtainViewState(state: MediaHostState?): TransitionViewState? {
if (state == null || state.measurementInput == null) {
return null
}
@@ -291,41 +354,46 @@
val viewState = viewStates[cacheKey]
if (viewState != null) {
// we already have cached this measurement, let's continue
+ if (state.squishFraction <= 1f) {
+ return squishViewState(viewState, state.squishFraction)
+ }
return viewState
}
// Copy the key since this might call recursively into it and we're using tmpKey
cacheKey = cacheKey.copy()
val result: TransitionViewState?
- if (transitionLayout != null) {
- // Let's create a new measurement
- if (state.expansion == 0.0f || state.expansion == 1.0f) {
- result = transitionLayout!!.calculateViewState(
- state.measurementInput!!,
- constraintSetForExpansion(state.expansion),
- TransitionViewState())
+ if (transitionLayout == null) {
+ return null
+ }
+ // Let's create a new measurement
+ if (state.expansion == 0.0f || state.expansion == 1.0f) {
+ result = transitionLayout!!.calculateViewState(
+ state.measurementInput!!,
+ constraintSetForExpansion(state.expansion),
+ TransitionViewState())
- setGutsViewState(result)
- // We don't want to cache interpolated or null states as this could quickly fill up
- // our cache. We only cache the start and the end states since the interpolation
- // is cheap
- viewStates[cacheKey] = result
- } else {
- // This is an interpolated state
- val startState = state.copy().also { it.expansion = 0.0f }
-
- // Given that we have a measurement and a view, let's get (guaranteed) viewstates
- // from the start and end state and interpolate them
- val startViewState = obtainViewState(startState) as TransitionViewState
- val endState = state.copy().also { it.expansion = 1.0f }
- val endViewState = obtainViewState(endState) as TransitionViewState
- result = layoutController.getInterpolatedState(
- startViewState,
- endViewState,
- state.expansion)
- }
+ setGutsViewState(result)
+ // We don't want to cache interpolated or null states as this could quickly fill up
+ // our cache. We only cache the start and the end states since the interpolation
+ // is cheap
+ viewStates[cacheKey] = result
} else {
- result = null
+ // This is an interpolated state
+ val startState = state.copy().also { it.expansion = 0.0f }
+
+ // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+ // from the start and end state and interpolate them
+ val startViewState = obtainViewState(startState) as TransitionViewState
+ val endState = state.copy().also { it.expansion = 1.0f }
+ val endViewState = obtainViewState(endState) as TransitionViewState
+ result = layoutController.getInterpolatedState(
+ startViewState,
+ endViewState,
+ state.expansion)
+ }
+ if (state.squishFraction <= 1f) {
+ return squishViewState(result, state.squishFraction)
}
return result
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
index 52ac4e0..8ae75fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
@@ -106,5 +106,20 @@
R.id.media_subtitle2,
R.id.media_subtitle3
)
+
+ val mediaTitlesAndSubtitlesIds = setOf(
+ R.id.media_title1,
+ R.id.media_title2,
+ R.id.media_title3,
+ R.id.media_subtitle1,
+ R.id.media_subtitle2,
+ R.id.media_subtitle3
+ )
+
+ val mediaContainersIds = setOf(
+ R.id.media_cover1_container,
+ R.id.media_cover2_container,
+ R.id.media_cover3_container
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index 0b9b32b..2a8168b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -51,9 +51,10 @@
* {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
*/
@SysUISingleton
-public class RingtonePlayer extends CoreStartable {
+public class RingtonePlayer implements CoreStartable {
private static final String TAG = "RingtonePlayer";
private static final boolean LOGD = false;
+ private final Context mContext;
// TODO: support Uri switching under same IBinder
@@ -64,7 +65,7 @@
@Inject
public RingtonePlayer(Context context) {
- super(context);
+ mContext = context;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index 53b4d43..91e7b49 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -18,7 +18,6 @@
import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION;
-import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -38,7 +37,7 @@
* {@link MediaDreamSentinel} is responsible for tracking media state and registering/unregistering
* the media complication as appropriate
*/
-public class MediaDreamSentinel extends CoreStartable {
+public class MediaDreamSentinel implements CoreStartable {
private static final String TAG = "MediaDreamSentinel";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -113,11 +112,10 @@
private final FeatureFlags mFeatureFlags;
@Inject
- public MediaDreamSentinel(Context context, MediaDataManager mediaDataManager,
+ public MediaDreamSentinel(MediaDataManager mediaDataManager,
DreamOverlayStateController dreamOverlayStateController,
DreamMediaEntryComplication mediaEntryComplication,
FeatureFlags featureFlags) {
- super(context);
mMediaDataManager = mediaDataManager;
mDreamOverlayStateController = dreamOverlayStateController;
mMediaEntryComplication = mediaEntryComplication;
diff --git a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
index d60172a..0ba5f28 100644
--- a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
@@ -40,7 +40,7 @@
* documented at {@link #handleTaskStackChanged} apply.
*/
@SysUISingleton
-public class HomeSoundEffectController extends CoreStartable {
+public class HomeSoundEffectController implements CoreStartable {
private static final String TAG = "HomeSoundEffectController";
private final AudioManager mAudioManager;
@@ -65,7 +65,6 @@
TaskStackChangeListeners taskStackChangeListeners,
ActivityManagerWrapper activityManagerWrapper,
PackageManager packageManager) {
- super(context);
mAudioManager = audioManager;
mTaskStackChangeListeners = taskStackChangeListeners;
mActivityManagerWrapper = activityManagerWrapper;
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index f5caefb..a4a96806 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -43,7 +43,7 @@
private val commandRegistry: CommandRegistry,
private val context: Context,
@Main private val mainExecutor: Executor
-) : CoreStartable(context) {
+) : CoreStartable {
/** All commands for the sender device. */
inner class SenderCommand : Command {
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 67dae9e..fae0b50 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -59,7 +59,7 @@
import dagger.Lazy;
@SysUISingleton
-public class PowerUI extends CoreStartable implements CommandQueue.Callbacks {
+public class PowerUI implements CoreStartable, CommandQueue.Callbacks {
static final String TAG = "PowerUI";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -103,6 +103,7 @@
private IThermalEventListener mSkinThermalEventListener;
private IThermalEventListener mUsbThermalEventListener;
+ private final Context mContext;
private final BroadcastDispatcher mBroadcastDispatcher;
private final CommandQueue mCommandQueue;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -112,7 +113,7 @@
CommandQueue commandQueue, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
WarningsUI warningsUI, EnhancedEstimates enhancedEstimates,
PowerManager powerManager) {
- super(context);
+ mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
mCommandQueue = commandQueue;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
@@ -169,7 +170,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
final int mask = ActivityInfo.CONFIG_MCC | ActivityInfo.CONFIG_MNC;
// Safe to modify mLastConfiguration here as it's only updated by the main thread (here).
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/TvPrivacyChipsController.java b/packages/SystemUI/src/com/android/systemui/privacy/television/TvPrivacyChipsController.java
index 829077b..1c47799 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/television/TvPrivacyChipsController.java
+++ b/packages/SystemUI/src/com/android/systemui/privacy/television/TvPrivacyChipsController.java
@@ -70,8 +70,8 @@
* recording audio, camera, the screen, or accessing the location.
*/
@SysUISingleton
-public class TvPrivacyChipsController extends CoreStartable
- implements PrivacyItemController.Callback {
+public class TvPrivacyChipsController
+ implements CoreStartable, PrivacyItemController.Callback {
private static final String TAG = "TvPrivacyChipsController";
private static final boolean DEBUG = false;
@@ -106,6 +106,7 @@
// How long chips stay expanded after an update.
private static final int EXPANDED_DURATION_MS = 4000;
+ private final Context mContext;
private final Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
private final Runnable mCollapseRunnable = this::collapseChips;
private final Runnable mUpdatePrivacyItemsRunnable = this::updateChipsAndAnnounce;
@@ -130,7 +131,7 @@
@Inject
public TvPrivacyChipsController(Context context, PrivacyItemController privacyItemController,
IWindowManager iWindowManager) {
- super(context);
+ mContext = context;
if (DEBUG) Log.d(TAG, "TvPrivacyChipsController running");
mPrivacyItemController = privacyItemController;
mIWindowManager = iWindowManager;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 7a44058..498a98b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -691,6 +691,15 @@
if (mQSAnimator != null) {
mQSAnimator.setPosition(expansion);
}
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD
+ || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
+ // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen
+ // and media player expect no change by squishiness in lock screen shade
+ mQsMediaHost.setSquishFraction(1.0F);
+ } else {
+ mQsMediaHost.setSquishFraction(mSquishinessFraction);
+ }
+
}
private void setAlphaAnimationProgress(float progress) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 3e445dd..d393680 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -36,6 +36,7 @@
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Main;
@@ -182,6 +183,10 @@
setBindService(true);
}
+ /**
+ * Binds or unbinds to IQSService
+ */
+ @WorkerThread
public void setBindService(boolean bind) {
if (mBound && mUnbindImmediate) {
// If we are already bound and expecting to unbind, this means we should stay bound
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index 9b3b843..b041f95 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -29,13 +29,14 @@
/**
* A proxy to a Recents implementation.
*/
-public class Recents extends CoreStartable implements CommandQueue.Callbacks {
+public class Recents implements CoreStartable, CommandQueue.Callbacks {
+ private final Context mContext;
private final RecentsImplementation mImpl;
private final CommandQueue mCommandQueue;
public Recents(Context context, RecentsImplementation impl, CommandQueue commandQueue) {
- super(context);
+ mContext = context;
mImpl = impl;
mCommandQueue = commandQueue;
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 309059f..95cc0dc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -76,7 +76,7 @@
)
} else {
// Create a new request of the same type which includes the top component
- ScreenshotRequest(request.source, request.type, info.component)
+ ScreenshotRequest(request.type, request.source, info.component)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
index ad073c0..d450afa 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
@@ -42,11 +42,11 @@
@SysUISingleton
class UserFileManagerImpl @Inject constructor(
// Context of system process and system user.
- val context: Context,
+ private val context: Context,
val userManager: UserManager,
val broadcastDispatcher: BroadcastDispatcher,
@Background val backgroundExecutor: DelayableExecutor
-) : UserFileManager, CoreStartable(context) {
+) : UserFileManager, CoreStartable {
companion object {
private const val FILES = "files"
@VisibleForTesting internal const val SHARED_PREFS = "shared_prefs"
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 6d76c17..e331812 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -17,6 +17,8 @@
package com.android.systemui.shade;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
import static androidx.constraintlayout.widget.ConstraintSet.END;
import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
@@ -26,8 +28,12 @@
import static com.android.keyguard.KeyguardClockSwitch.SMALL;
import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE;
import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
+import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.shade.NotificationPanelView.DEBUG;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
@@ -41,6 +47,8 @@
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
import static com.android.systemui.util.DumpUtilsKt.asIndenting;
+import static java.lang.Float.isNaN;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
@@ -48,6 +56,8 @@
import android.app.Fragment;
import android.app.StatusBarManager;
import android.content.ContentResolver;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -73,11 +83,13 @@
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
+import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.View.AccessibilityDelegate;
+import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.ViewStub;
@@ -86,6 +98,7 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
@@ -178,6 +191,7 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.phone.BounceInterpolator;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -230,8 +244,13 @@
import javax.inject.Provider;
@CentralSurfacesComponent.CentralSurfacesScope
-public final class NotificationPanelViewController extends PanelViewController {
+public final class NotificationPanelViewController {
+ public static final String TAG = NotificationPanelView.class.getSimpleName();
+ public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
+ public static final float FLING_SPEED_UP_FACTOR = 0.6f;
+ public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f;
+ public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f;
private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean DEBUG_DRAWABLE = false;
@@ -262,6 +281,22 @@
ActivityLaunchAnimator.TIMINGS.getTotalDuration()
- CollapsedStatusBarFragment.FADE_IN_DURATION
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
+ private static final int NO_FIXED_DURATION = -1;
+ private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
+ private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L;
+ /**
+ * The factor of the usual high velocity that is needed in order to reach the maximum overshoot
+ * when flinging. A low value will make it that most flings will reach the maximum overshoot.
+ */
+ private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
+ private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+ private final Resources mResources;
+ private final KeyguardStateController mKeyguardStateController;
+ private final SysuiStatusBarStateController mStatusBarStateController;
+ private final AmbientState mAmbientState;
+ private final LockscreenGestureLogger mLockscreenGestureLogger;
+ private final SystemClock mSystemClock;
+ private final ShadeLogger mShadeLog;
private final DozeParameters mDozeParameters;
private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener();
@@ -333,6 +368,28 @@
private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
private final RecordingController mRecordingController;
private final PanelEventsEmitter mPanelEventsEmitter;
+ private final boolean mVibrateOnOpening;
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private final FlingAnimationUtils mFlingAnimationUtilsClosing;
+ private final FlingAnimationUtils mFlingAnimationUtilsDismissing;
+ private final LatencyTracker mLatencyTracker;
+ private final DozeLog mDozeLog;
+ /** Whether or not the NotificationPanelView can be expanded or collapsed with a drag. */
+ private final boolean mNotificationsDragEnabled;
+ private final Interpolator mBounceInterpolator;
+ private final NotificationShadeWindowController mNotificationShadeWindowController;
+ private final ShadeExpansionStateManager mShadeExpansionStateManager;
+ private long mDownTime;
+ private boolean mTouchSlopExceededBeforeDown;
+ private boolean mIsLaunchAnimationRunning;
+ private float mOverExpansion;
+ private CentralSurfaces mCentralSurfaces;
+ private HeadsUpManagerPhone mHeadsUpManager;
+ private float mExpandedHeight = 0;
+ private boolean mTracking;
+ private boolean mHintAnimationRunning;
+ private KeyguardBottomAreaView mKeyguardBottomArea;
+ private boolean mExpanding;
private boolean mSplitShadeEnabled;
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
private float mKeyguardNotificationBottomPadding;
@@ -706,6 +763,54 @@
private final CameraGestureHelper mCameraGestureHelper;
private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+ private float mMinExpandHeight;
+ private boolean mPanelUpdateWhenAnimatorEnds;
+ private boolean mHasVibratedOnOpen = false;
+ private int mFixedDuration = NO_FIXED_DURATION;
+ /** The overshoot amount when the panel flings open. */
+ private float mPanelFlingOvershootAmount;
+ /** The amount of pixels that we have overexpanded the last time with a gesture. */
+ private float mLastGesturedOverExpansion = -1;
+ /** Whether the current animator is the spring back animation. */
+ private boolean mIsSpringBackAnimation;
+ private boolean mInSplitShade;
+ private float mHintDistance;
+ private float mInitialOffsetOnTouch;
+ private boolean mCollapsedAndHeadsUpOnDown;
+ private float mExpandedFraction = 0;
+ private float mExpansionDragDownAmountPx = 0;
+ private boolean mPanelClosedOnDown;
+ private boolean mHasLayoutedSinceDown;
+ private float mUpdateFlingVelocity;
+ private boolean mUpdateFlingOnLayout;
+ private boolean mClosing;
+ private boolean mTouchSlopExceeded;
+ private int mTrackingPointer;
+ private int mTouchSlop;
+ private float mSlopMultiplier;
+ private boolean mTouchAboveFalsingThreshold;
+ private boolean mTouchStartedInEmptyArea;
+ private boolean mMotionAborted;
+ private boolean mUpwardsWhenThresholdReached;
+ private boolean mAnimatingOnDown;
+ private boolean mHandlingPointerUp;
+ private ValueAnimator mHeightAnimator;
+ /** Whether an instant expand request is currently pending and we are waiting for layout. */
+ private boolean mInstantExpanding;
+ private boolean mAnimateAfterExpanding;
+ private boolean mIsFlinging;
+ private String mViewName;
+ private float mInitialExpandY;
+ private float mInitialExpandX;
+ private boolean mTouchDisabled;
+ private boolean mInitialTouchFromKeyguard;
+ /** Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. */
+ private float mNextCollapseSpeedUpFactor = 1.0f;
+ private boolean mGestureWaitForTouchSlop;
+ private boolean mIgnoreXTouchSlop;
+ private boolean mExpandLatencyTracking;
+ private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
+ mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@@ -775,32 +880,73 @@
CameraGestureHelper cameraGestureHelper,
KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
KeyguardBottomAreaInteractor keyguardBottomAreaInteractor) {
- super(view,
- falsingManager,
- dozeLog,
- keyguardStateController,
- (SysuiStatusBarStateController) statusBarStateController,
- notificationShadeWindowController,
- vibratorHelper,
- statusBarKeyguardViewManager,
- latencyTracker,
- flingAnimationUtilsBuilder.get(),
- statusBarTouchableRegionManager,
- lockscreenGestureLogger,
- shadeExpansionStateManager,
- ambientState,
- interactionJankMonitor,
- shadeLogger,
- systemClock);
+ keyguardStateController.addCallback(new KeyguardStateController.Callback() {
+ @Override
+ public void onKeyguardFadingAwayChanged() {
+ updateExpandedHeightToMaxHeight();
+ }
+ });
+ mAmbientState = ambientState;
mView = view;
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ mLockscreenGestureLogger = lockscreenGestureLogger;
+ mShadeExpansionStateManager = shadeExpansionStateManager;
+ mShadeLog = shadeLogger;
+ TouchHandler touchHandler = createTouchHandler();
+ mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mViewName = mResources.getResourceName(mView.getId());
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
+ });
+
+ mView.addOnLayoutChangeListener(createLayoutChangeListener());
+ mView.setOnTouchListener(touchHandler);
+ mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
+
+ mResources = mView.getResources();
+ mKeyguardStateController = keyguardStateController;
+ mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
+ mNotificationShadeWindowController = notificationShadeWindowController;
+ FlingAnimationUtils.Builder fauBuilder = flingAnimationUtilsBuilder.get();
+ mFlingAnimationUtils = fauBuilder
+ .reset()
+ .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
+ .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
+ .build();
+ mFlingAnimationUtilsClosing = fauBuilder
+ .reset()
+ .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS)
+ .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR)
+ .build();
+ mFlingAnimationUtilsDismissing = fauBuilder
+ .reset()
+ .setMaxLengthSeconds(0.5f)
+ .setSpeedUpFactor(0.6f)
+ .setX2(0.6f)
+ .setY2(0.84f)
+ .build();
+ mLatencyTracker = latencyTracker;
+ mBounceInterpolator = new BounceInterpolator();
+ mFalsingManager = falsingManager;
+ mDozeLog = dozeLog;
+ mNotificationsDragEnabled = mResources.getBoolean(
+ R.bool.config_enableNotificationShadeDrag);
mVibratorHelper = vibratorHelper;
+ mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
+ mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
+ mInteractionJankMonitor = interactionJankMonitor;
+ mSystemClock = systemClock;
mKeyguardMediaController = keyguardMediaController;
mPrivacyDotViewController = privacyDotViewController;
mMetricsLogger = metricsLogger;
mConfigurationController = configurationController;
mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
mMediaHierarchyManager = mediaHierarchyManager;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mNotificationsQSContainerController = notificationsQSContainerController;
mNotificationListContainer = notificationListContainer;
mNotificationStackSizeCalculator = notificationStackSizeCalculator;
@@ -822,7 +968,6 @@
mLargeScreenShadeHeaderController = largeScreenShadeHeaderController;
mLayoutInflater = layoutInflater;
mFeatureFlags = featureFlags;
- mFalsingManager = falsingManager;
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
@@ -838,7 +983,6 @@
mUserManager = userManager;
mMediaDataManager = mediaDataManager;
mTapAgainViewController = tapAgainViewController;
- mInteractionJankMonitor = interactionJankMonitor;
mSysUiState = sysUiState;
mPanelEventsEmitter = panelEventsEmitter;
pulseExpansionHandler.setPulseExpandAbortListener(() -> {
@@ -1040,9 +1184,14 @@
controller.setup(mNotificationContainerParent));
}
- @Override
- protected void loadDimens() {
- super.loadDimens();
+ @VisibleForTesting
+ void loadDimens() {
+ final ViewConfiguration configuration = ViewConfiguration.get(this.mView.getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
+ mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
+ mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
+ mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get()
.setMaxLengthSeconds(0.4f).build();
mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
@@ -1715,11 +1864,10 @@
// it's possible that nothing animated, so we replicate the termination
// conditions of panelExpansionChanged here
// TODO(b/200063118): This can likely go away in a future refactor CL.
- getPanelExpansionStateManager().updateState(STATE_CLOSED);
+ getShadeExpansionStateManager().updateState(STATE_CLOSED);
}
}
- @Override
public void collapse(boolean delayed, float speedUpFactor) {
if (!canPanelBeCollapsed()) {
return;
@@ -1729,7 +1877,20 @@
setQsExpandImmediate(true);
setShowShelfOnly(true);
}
- super.collapse(delayed, speedUpFactor);
+ if (DEBUG) this.logf("collapse: " + this);
+ if (canPanelBeCollapsed()) {
+ cancelHeightAnimator();
+ notifyExpandingStarted();
+
+ // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
+ setIsClosing(true);
+ if (delayed) {
+ mNextCollapseSpeedUpFactor = speedUpFactor;
+ this.mView.postDelayed(mFlingCollapseRunnable, 120);
+ } else {
+ fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
+ }
+ }
}
private void setQsExpandImmediate(boolean expandImmediate) {
@@ -1749,10 +1910,15 @@
setQsExpansion(mQsMinExpansionHeight);
}
- @Override
@VisibleForTesting
- protected void cancelHeightAnimator() {
- super.cancelHeightAnimator();
+ void cancelHeightAnimator() {
+ if (mHeightAnimator != null) {
+ if (mHeightAnimator.isRunning()) {
+ mPanelUpdateWhenAnimatorEnds = false;
+ }
+ mHeightAnimator.cancel();
+ }
+ endClosing();
}
public void cancelAnimation() {
@@ -1820,28 +1986,123 @@
}
}
- @Override
public void fling(float vel, boolean expand) {
GestureRecorder gr = mCentralSurfaces.getGestureRecorder();
if (gr != null) {
gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
}
- super.fling(vel, expand);
+ fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
}
- @Override
- protected void flingToHeight(float vel, boolean expand, float target,
+ @VisibleForTesting
+ void flingToHeight(float vel, boolean expand, float target,
float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
mHeadsUpTouchHelper.notifyFling(!expand);
mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
mNotificationStackScrollLayoutController.setPanelFlinging(true);
- super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+ if (target == mExpandedHeight && mOverExpansion == 0.0f) {
+ // We're at the target and didn't fling and there's no overshoot
+ onFlingEnd(false /* cancelled */);
+ return;
+ }
+ mIsFlinging = true;
+ // we want to perform an overshoot animation when flinging open
+ final boolean addOverscroll =
+ expand
+ && !mInSplitShade // Split shade has its own overscroll logic
+ && mStatusBarStateController.getState() != KEYGUARD
+ && mOverExpansion == 0.0f
+ && vel >= 0;
+ final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand);
+ float overshootAmount = 0.0f;
+ if (addOverscroll) {
+ // Let's overshoot depending on the amount of velocity
+ overshootAmount = MathUtils.lerp(
+ 0.2f,
+ 1.0f,
+ MathUtils.saturate(vel
+ / (this.mFlingAnimationUtils.getHighVelocityPxPerSecond()
+ * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT)));
+ overshootAmount += mOverExpansion / mPanelFlingOvershootAmount;
+ }
+ ValueAnimator animator = createHeightAnimator(target, overshootAmount);
+ if (expand) {
+ if (expandBecauseOfFalsing && vel < 0) {
+ vel = 0;
+ }
+ this.mFlingAnimationUtils.apply(animator, mExpandedHeight,
+ target + overshootAmount * mPanelFlingOvershootAmount, vel,
+ this.mView.getHeight());
+ if (vel == 0) {
+ animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
+ }
+ } else {
+ if (shouldUseDismissingAnimation()) {
+ if (vel == 0) {
+ animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+ long duration = (long) (200 + mExpandedHeight / this.mView.getHeight() * 100);
+ animator.setDuration(duration);
+ } else {
+ mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
+ this.mView.getHeight());
+ }
+ } else {
+ mFlingAnimationUtilsClosing.apply(
+ animator, mExpandedHeight, target, vel, this.mView.getHeight());
+ }
+
+ // Make it shorter if we run a canned animation
+ if (vel == 0) {
+ animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
+ }
+ if (mFixedDuration != NO_FIXED_DURATION) {
+ animator.setDuration(mFixedDuration);
+ }
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (!mStatusBarStateController.isDozing()) {
+ beginJankMonitoring();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (shouldSpringBack && !mCancelled) {
+ // After the shade is flinged open to an overscrolled state, spring back
+ // the shade by reducing section padding to 0.
+ springBack();
+ } else {
+ onFlingEnd(mCancelled);
+ }
+ }
+ });
+ setAnimator(animator);
+ animator.start();
}
- @Override
- protected void onFlingEnd(boolean cancelled) {
- super.onFlingEnd(cancelled);
+ private void onFlingEnd(boolean cancelled) {
+ mIsFlinging = false;
+ // No overshoot when the animation ends
+ setOverExpansionInternal(0, false /* isFromGesture */);
+ setAnimator(null);
+ mKeyguardStateController.notifyPanelFlingEnd();
+ if (!cancelled) {
+ endJankMonitoring();
+ notifyExpandingFinished();
+ } else {
+ cancelJankMonitoring();
+ }
+ updatePanelExpansionAndVisibility();
mNotificationStackScrollLayoutController.setPanelFlinging(false);
}
@@ -1936,8 +2197,7 @@
return mQsTracking;
}
- @Override
- protected boolean isInContentBounds(float x, float y) {
+ private boolean isInContentBounds(float x, float y) {
float stackScrollerX = mNotificationStackScrollLayoutController.getX();
return !mNotificationStackScrollLayoutController
.isBelowLastNotification(x - stackScrollerX, y)
@@ -2070,9 +2330,8 @@
- mQsMinExpansionHeight));
}
- @Override
- protected boolean shouldExpandWhenNotFlinging() {
- if (super.shouldExpandWhenNotFlinging()) {
+ private boolean shouldExpandWhenNotFlinging() {
+ if (getExpandedFraction() > 0.5f) {
return true;
}
if (mAllowExpandForSmallExpansion) {
@@ -2084,8 +2343,7 @@
return false;
}
- @Override
- protected float getOpeningHeight() {
+ private float getOpeningHeight() {
return mNotificationStackScrollLayoutController.getOpeningHeight();
}
@@ -2236,9 +2494,20 @@
}
}
- @Override
- protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
- boolean expands = super.flingExpands(vel, vectorVel, x, y);
+ private boolean flingExpands(float vel, float vectorVel, float x, float y) {
+ boolean expands = true;
+ if (!this.mFalsingManager.isUnlockingDisabled()) {
+ @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0
+ ? QUICK_SETTINGS : (
+ mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK);
+ if (!isFalseTouch(x, y, interactionType)) {
+ if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+ expands = shouldExpandWhenNotFlinging();
+ } else {
+ expands = vel > 0;
+ }
+ }
+ }
// If we are already running a QS expansion, make sure that we keep the panel open.
if (mQsExpansionAnimator != null) {
@@ -2247,8 +2516,7 @@
return expands;
}
- @Override
- protected boolean shouldGestureWaitForTouchSlop() {
+ private boolean shouldGestureWaitForTouchSlop() {
if (mExpectingSynthesizedDown) {
mExpectingSynthesizedDown = false;
return false;
@@ -2326,7 +2594,7 @@
}
}
- protected int getFalsingThreshold() {
+ private int getFalsingThreshold() {
float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
return (int) (mQsFalsingThreshold * factor);
}
@@ -3066,8 +3334,8 @@
}
}
- @Override
- protected boolean canCollapsePanelOnTouch() {
+ @VisibleForTesting
+ boolean canCollapsePanelOnTouch() {
if (!isInSettings() && mBarState == KEYGUARD) {
return true;
}
@@ -3079,7 +3347,6 @@
return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS);
}
- @Override
public int getMaxPanelHeight() {
int min = mStatusBarMinHeight;
if (!(mBarState == KEYGUARD)
@@ -3113,8 +3380,7 @@
return mIsExpanding;
}
- @Override
- protected void onHeightUpdated(float expandedHeight) {
+ private void onHeightUpdated(float expandedHeight) {
if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
// Updating the clock position will set the top padding which might
// trigger a new panel height and re-position the clock.
@@ -3293,9 +3559,7 @@
mLockIconViewController.setAlpha(alpha);
}
- @Override
- protected void onExpandingStarted() {
- super.onExpandingStarted();
+ private void onExpandingStarted() {
mNotificationStackScrollLayoutController.onExpansionStarted();
mIsExpanding = true;
mQsExpandedWhenExpandingStarted = mQsFullyExpanded;
@@ -3311,8 +3575,7 @@
mQs.setHeaderListening(true);
}
- @Override
- protected void onExpandingFinished() {
+ private void onExpandingFinished() {
mScrimController.onExpandingFinished();
mNotificationStackScrollLayoutController.onExpansionStopped();
mHeadsUpManager.onExpandingFinished();
@@ -3364,18 +3627,58 @@
mQs.setListening(listening);
}
- @Override
public void expand(boolean animate) {
- super.expand(animate);
+ if (isFullyCollapsed() || isCollapsing()) {
+ mInstantExpanding = true;
+ mAnimateAfterExpanding = animate;
+ mUpdateFlingOnLayout = false;
+ abortAnimations();
+ if (mTracking) {
+ // The panel is expanded after this call.
+ onTrackingStopped(true /* expands */);
+ }
+ if (mExpanding) {
+ notifyExpandingFinished();
+ }
+ updatePanelExpansionAndVisibility();
+ // Wait for window manager to pickup the change, so we know the maximum height of the
+ // panel then.
+ this.mView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (!mInstantExpanding) {
+ mView.getViewTreeObserver().removeOnGlobalLayoutListener(
+ this);
+ return;
+ }
+ if (mCentralSurfaces.getNotificationShadeWindowView()
+ .isVisibleToUser()) {
+ mView.getViewTreeObserver().removeOnGlobalLayoutListener(
+ this);
+ if (mAnimateAfterExpanding) {
+ notifyExpandingStarted();
+ beginJankMonitoring();
+ fling(0, true /* expand */);
+ } else {
+ setExpandedFraction(1f);
+ }
+ mInstantExpanding = false;
+ }
+ }
+ });
+ // Make sure a layout really happens.
+ this.mView.requestLayout();
+ }
+
setListening(true);
}
- @Override
public void setOverExpansion(float overExpansion) {
if (overExpansion == mOverExpansion) {
return;
}
- super.setOverExpansion(overExpansion);
+ mOverExpansion = overExpansion;
// Translating the quick settings by half the overexpansion to center it in the background
// frame
updateQsFrameTranslation();
@@ -3383,14 +3686,18 @@
}
private void updateQsFrameTranslation() {
- mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, mOverExpansion,
- mQsTranslationForFullShadeTransition);
+ mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs,
+ mNavigationBarBottomHeight + mAmbientState.getStackTopMargin());
+
}
- @Override
- protected void onTrackingStarted() {
+ private void onTrackingStarted() {
mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen());
- super.onTrackingStarted();
+ endClosing();
+ mTracking = true;
+ mCentralSurfaces.onTrackingStarted();
+ notifyExpandingStarted();
+ updatePanelExpansionAndVisibility();
mScrimController.onTrackingStarted();
if (mQsFullyExpanded) {
setQsExpandImmediate(true);
@@ -3400,10 +3707,11 @@
cancelPendingPanelCollapse();
}
- @Override
- protected void onTrackingStopped(boolean expand) {
+ private void onTrackingStopped(boolean expand) {
mFalsingCollector.onTrackingStopped();
- super.onTrackingStopped(expand);
+ mTracking = false;
+ mCentralSurfaces.onTrackingStopped(expand);
+ updatePanelExpansionAndVisibility();
if (expand) {
mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */,
true /* animate */);
@@ -3420,37 +3728,48 @@
getHeight(), mNavigationBarBottomHeight);
}
- @Override
- protected void startUnlockHintAnimation() {
+ @VisibleForTesting
+ void startUnlockHintAnimation() {
if (mPowerManager.isPowerSaveMode() || mAmbientState.getDozeAmount() > 0f) {
onUnlockHintStarted();
onUnlockHintFinished();
return;
}
- super.startUnlockHintAnimation();
+
+ // We don't need to hint the user if an animation is already running or the user is changing
+ // the expansion.
+ if (mHeightAnimator != null || mTracking) {
+ return;
+ }
+ notifyExpandingStarted();
+ startUnlockHintAnimationPhase1(() -> {
+ notifyExpandingFinished();
+ onUnlockHintFinished();
+ mHintAnimationRunning = false;
+ });
+ onUnlockHintStarted();
+ mHintAnimationRunning = true;
}
- @Override
- protected void onUnlockHintFinished() {
- super.onUnlockHintFinished();
+ @VisibleForTesting
+ void onUnlockHintFinished() {
+ mCentralSurfaces.onHintFinished();
mScrimController.setExpansionAffectsAlpha(true);
mNotificationStackScrollLayoutController.setUnlockHintRunning(false);
}
- @Override
- protected void onUnlockHintStarted() {
- super.onUnlockHintStarted();
+ @VisibleForTesting
+ void onUnlockHintStarted() {
+ mCentralSurfaces.onUnlockHintStarted();
mScrimController.setExpansionAffectsAlpha(false);
mNotificationStackScrollLayoutController.setUnlockHintRunning(true);
}
- @Override
- protected boolean shouldUseDismissingAnimation() {
+ private boolean shouldUseDismissingAnimation() {
return mBarState != StatusBarState.SHADE && (mKeyguardStateController.canDismissLockScreen()
|| !isTracking());
}
- @Override
public int getMaxPanelTransitionDistance() {
// Traditionally the value is based on the number of notifications. On split-shade, we want
// the required distance to be a specific and constant value, to make sure the expansion
@@ -3475,8 +3794,8 @@
}
}
- @Override
- protected boolean isTrackingBlocked() {
+ @VisibleForTesting
+ boolean isTrackingBlocked() {
return mConflictingQsExpansionGesture && mQsExpanded || mBlockingExpansionForCurrentTouch;
}
@@ -3498,19 +3817,17 @@
return mIsLaunchTransitionFinished;
}
- @Override
public void setIsLaunchAnimationRunning(boolean running) {
boolean wasRunning = mIsLaunchAnimationRunning;
- super.setIsLaunchAnimationRunning(running);
+ mIsLaunchAnimationRunning = running;
if (wasRunning != mIsLaunchAnimationRunning) {
mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
}
}
- @Override
- protected void setIsClosing(boolean isClosing) {
+ private void setIsClosing(boolean isClosing) {
boolean wasClosing = isClosing();
- super.setIsClosing(isClosing);
+ mClosing = isClosing;
if (wasClosing != isClosing) {
mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
}
@@ -3523,7 +3840,6 @@
}
}
- @Override
public boolean isDozing() {
return mDozing;
}
@@ -3540,8 +3856,7 @@
mKeyguardStatusViewController.dozeTimeTick();
}
- @Override
- protected boolean onMiddleClicked() {
+ private boolean onMiddleClicked() {
switch (mBarState) {
case KEYGUARD:
if (!mDozingOnDown) {
@@ -3600,15 +3915,13 @@
updateVisibility();
}
- @Override
- protected boolean shouldPanelBeVisible() {
+ private boolean shouldPanelBeVisible() {
boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode;
return headsUpVisible || isExpanded() || mBouncerShowing;
}
- @Override
public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
- super.setHeadsUpManager(headsUpManager);
+ mHeadsUpManager = headsUpManager;
mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager,
mNotificationStackScrollLayoutController.getHeadsUpCallback(),
NotificationPanelViewController.this);
@@ -3622,8 +3935,7 @@
// otherwise we update the state when the expansion is finished
}
- @Override
- protected void onClosingFinished() {
+ private void onClosingFinished() {
mCentralSurfaces.onClosingFinished();
setClosingWithAlphaFadeout(false);
mMediaHierarchyManager.closeGuts();
@@ -3712,8 +4024,7 @@
mCentralSurfaces.clearNotificationEffects();
}
- @Override
- protected boolean isPanelVisibleBecauseOfHeadsUp() {
+ private boolean isPanelVisibleBecauseOfHeadsUp() {
return (mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway)
&& mBarState == StatusBarState.SHADE;
}
@@ -3828,9 +4139,15 @@
mNotificationBoundsAnimationDelay = delay;
}
- @Override
public void setTouchAndAnimationDisabled(boolean disabled) {
- super.setTouchAndAnimationDisabled(disabled);
+ mTouchDisabled = disabled;
+ if (mTouchDisabled) {
+ cancelHeightAnimator();
+ if (mTracking) {
+ onTrackingStopped(true /* expanded */);
+ }
+ notifyExpandingFinished();
+ }
mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled);
}
@@ -4030,9 +4347,14 @@
mBlockingExpansionForCurrentTouch = mTracking;
}
- @Override
public void dump(PrintWriter pw, String[] args) {
- super.dump(pw, args);
+ pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
+ + " tracking=%s timeAnim=%s%s "
+ + "touchDisabled=%s" + "]",
+ this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
+ mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
+ ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
+ mTouchDisabled ? "T" : "f"));
IndentingPrintWriter ipw = asIndenting(pw);
ipw.increaseIndent();
ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect());
@@ -4175,126 +4497,13 @@
mConfigurationListener.onThemeChanged();
}
- @Override
- protected OnLayoutChangeListener createLayoutChangeListener() {
- return new OnLayoutChangeListenerImpl();
+ private OnLayoutChangeListener createLayoutChangeListener() {
+ return new OnLayoutChangeListener();
}
- @Override
- protected TouchHandler createTouchHandler() {
- return new TouchHandler() {
-
- private long mLastTouchDownTime = -1L;
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (SPEW_LOGCAT) {
- Log.v(TAG,
- "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
- + "," + event.getY() + ")");
- }
- if (mQs.disallowPanelTouches()) {
- return false;
- }
- initDownStates(event);
- // Do not let touches go to shade or QS if the bouncer is visible,
- // but still let user swipe down to expand the panel, dismissing the bouncer.
- if (mCentralSurfaces.isBouncerShowing()) {
- return true;
- }
- if (mCommandQueue.panelsEnabled()
- && !mNotificationStackScrollLayoutController.isLongPressInProgress()
- && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
- mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
- mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
- return true;
- }
- if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
- && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
- return true;
- }
-
- if (!isFullyCollapsed() && onQsIntercept(event)) {
- if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
- return true;
- }
- return super.onInterceptTouchEvent(event);
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- if (event.getDownTime() == mLastTouchDownTime) {
- // An issue can occur when swiping down after unlock, where multiple down
- // events are received in this handler with identical downTimes. Until the
- // source of the issue can be located, detect this case and ignore.
- // see b/193350347
- Log.w(TAG, "Duplicate down event detected... ignoring");
- return true;
- }
- mLastTouchDownTime = event.getDownTime();
- }
-
-
- if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
- return false;
- }
-
- // Do not allow panel expansion if bouncer is scrimmed or showing over a dream,
- // otherwise user would be able to pull down QS or expand the shade.
- if (mCentralSurfaces.isBouncerShowingScrimmed()
- || mCentralSurfaces.isBouncerShowingOverDream()) {
- return false;
- }
-
- // Make sure the next touch won't the blocked after the current ends.
- if (event.getAction() == MotionEvent.ACTION_UP
- || event.getAction() == MotionEvent.ACTION_CANCEL) {
- mBlockingExpansionForCurrentTouch = false;
- }
- // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately
- // without any ACTION_MOVE event.
- // In such case, simply expand the panel instead of being stuck at the bottom bar.
- if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) {
- expand(true /* animate */);
- }
- initDownStates(event);
-
- // If pulse is expanding already, let's give it the touch. There are situations
- // where the panel starts expanding even though we're also pulsing
- boolean pulseShouldGetTouch = (!mIsExpanding
- && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
- || mPulseExpansionHandler.isExpanding();
- if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
- // We're expanding all the other ones shouldn't get this anymore
- mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event");
- return true;
- }
- if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
- && !mNotificationStackScrollLayoutController.isLongPressInProgress()
- && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
- mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
- }
- boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
-
- if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
- mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
- return true;
- }
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
- mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
- handled = true;
- }
-
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
- && mKeyguardStateController.isShowing()) {
- mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
- }
-
- handled |= super.onTouch(v, event);
- return !mDozing || mPulsing || handled;
- }
- };
+ @VisibleForTesting
+ TouchHandler createTouchHandler() {
+ return new TouchHandler();
}
private final PhoneStatusBarView.TouchEventHandler mStatusBarViewTouchEventHandler =
@@ -4346,8 +4555,7 @@
}
};
- @Override
- protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
+ private OnConfigurationChangedListener createOnConfigurationChangedListener() {
return new OnConfigurationChangedListener();
}
@@ -4409,6 +4617,593 @@
.commitUpdate(mDisplayId);
}
+ private void logf(String fmt, Object... args) {
+ Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+ }
+
+ private void notifyExpandingStarted() {
+ if (!mExpanding) {
+ mExpanding = true;
+ onExpandingStarted();
+ }
+ }
+
+ private void notifyExpandingFinished() {
+ endClosing();
+ if (mExpanding) {
+ mExpanding = false;
+ onExpandingFinished();
+ }
+ }
+
+ private float getTouchSlop(MotionEvent event) {
+ // Adjust the touch slop if another gesture may be being performed.
+ return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
+ ? mTouchSlop * mSlopMultiplier
+ : mTouchSlop;
+ }
+
+ private void addMovement(MotionEvent event) {
+ // Add movement to velocity tracker using raw screen X and Y coordinates instead
+ // of window coordinates because the window frame may be moving at the same time.
+ float deltaX = event.getRawX() - event.getX();
+ float deltaY = event.getRawY() - event.getY();
+ event.offsetLocation(deltaX, deltaY);
+ mVelocityTracker.addMovement(event);
+ event.offsetLocation(-deltaX, -deltaY);
+ }
+
+ /** If the latency tracker is enabled, begins tracking expand latency. */
+ public void startExpandLatencyTracking() {
+ if (mLatencyTracker.isEnabled()) {
+ mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL);
+ mExpandLatencyTracking = true;
+ }
+ }
+
+ private void startOpening(MotionEvent event) {
+ updatePanelExpansionAndVisibility();
+ // Reset at start so haptic can be triggered as soon as panel starts to open.
+ mHasVibratedOnOpen = false;
+ //TODO: keyguard opens QS a different way; log that too?
+
+ // Log the position of the swipe that opened the panel
+ float width = mCentralSurfaces.getDisplayWidth();
+ float height = mCentralSurfaces.getDisplayHeight();
+ int rot = mCentralSurfaces.getRotation();
+
+ mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
+ (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot);
+ mLockscreenGestureLogger
+ .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND);
+ }
+
+ /**
+ * Maybe vibrate as panel is opened.
+ *
+ * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead
+ * being opened programmatically (such as by the open panel gesture), we always play haptic.
+ */
+ private void maybeVibrateOnOpening(boolean openingWithTouch) {
+ if (mVibrateOnOpening) {
+ if (!openingWithTouch || !mHasVibratedOnOpen) {
+ mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ mHasVibratedOnOpen = true;
+ }
+ }
+ }
+
+ /**
+ * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
+ * horizontal direction
+ */
+ private boolean isDirectionUpwards(float x, float y) {
+ float xDiff = x - mInitialExpandX;
+ float yDiff = y - mInitialExpandY;
+ if (yDiff >= 0) {
+ return false;
+ }
+ return Math.abs(yDiff) >= Math.abs(xDiff);
+ }
+
+ /** Called when a MotionEvent is about to trigger Shade expansion. */
+ public void startExpandMotion(float newX, float newY, boolean startTracking,
+ float expandedHeight) {
+ if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
+ beginJankMonitoring();
+ }
+ mInitialOffsetOnTouch = expandedHeight;
+ mInitialExpandY = newY;
+ mInitialExpandX = newX;
+ mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
+ if (startTracking) {
+ mTouchSlopExceeded = true;
+ setExpandedHeight(mInitialOffsetOnTouch);
+ onTrackingStarted();
+ }
+ }
+
+ private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
+ mTrackingPointer = -1;
+ mAmbientState.setSwipingUp(false);
+ if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
+ || Math.abs(y - mInitialExpandY) > mTouchSlop
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float vel = mVelocityTracker.getYVelocity();
+ float vectorVel = (float) Math.hypot(
+ mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+ final boolean onKeyguard = mKeyguardStateController.isShowing();
+ final boolean expand;
+ if (mKeyguardStateController.isKeyguardFadingAway()
+ || (mInitialTouchFromKeyguard && !onKeyguard)) {
+ // Don't expand for any touches that started from the keyguard and ended after the
+ // keyguard is gone.
+ expand = false;
+ } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+ if (onKeyguard) {
+ expand = true;
+ } else if (mCentralSurfaces.isBouncerShowingOverDream()) {
+ expand = false;
+ } else {
+ // If we get a cancel, put the shade back to the state it was in when the
+ // gesture started
+ expand = !mPanelClosedOnDown;
+ }
+ } else {
+ expand = flingExpands(vel, vectorVel, x, y);
+ }
+
+ mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
+ mCentralSurfaces.isFalsingThresholdNeeded(),
+ mCentralSurfaces.isWakeUpComingFromTouch());
+ // Log collapse gesture if on lock screen.
+ if (!expand && onKeyguard) {
+ float displayDensity = mCentralSurfaces.getDisplayDensity();
+ int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity);
+ int velocityDp = (int) Math.abs(vel / displayDensity);
+ mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
+ mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
+ }
+ @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
+ : y - mInitialExpandY > 0 ? QUICK_SETTINGS
+ : (mKeyguardStateController.canDismissLockScreen()
+ ? UNLOCK : BOUNCER_UNLOCK);
+
+ fling(vel, expand, isFalseTouch(x, y, interactionType));
+ onTrackingStopped(expand);
+ mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
+ if (mUpdateFlingOnLayout) {
+ mUpdateFlingVelocity = vel;
+ }
+ } else if (!mCentralSurfaces.isBouncerShowing()
+ && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
+ && !mKeyguardStateController.isKeyguardGoingAway()) {
+ boolean expands = onEmptySpaceClick();
+ onTrackingStopped(expands);
+ }
+ mVelocityTracker.clear();
+ }
+
+ private float getCurrentExpandVelocity() {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ return mVelocityTracker.getYVelocity();
+ }
+
+ private void endClosing() {
+ if (mClosing) {
+ setIsClosing(false);
+ onClosingFinished();
+ }
+ }
+
+ /**
+ * @param x the final x-coordinate when the finger was lifted
+ * @param y the final y-coordinate when the finger was lifted
+ * @return whether this motion should be regarded as a false touch
+ */
+ private boolean isFalseTouch(float x, float y,
+ @Classifier.InteractionType int interactionType) {
+ if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
+ return false;
+ }
+ if (mFalsingManager.isClassifierEnabled()) {
+ return mFalsingManager.isFalseTouch(interactionType);
+ }
+ if (!mTouchAboveFalsingThreshold) {
+ return true;
+ }
+ if (mUpwardsWhenThresholdReached) {
+ return false;
+ }
+ return !isDirectionUpwards(x, y);
+ }
+
+ private void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
+ fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
+ }
+
+ private void fling(float vel, boolean expand, float collapseSpeedUpFactor,
+ boolean expandBecauseOfFalsing) {
+ float target = expand ? getMaxPanelHeight() : 0;
+ if (!expand) {
+ setIsClosing(true);
+ }
+ flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+ }
+
+ private void springBack() {
+ if (mOverExpansion == 0) {
+ onFlingEnd(false /* cancelled */);
+ return;
+ }
+ mIsSpringBackAnimation = true;
+ ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
+ animator.addUpdateListener(
+ animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
+ false /* isFromGesture */));
+ animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsSpringBackAnimation = false;
+ onFlingEnd(mCancelled);
+ }
+ });
+ setAnimator(animator);
+ animator.start();
+ }
+
+ public String getName() {
+ return mViewName;
+ }
+
+ @VisibleForTesting
+ void setExpandedHeight(float height) {
+ if (DEBUG) logf("setExpandedHeight(%.1f)", height);
+ setExpandedHeightInternal(height);
+ }
+
+ private void updateExpandedHeightToMaxHeight() {
+ float currentMaxPanelHeight = getMaxPanelHeight();
+
+ if (isFullyCollapsed()) {
+ return;
+ }
+
+ if (currentMaxPanelHeight == mExpandedHeight) {
+ return;
+ }
+
+ if (mTracking && !isTrackingBlocked()) {
+ return;
+ }
+
+ if (mHeightAnimator != null && !mIsSpringBackAnimation) {
+ mPanelUpdateWhenAnimatorEnds = true;
+ return;
+ }
+
+ setExpandedHeight(currentMaxPanelHeight);
+ }
+
+ private void setExpandedHeightInternal(float h) {
+ if (isNaN(h)) {
+ Log.wtf(TAG, "ExpandedHeight set to NaN");
+ }
+ mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
+ if (mExpandLatencyTracking && h != 0f) {
+ DejankUtils.postAfterTraversal(
+ () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
+ mExpandLatencyTracking = false;
+ }
+ float maxPanelHeight = getMaxPanelTransitionDistance();
+ if (mHeightAnimator == null) {
+ // Split shade has its own overscroll logic
+ if (mTracking && !mInSplitShade) {
+ float overExpansionPixels = Math.max(0, h - maxPanelHeight);
+ setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
+ }
+ }
+ mExpandedHeight = Math.min(h, maxPanelHeight);
+ // If we are closing the panel and we are almost there due to a slow decelerating
+ // interpolator, abort the animation.
+ if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
+ mExpandedHeight = 0f;
+ if (mHeightAnimator != null) {
+ mHeightAnimator.end();
+ }
+ }
+ mExpansionDragDownAmountPx = h;
+ mExpandedFraction = Math.min(1f,
+ maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
+ mAmbientState.setExpansionFraction(mExpandedFraction);
+ onHeightUpdated(mExpandedHeight);
+ updatePanelExpansionAndVisibility();
+ });
+ }
+
+ /**
+ * Set the current overexpansion
+ *
+ * @param overExpansion the amount of overexpansion to apply
+ * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
+ */
+ private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
+ if (!isFromGesture) {
+ mLastGesturedOverExpansion = -1;
+ setOverExpansion(overExpansion);
+ } else if (mLastGesturedOverExpansion != overExpansion) {
+ mLastGesturedOverExpansion = overExpansion;
+ final float heightForFullOvershoot = mView.getHeight() / 3.0f;
+ float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
+ newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
+ setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
+ }
+ }
+
+ /** Sets the expanded height relative to a number from 0 to 1. */
+ public void setExpandedFraction(float frac) {
+ setExpandedHeight(getMaxPanelTransitionDistance() * frac);
+ }
+
+ @VisibleForTesting
+ float getExpandedHeight() {
+ return mExpandedHeight;
+ }
+
+ public float getExpandedFraction() {
+ return mExpandedFraction;
+ }
+
+ public boolean isFullyExpanded() {
+ return mExpandedHeight >= getMaxPanelHeight();
+ }
+
+ public boolean isFullyCollapsed() {
+ return mExpandedFraction <= 0.0f;
+ }
+
+ public boolean isCollapsing() {
+ return mClosing || mIsLaunchAnimationRunning;
+ }
+
+ public boolean isFlinging() {
+ return mIsFlinging;
+ }
+
+ public boolean isTracking() {
+ return mTracking;
+ }
+
+ /** Returns whether the shade can be collapsed. */
+ public boolean canPanelBeCollapsed() {
+ return !isFullyCollapsed() && !mTracking && !mClosing;
+ }
+
+ /** Collapses the shade instantly without animation. */
+ public void instantCollapse() {
+ abortAnimations();
+ setExpandedFraction(0f);
+ if (mExpanding) {
+ notifyExpandingFinished();
+ }
+ if (mInstantExpanding) {
+ mInstantExpanding = false;
+ updatePanelExpansionAndVisibility();
+ }
+ }
+
+ private void abortAnimations() {
+ cancelHeightAnimator();
+ mView.removeCallbacks(mFlingCollapseRunnable);
+ }
+
+ public boolean isUnlockHintRunning() {
+ return mHintAnimationRunning;
+ }
+
+ /**
+ * Phase 1: Move everything upwards.
+ */
+ private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
+ float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
+ ValueAnimator animator = createHeightAnimator(target);
+ animator.setDuration(250);
+ animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ animator.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ setAnimator(null);
+ onAnimationFinished.run();
+ } else {
+ startUnlockHintAnimationPhase2(onAnimationFinished);
+ }
+ }
+ });
+ animator.start();
+ setAnimator(animator);
+
+ final List<ViewPropertyAnimator> indicationAnimators =
+ mKeyguardBottomArea.getIndicationAreaAnimators();
+ for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
+ indicationAreaAnimator
+ .translationY(-mHintDistance)
+ .setDuration(250)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .withEndAction(() -> indicationAreaAnimator
+ .translationY(0)
+ .setDuration(450)
+ .setInterpolator(mBounceInterpolator)
+ .start())
+ .start();
+ }
+ }
+
+ private void setAnimator(ValueAnimator animator) {
+ mHeightAnimator = animator;
+ if (animator == null && mPanelUpdateWhenAnimatorEnds) {
+ mPanelUpdateWhenAnimatorEnds = false;
+ updateExpandedHeightToMaxHeight();
+ }
+ }
+
+ /**
+ * Phase 2: Bounce down.
+ */
+ private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
+ ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
+ animator.setDuration(450);
+ animator.setInterpolator(mBounceInterpolator);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setAnimator(null);
+ onAnimationFinished.run();
+ updatePanelExpansionAndVisibility();
+ }
+ });
+ animator.start();
+ setAnimator(animator);
+ }
+
+ private ValueAnimator createHeightAnimator(float targetHeight) {
+ return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
+ }
+
+ /**
+ * Create an animator that can also overshoot
+ *
+ * @param targetHeight the target height
+ * @param overshootAmount the amount of overshoot desired
+ */
+ private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
+ float startExpansion = mOverExpansion;
+ ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
+ animator.addUpdateListener(
+ animation -> {
+ if (overshootAmount > 0.0f
+ // Also remove the overExpansion when collapsing
+ || (targetHeight == 0.0f && startExpansion != 0)) {
+ final float expansion = MathUtils.lerp(
+ startExpansion,
+ mPanelFlingOvershootAmount * overshootAmount,
+ Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+ animator.getAnimatedFraction()));
+ setOverExpansionInternal(expansion, false /* isFromGesture */);
+ }
+ setExpandedHeightInternal((float) animation.getAnimatedValue());
+ });
+ return animator;
+ }
+
+ /** Update the visibility of {@link NotificationPanelView} if necessary. */
+ private void updateVisibility() {
+ mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
+ }
+
+ /**
+ * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary.
+ *
+ * TODO(b/200063118): Could public calls to this method be replaced with calls to
+ * {@link #updateVisibility()}? That would allow us to make this method private.
+ */
+ public void updatePanelExpansionAndVisibility() {
+ mShadeExpansionStateManager.onPanelExpansionChanged(
+ mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+ updateVisibility();
+ }
+
+ public boolean isExpanded() {
+ return mExpandedFraction > 0f
+ || mInstantExpanding
+ || isPanelVisibleBecauseOfHeadsUp()
+ || mTracking
+ || mHeightAnimator != null
+ && !mIsSpringBackAnimation;
+ }
+
+ /**
+ * Gets called when the user performs a click anywhere in the empty area of the panel.
+ *
+ * @return whether the panel will be expanded after the action performed by this method
+ */
+ private boolean onEmptySpaceClick() {
+ if (mHintAnimationRunning) {
+ return true;
+ }
+ return onMiddleClicked();
+ }
+
+ @VisibleForTesting
+ boolean isClosing() {
+ return mClosing;
+ }
+
+ /** Collapses the shade with an animation duration in milliseconds. */
+ public void collapseWithDuration(int animationDuration) {
+ mFixedDuration = animationDuration;
+ collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+ mFixedDuration = NO_FIXED_DURATION;
+ }
+
+ /** Returns the NotificationPanelView. */
+ public ViewGroup getView() {
+ // TODO: remove this method, or at least reduce references to it.
+ return mView;
+ }
+
+ private void beginJankMonitoring() {
+ if (mInteractionJankMonitor == null) {
+ return;
+ }
+ InteractionJankMonitor.Configuration.Builder builder =
+ InteractionJankMonitor.Configuration.Builder.withView(
+ InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+ mView)
+ .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
+ mInteractionJankMonitor.begin(builder);
+ }
+
+ private void endJankMonitoring() {
+ if (mInteractionJankMonitor == null) {
+ return;
+ }
+ InteractionJankMonitor.getInstance().end(
+ InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+ }
+
+ private void cancelJankMonitoring() {
+ if (mInteractionJankMonitor == null) {
+ return;
+ }
+ InteractionJankMonitor.getInstance().cancel(
+ InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+ }
+
+ private float getExpansionFraction() {
+ return mExpandedFraction;
+ }
+
+ private ShadeExpansionStateManager getShadeExpansionStateManager() {
+ return mShadeExpansionStateManager;
+ }
+
private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener {
@Override
public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
@@ -4817,13 +5612,18 @@
}
}
- private class OnLayoutChangeListenerImpl extends OnLayoutChangeListener {
-
+ private final class OnLayoutChangeListener implements View.OnLayoutChangeListener {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
DejankUtils.startDetectingBlockingIpcs("NVP#onLayout");
- super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom);
+ updateExpandedHeightToMaxHeight();
+ mHasLayoutedSinceDown = true;
+ if (mUpdateFlingOnLayout) {
+ abortAnimations();
+ fling(mUpdateFlingVelocity, true /* expands */);
+ mUpdateFlingOnLayout = false;
+ }
updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount());
setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth());
@@ -5081,4 +5881,361 @@
}
}
}
+
+ /** Handles MotionEvents for the Shade. */
+ public final class TouchHandler implements View.OnTouchListener {
+ private long mLastTouchDownTime = -1L;
+
+ /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (SPEW_LOGCAT) {
+ Log.v(TAG,
+ "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
+ + "," + event.getY() + ")");
+ }
+ if (mQs.disallowPanelTouches()) {
+ return false;
+ }
+ initDownStates(event);
+ // Do not let touches go to shade or QS if the bouncer is visible,
+ // but still let user swipe down to expand the panel, dismissing the bouncer.
+ if (mCentralSurfaces.isBouncerShowing()) {
+ return true;
+ }
+ if (mCommandQueue.panelsEnabled()
+ && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+ && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+ mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
+ mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+ return true;
+ }
+ if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
+ && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
+ return true;
+ }
+
+ if (!isFullyCollapsed() && onQsIntercept(event)) {
+ if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
+ return true;
+ }
+ if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
+ && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ /* If the user drags anywhere inside the panel we intercept it if the movement is
+ upwards. This allows closing the shade from anywhere inside the panel.
+ We only do this if the current content is scrolled to the bottom, i.e.
+ canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
+ gesture possible. */
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ boolean canCollapsePanel = canCollapsePanelOnTouch();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mCentralSurfaces.userActivity();
+ mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
+ mMinExpandHeight = 0.0f;
+ mDownTime = mSystemClock.uptimeMillis();
+ if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
+ cancelHeightAnimator();
+ mTouchSlopExceeded = true;
+ return true;
+ }
+ mInitialExpandY = y;
+ mInitialExpandX = x;
+ mTouchStartedInEmptyArea = !isInContentBounds(x, y);
+ mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
+ mMotionAborted = false;
+ mPanelClosedOnDown = isFullyCollapsed();
+ mCollapsedAndHeadsUpOnDown = false;
+ mHasLayoutedSinceDown = false;
+ mUpdateFlingOnLayout = false;
+ mTouchAboveFalsingThreshold = false;
+ addMovement(event);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialExpandX = event.getX(newIndex);
+ mInitialExpandY = event.getY(newIndex);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+ mMotionAborted = true;
+ mVelocityTracker.clear();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialExpandY;
+ addMovement(event);
+ final boolean openShadeWithoutHun =
+ mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
+ if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown
+ || openShadeWithoutHun) {
+ float hAbs = Math.abs(h);
+ float touchSlop = getTouchSlop(event);
+ if ((h < -touchSlop
+ || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop))
+ && hAbs > Math.abs(x - mInitialExpandX)) {
+ cancelHeightAnimator();
+ startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ mVelocityTracker.clear();
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (event.getDownTime() == mLastTouchDownTime) {
+ // An issue can occur when swiping down after unlock, where multiple down
+ // events are received in this handler with identical downTimes. Until the
+ // source of the issue can be located, detect this case and ignore.
+ // see b/193350347
+ Log.w(TAG, "Duplicate down event detected... ignoring");
+ return true;
+ }
+ mLastTouchDownTime = event.getDownTime();
+ }
+
+
+ if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
+ return false;
+ }
+
+ // Do not allow panel expansion if bouncer is scrimmed or showing over a dream,
+ // otherwise user would be able to pull down QS or expand the shade.
+ if (mCentralSurfaces.isBouncerShowingScrimmed()
+ || mCentralSurfaces.isBouncerShowingOverDream()) {
+ return false;
+ }
+
+ // Make sure the next touch won't the blocked after the current ends.
+ if (event.getAction() == MotionEvent.ACTION_UP
+ || event.getAction() == MotionEvent.ACTION_CANCEL) {
+ mBlockingExpansionForCurrentTouch = false;
+ }
+ // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately
+ // without any ACTION_MOVE event.
+ // In such case, simply expand the panel instead of being stuck at the bottom bar.
+ if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) {
+ expand(true /* animate */);
+ }
+ initDownStates(event);
+
+ // If pulse is expanding already, let's give it the touch. There are situations
+ // where the panel starts expanding even though we're also pulsing
+ boolean pulseShouldGetTouch = (!mIsExpanding
+ && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
+ || mPulseExpansionHandler.isExpanding();
+ if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
+ // We're expanding all the other ones shouldn't get this anymore
+ mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event");
+ return true;
+ }
+ if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
+ && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+ && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+ mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+ }
+ boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
+
+ if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
+ mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
+ return true;
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
+ mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
+ handled = true;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
+ && mKeyguardStateController.isShowing()) {
+ mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
+ }
+
+ handled |= handleTouch(event);
+ return !mDozing || mPulsing || handled;
+ }
+
+ private boolean handleTouch(MotionEvent event) {
+ if (mInstantExpanding) {
+ mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
+ return false;
+ }
+ if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
+ mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
+ return false;
+ }
+ if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+ mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted");
+ return false;
+ }
+
+ // If dragging should not expand the notifications shade, then return false.
+ if (!mNotificationsDragEnabled) {
+ if (mTracking) {
+ // Turn off tracking if it's on or the shade can get stuck in the down position.
+ onTrackingStopped(true /* expand */);
+ }
+ mShadeLog.logMotionEvent(event, "onTouch: drag not enabled");
+ return false;
+ }
+
+ // On expanding, single mouse click expands the panel instead of dragging.
+ if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ expand(true);
+ }
+ return true;
+ }
+
+ /*
+ * We capture touch events here and update the expand height here in case according to
+ * the users fingers. This also handles multi-touch.
+ *
+ * Flinging is also enabled in order to open or close the shade.
+ */
+
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
+ mIgnoreXTouchSlop = true;
+ }
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+ mMinExpandHeight = 0.0f;
+ mPanelClosedOnDown = isFullyCollapsed();
+ mHasLayoutedSinceDown = false;
+ mUpdateFlingOnLayout = false;
+ mMotionAborted = false;
+ mDownTime = mSystemClock.uptimeMillis();
+ mTouchAboveFalsingThreshold = false;
+ mCollapsedAndHeadsUpOnDown =
+ isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
+ addMovement(event);
+ boolean regularHeightAnimationRunning = mHeightAnimator != null
+ && !mHintAnimationRunning && !mIsSpringBackAnimation;
+ if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
+ mTouchSlopExceeded = regularHeightAnimationRunning
+ || mTouchSlopExceededBeforeDown;
+ cancelHeightAnimator();
+ onTrackingStarted();
+ }
+ if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
+ && !mCentralSurfaces.isBouncerShowing()) {
+ startOpening(event);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ final float newY = event.getY(newIndex);
+ final float newX = event.getX(newIndex);
+ mTrackingPointer = event.getPointerId(newIndex);
+ mHandlingPointerUp = true;
+ startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
+ mHandlingPointerUp = false;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+ mMotionAborted = true;
+ endMotionEvent(event, x, y, true /* forceCancel */);
+ return false;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ addMovement(event);
+ if (!isFullyCollapsed()) {
+ maybeVibrateOnOpening(true /* openingWithTouch */);
+ }
+ float h = y - mInitialExpandY;
+
+ // If the panel was collapsed when touching, we only need to check for the
+ // y-component of the gesture, as we have no conflicting horizontal gesture.
+ if (Math.abs(h) > getTouchSlop(event)
+ && (Math.abs(h) > Math.abs(x - mInitialExpandX)
+ || mIgnoreXTouchSlop)) {
+ mTouchSlopExceeded = true;
+ if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
+ if (mInitialOffsetOnTouch != 0f) {
+ startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+ h = 0;
+ }
+ cancelHeightAnimator();
+ onTrackingStarted();
+ }
+ }
+ float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
+ newHeight = Math.max(newHeight, mMinExpandHeight);
+ if (-h >= getFalsingThreshold()) {
+ mTouchAboveFalsingThreshold = true;
+ mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
+ }
+ if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
+ // Count h==0 as part of swipe-up,
+ // otherwise {@link NotificationStackScrollLayout}
+ // wrongly enables stack height updates at the start of lockscreen swipe-up
+ mAmbientState.setSwipingUp(h <= 0);
+ setExpandedHeightInternal(newHeight);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ addMovement(event);
+ endMotionEvent(event, x, y, false /* forceCancel */);
+ // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
+ if (mHeightAnimator == null) {
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ endJankMonitoring();
+ } else {
+ cancelJankMonitoring();
+ }
+ }
+ break;
+ }
+ return !mGestureWaitForTouchSlop || mTracking;
+ }
+ }
+
+ /** Listens for config changes. */
+ public class OnConfigurationChangedListener implements
+ NotificationPanelView.OnConfigurationChangedListener {
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ loadDimens();
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
deleted file mode 100644
index fa51d85..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
+++ /dev/null
@@ -1,1493 +0,0 @@
-/*
- * 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.systemui.shade;
-
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
-import static com.android.systemui.classifier.Classifier.GENERIC;
-import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
-import static com.android.systemui.classifier.Classifier.UNLOCK;
-import static com.android.systemui.shade.NotificationPanelView.DEBUG;
-
-import static java.lang.Float.isNaN;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.VibrationEffect;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewPropertyAnimator;
-import android.view.ViewTreeObserver;
-import android.view.animation.Interpolator;
-
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.LatencyTracker;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.classifier.Classifier;
-import com.android.systemui.doze.DozeLog;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.notification.stack.AmbientState;
-import com.android.systemui.statusbar.phone.BounceInterpolator;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.time.SystemClock;
-import com.android.wm.shell.animation.FlingAnimationUtils;
-
-import java.io.PrintWriter;
-import java.util.List;
-
-public abstract class PanelViewController {
- public static final String TAG = NotificationPanelView.class.getSimpleName();
- public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
- public static final float FLING_SPEED_UP_FACTOR = 0.6f;
- public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f;
- public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f;
- private static final int NO_FIXED_DURATION = -1;
- private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
- private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L;
-
- /**
- * The factor of the usual high velocity that is needed in order to reach the maximum overshoot
- * when flinging. A low value will make it that most flings will reach the maximum overshoot.
- */
- private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
-
- protected long mDownTime;
- protected boolean mTouchSlopExceededBeforeDown;
- private float mMinExpandHeight;
- private boolean mPanelUpdateWhenAnimatorEnds;
- private final boolean mVibrateOnOpening;
- private boolean mHasVibratedOnOpen = false;
- protected boolean mIsLaunchAnimationRunning;
- private int mFixedDuration = NO_FIXED_DURATION;
- protected float mOverExpansion;
-
- /**
- * The overshoot amount when the panel flings open
- */
- private float mPanelFlingOvershootAmount;
-
- /**
- * The amount of pixels that we have overexpanded the last time with a gesture
- */
- private float mLastGesturedOverExpansion = -1;
-
- /**
- * Is the current animator the spring back animation?
- */
- private boolean mIsSpringBackAnimation;
-
- private boolean mInSplitShade;
-
- private void logf(String fmt, Object... args) {
- Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
- }
-
- protected CentralSurfaces mCentralSurfaces;
- protected HeadsUpManagerPhone mHeadsUpManager;
- protected final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
-
- private float mHintDistance;
- private float mInitialOffsetOnTouch;
- private boolean mCollapsedAndHeadsUpOnDown;
- private float mExpandedFraction = 0;
- private float mExpansionDragDownAmountPx = 0;
- protected float mExpandedHeight = 0;
- private boolean mPanelClosedOnDown;
- private boolean mHasLayoutedSinceDown;
- private float mUpdateFlingVelocity;
- private boolean mUpdateFlingOnLayout;
- private boolean mClosing;
- protected boolean mTracking;
- private boolean mTouchSlopExceeded;
- private int mTrackingPointer;
- private int mTouchSlop;
- private float mSlopMultiplier;
- protected boolean mHintAnimationRunning;
- private boolean mTouchAboveFalsingThreshold;
- private boolean mTouchStartedInEmptyArea;
- private boolean mMotionAborted;
- private boolean mUpwardsWhenThresholdReached;
- private boolean mAnimatingOnDown;
- private boolean mHandlingPointerUp;
-
- private ValueAnimator mHeightAnimator;
- private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
- private final FlingAnimationUtils mFlingAnimationUtils;
- private final FlingAnimationUtils mFlingAnimationUtilsClosing;
- private final FlingAnimationUtils mFlingAnimationUtilsDismissing;
- private final LatencyTracker mLatencyTracker;
- private final FalsingManager mFalsingManager;
- private final DozeLog mDozeLog;
- private final VibratorHelper mVibratorHelper;
-
- /**
- * Whether an instant expand request is currently pending and we are just waiting for layout.
- */
- private boolean mInstantExpanding;
- private boolean mAnimateAfterExpanding;
- private boolean mIsFlinging;
-
- private String mViewName;
- private float mInitialExpandY;
- private float mInitialExpandX;
- private boolean mTouchDisabled;
- private boolean mInitialTouchFromKeyguard;
-
- /**
- * Whether or not the NotificationPanelView can be expanded or collapsed with a drag.
- */
- private final boolean mNotificationsDragEnabled;
-
- private final Interpolator mBounceInterpolator;
- protected KeyguardBottomAreaView mKeyguardBottomArea;
-
- /**
- * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
- */
- private float mNextCollapseSpeedUpFactor = 1.0f;
-
- protected boolean mExpanding;
- private boolean mGestureWaitForTouchSlop;
- private boolean mIgnoreXTouchSlop;
- private boolean mExpandLatencyTracking;
- private final NotificationPanelView mView;
- private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
- private final NotificationShadeWindowController mNotificationShadeWindowController;
- protected final Resources mResources;
- protected final KeyguardStateController mKeyguardStateController;
- protected final SysuiStatusBarStateController mStatusBarStateController;
- protected final AmbientState mAmbientState;
- protected final LockscreenGestureLogger mLockscreenGestureLogger;
- private final ShadeExpansionStateManager mShadeExpansionStateManager;
- private final InteractionJankMonitor mInteractionJankMonitor;
- protected final SystemClock mSystemClock;
-
- protected final ShadeLogger mShadeLog;
-
- protected abstract void onExpandingFinished();
-
- protected void onExpandingStarted() {
- }
-
- protected void notifyExpandingStarted() {
- if (!mExpanding) {
- mExpanding = true;
- onExpandingStarted();
- }
- }
-
- protected final void notifyExpandingFinished() {
- endClosing();
- if (mExpanding) {
- mExpanding = false;
- onExpandingFinished();
- }
- }
-
- protected AmbientState getAmbientState() {
- return mAmbientState;
- }
-
- public PanelViewController(
- NotificationPanelView view,
- FalsingManager falsingManager,
- DozeLog dozeLog,
- KeyguardStateController keyguardStateController,
- SysuiStatusBarStateController statusBarStateController,
- NotificationShadeWindowController notificationShadeWindowController,
- VibratorHelper vibratorHelper,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager,
- LatencyTracker latencyTracker,
- FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
- StatusBarTouchableRegionManager statusBarTouchableRegionManager,
- LockscreenGestureLogger lockscreenGestureLogger,
- ShadeExpansionStateManager shadeExpansionStateManager,
- AmbientState ambientState,
- InteractionJankMonitor interactionJankMonitor,
- ShadeLogger shadeLogger,
- SystemClock systemClock) {
- keyguardStateController.addCallback(new KeyguardStateController.Callback() {
- @Override
- public void onKeyguardFadingAwayChanged() {
- updateExpandedHeightToMaxHeight();
- }
- });
- mAmbientState = ambientState;
- mView = view;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
- mLockscreenGestureLogger = lockscreenGestureLogger;
- mShadeExpansionStateManager = shadeExpansionStateManager;
- mShadeLog = shadeLogger;
- TouchHandler touchHandler = createTouchHandler();
- mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- mViewName = mResources.getResourceName(mView.getId());
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- }
- });
-
- mView.addOnLayoutChangeListener(createLayoutChangeListener());
- mView.setOnTouchListener(touchHandler);
- mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
-
- mResources = mView.getResources();
- mKeyguardStateController = keyguardStateController;
- mStatusBarStateController = statusBarStateController;
- mNotificationShadeWindowController = notificationShadeWindowController;
- mFlingAnimationUtils = flingAnimationUtilsBuilder
- .reset()
- .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
- .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
- .build();
- mFlingAnimationUtilsClosing = flingAnimationUtilsBuilder
- .reset()
- .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS)
- .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR)
- .build();
- mFlingAnimationUtilsDismissing = flingAnimationUtilsBuilder
- .reset()
- .setMaxLengthSeconds(0.5f)
- .setSpeedUpFactor(0.6f)
- .setX2(0.6f)
- .setY2(0.84f)
- .build();
- mLatencyTracker = latencyTracker;
- mBounceInterpolator = new BounceInterpolator();
- mFalsingManager = falsingManager;
- mDozeLog = dozeLog;
- mNotificationsDragEnabled = mResources.getBoolean(
- R.bool.config_enableNotificationShadeDrag);
- mVibratorHelper = vibratorHelper;
- mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
- mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
- mInteractionJankMonitor = interactionJankMonitor;
- mSystemClock = systemClock;
- }
-
- protected void loadDimens() {
- final ViewConfiguration configuration = ViewConfiguration.get(mView.getContext());
- mTouchSlop = configuration.getScaledTouchSlop();
- mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
- mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
- mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
- mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
- }
-
- protected float getTouchSlop(MotionEvent event) {
- // Adjust the touch slop if another gesture may be being performed.
- return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
- ? mTouchSlop * mSlopMultiplier
- : mTouchSlop;
- }
-
- private void addMovement(MotionEvent event) {
- // Add movement to velocity tracker using raw screen X and Y coordinates instead
- // of window coordinates because the window frame may be moving at the same time.
- float deltaX = event.getRawX() - event.getX();
- float deltaY = event.getRawY() - event.getY();
- event.offsetLocation(deltaX, deltaY);
- mVelocityTracker.addMovement(event);
- event.offsetLocation(-deltaX, -deltaY);
- }
-
- public void setTouchAndAnimationDisabled(boolean disabled) {
- mTouchDisabled = disabled;
- if (mTouchDisabled) {
- cancelHeightAnimator();
- if (mTracking) {
- onTrackingStopped(true /* expanded */);
- }
- notifyExpandingFinished();
- }
- }
-
- public void startExpandLatencyTracking() {
- if (mLatencyTracker.isEnabled()) {
- mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL);
- mExpandLatencyTracking = true;
- }
- }
-
- private void startOpening(MotionEvent event) {
- updatePanelExpansionAndVisibility();
- // Reset at start so haptic can be triggered as soon as panel starts to open.
- mHasVibratedOnOpen = false;
- //TODO: keyguard opens QS a different way; log that too?
-
- // Log the position of the swipe that opened the panel
- float width = mCentralSurfaces.getDisplayWidth();
- float height = mCentralSurfaces.getDisplayHeight();
- int rot = mCentralSurfaces.getRotation();
-
- mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
- (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot);
- mLockscreenGestureLogger
- .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND);
- }
-
- /**
- * Maybe vibrate as panel is opened.
- *
- * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead
- * being opened programmatically (such as by the open panel gesture), we always play haptic.
- */
- protected void maybeVibrateOnOpening(boolean openingWithTouch) {
- if (mVibrateOnOpening) {
- if (!openingWithTouch || !mHasVibratedOnOpen) {
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
- mHasVibratedOnOpen = true;
- }
- }
- }
-
- protected abstract float getOpeningHeight();
-
- /**
- * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
- * horizontal direction
- */
- private boolean isDirectionUpwards(float x, float y) {
- float xDiff = x - mInitialExpandX;
- float yDiff = y - mInitialExpandY;
- if (yDiff >= 0) {
- return false;
- }
- return Math.abs(yDiff) >= Math.abs(xDiff);
- }
-
- public void startExpandMotion(float newX, float newY, boolean startTracking,
- float expandedHeight) {
- if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
- beginJankMonitoring();
- }
- mInitialOffsetOnTouch = expandedHeight;
- mInitialExpandY = newY;
- mInitialExpandX = newX;
- mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
- if (startTracking) {
- mTouchSlopExceeded = true;
- setExpandedHeight(mInitialOffsetOnTouch);
- onTrackingStarted();
- }
- }
-
- private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
- mTrackingPointer = -1;
- mAmbientState.setSwipingUp(false);
- if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
- || Math.abs(y - mInitialExpandY) > mTouchSlop
- || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
- mVelocityTracker.computeCurrentVelocity(1000);
- float vel = mVelocityTracker.getYVelocity();
- float vectorVel = (float) Math.hypot(
- mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
-
- final boolean onKeyguard = mKeyguardStateController.isShowing();
- final boolean expand;
- if (mKeyguardStateController.isKeyguardFadingAway()
- || (mInitialTouchFromKeyguard && !onKeyguard)) {
- // Don't expand for any touches that started from the keyguard and ended after the
- // keyguard is gone.
- expand = false;
- } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
- if (onKeyguard) {
- expand = true;
- } else if (mCentralSurfaces.isBouncerShowingOverDream()) {
- expand = false;
- } else {
- // If we get a cancel, put the shade back to the state it was in when the
- // gesture started
- expand = !mPanelClosedOnDown;
- }
- } else {
- expand = flingExpands(vel, vectorVel, x, y);
- }
-
- mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
- mCentralSurfaces.isFalsingThresholdNeeded(),
- mCentralSurfaces.isWakeUpComingFromTouch());
- // Log collapse gesture if on lock screen.
- if (!expand && onKeyguard) {
- float displayDensity = mCentralSurfaces.getDisplayDensity();
- int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity);
- int velocityDp = (int) Math.abs(vel / displayDensity);
- mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
- mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
- }
- @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
- : y - mInitialExpandY > 0 ? QUICK_SETTINGS
- : (mKeyguardStateController.canDismissLockScreen()
- ? UNLOCK : BOUNCER_UNLOCK);
-
- fling(vel, expand, isFalseTouch(x, y, interactionType));
- onTrackingStopped(expand);
- mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
- if (mUpdateFlingOnLayout) {
- mUpdateFlingVelocity = vel;
- }
- } else if (!mCentralSurfaces.isBouncerShowing()
- && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
- && !mKeyguardStateController.isKeyguardGoingAway()) {
- boolean expands = onEmptySpaceClick();
- onTrackingStopped(expands);
- }
- mVelocityTracker.clear();
- }
-
- protected float getCurrentExpandVelocity() {
- mVelocityTracker.computeCurrentVelocity(1000);
- return mVelocityTracker.getYVelocity();
- }
-
- protected abstract int getFalsingThreshold();
-
- protected abstract boolean shouldGestureWaitForTouchSlop();
-
- protected void onTrackingStopped(boolean expand) {
- mTracking = false;
- mCentralSurfaces.onTrackingStopped(expand);
- updatePanelExpansionAndVisibility();
- }
-
- protected void onTrackingStarted() {
- endClosing();
- mTracking = true;
- mCentralSurfaces.onTrackingStarted();
- notifyExpandingStarted();
- updatePanelExpansionAndVisibility();
- }
-
- /**
- * @return Whether a pair of coordinates are inside the visible view content bounds.
- */
- protected abstract boolean isInContentBounds(float x, float y);
-
- protected void cancelHeightAnimator() {
- if (mHeightAnimator != null) {
- if (mHeightAnimator.isRunning()) {
- mPanelUpdateWhenAnimatorEnds = false;
- }
- mHeightAnimator.cancel();
- }
- endClosing();
- }
-
- private void endClosing() {
- if (mClosing) {
- setIsClosing(false);
- onClosingFinished();
- }
- }
-
- protected abstract boolean canCollapsePanelOnTouch();
-
- protected float getContentHeight() {
- return mExpandedHeight;
- }
-
- /**
- * @param vel the current vertical velocity of the motion
- * @param vectorVel the length of the vectorial velocity
- * @return whether a fling should expands the panel; contracts otherwise
- */
- protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
- if (mFalsingManager.isUnlockingDisabled()) {
- return true;
- }
-
- @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0
- ? QUICK_SETTINGS : (
- mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK);
-
- if (isFalseTouch(x, y, interactionType)) {
- return true;
- }
- if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
- return shouldExpandWhenNotFlinging();
- } else {
- return vel > 0;
- }
- }
-
- protected boolean shouldExpandWhenNotFlinging() {
- return getExpandedFraction() > 0.5f;
- }
-
- /**
- * @param x the final x-coordinate when the finger was lifted
- * @param y the final y-coordinate when the finger was lifted
- * @return whether this motion should be regarded as a false touch
- */
- private boolean isFalseTouch(float x, float y,
- @Classifier.InteractionType int interactionType) {
- if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
- return false;
- }
- if (mFalsingManager.isClassifierEnabled()) {
- return mFalsingManager.isFalseTouch(interactionType);
- }
- if (!mTouchAboveFalsingThreshold) {
- return true;
- }
- if (mUpwardsWhenThresholdReached) {
- return false;
- }
- return !isDirectionUpwards(x, y);
- }
-
- protected void fling(float vel, boolean expand) {
- fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
- }
-
- protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
- fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
- }
-
- protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
- boolean expandBecauseOfFalsing) {
- float target = expand ? getMaxPanelHeight() : 0;
- if (!expand) {
- setIsClosing(true);
- }
- flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
- }
-
- protected void flingToHeight(float vel, boolean expand, float target,
- float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
- if (target == mExpandedHeight && mOverExpansion == 0.0f) {
- // We're at the target and didn't fling and there's no overshoot
- onFlingEnd(false /* cancelled */);
- return;
- }
- mIsFlinging = true;
- // we want to perform an overshoot animation when flinging open
- final boolean addOverscroll =
- expand
- && !mInSplitShade // Split shade has its own overscroll logic
- && mStatusBarStateController.getState() != StatusBarState.KEYGUARD
- && mOverExpansion == 0.0f
- && vel >= 0;
- final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand);
- float overshootAmount = 0.0f;
- if (addOverscroll) {
- // Let's overshoot depending on the amount of velocity
- overshootAmount = MathUtils.lerp(
- 0.2f,
- 1.0f,
- MathUtils.saturate(vel
- / (mFlingAnimationUtils.getHighVelocityPxPerSecond()
- * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT)));
- overshootAmount += mOverExpansion / mPanelFlingOvershootAmount;
- }
- ValueAnimator animator = createHeightAnimator(target, overshootAmount);
- if (expand) {
- if (expandBecauseOfFalsing && vel < 0) {
- vel = 0;
- }
- mFlingAnimationUtils.apply(animator, mExpandedHeight,
- target + overshootAmount * mPanelFlingOvershootAmount, vel, mView.getHeight());
- if (vel == 0) {
- animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
- }
- } else {
- if (shouldUseDismissingAnimation()) {
- if (vel == 0) {
- animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
- long duration = (long) (200 + mExpandedHeight / mView.getHeight() * 100);
- animator.setDuration(duration);
- } else {
- mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
- mView.getHeight());
- }
- } else {
- mFlingAnimationUtilsClosing.apply(
- animator, mExpandedHeight, target, vel, mView.getHeight());
- }
-
- // Make it shorter if we run a canned animation
- if (vel == 0) {
- animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
- }
- if (mFixedDuration != NO_FIXED_DURATION) {
- animator.setDuration(mFixedDuration);
- }
- }
- animator.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationStart(Animator animation) {
- if (!mStatusBarStateController.isDozing()) {
- beginJankMonitoring();
- }
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (shouldSpringBack && !mCancelled) {
- // After the shade is flinged open to an overscrolled state, spring back
- // the shade by reducing section padding to 0.
- springBack();
- } else {
- onFlingEnd(mCancelled);
- }
- }
- });
- setAnimator(animator);
- animator.start();
- }
-
- private void springBack() {
- if (mOverExpansion == 0) {
- onFlingEnd(false /* cancelled */);
- return;
- }
- mIsSpringBackAnimation = true;
- ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
- animator.addUpdateListener(
- animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
- false /* isFromGesture */));
- animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
- animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- animator.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
- @Override
- public void onAnimationEnd(Animator animation) {
- mIsSpringBackAnimation = false;
- onFlingEnd(mCancelled);
- }
- });
- setAnimator(animator);
- animator.start();
- }
-
- protected void onFlingEnd(boolean cancelled) {
- mIsFlinging = false;
- // No overshoot when the animation ends
- setOverExpansionInternal(0, false /* isFromGesture */);
- setAnimator(null);
- mKeyguardStateController.notifyPanelFlingEnd();
- if (!cancelled) {
- endJankMonitoring();
- notifyExpandingFinished();
- } else {
- cancelJankMonitoring();
- }
- updatePanelExpansionAndVisibility();
- }
-
- protected abstract boolean shouldUseDismissingAnimation();
-
- public String getName() {
- return mViewName;
- }
-
- public void setExpandedHeight(float height) {
- if (DEBUG) logf("setExpandedHeight(%.1f)", height);
- setExpandedHeightInternal(height);
- }
-
- void updateExpandedHeightToMaxHeight() {
- float currentMaxPanelHeight = getMaxPanelHeight();
-
- if (isFullyCollapsed()) {
- return;
- }
-
- if (currentMaxPanelHeight == mExpandedHeight) {
- return;
- }
-
- if (mTracking && !isTrackingBlocked()) {
- return;
- }
-
- if (mHeightAnimator != null && !mIsSpringBackAnimation) {
- mPanelUpdateWhenAnimatorEnds = true;
- return;
- }
-
- setExpandedHeight(currentMaxPanelHeight);
- }
-
- /**
- * Returns drag down distance after which panel should be fully expanded. Usually it's the
- * same as max panel height but for large screen devices (especially split shade) we might
- * want to return different value to shorten drag distance
- */
- public abstract int getMaxPanelTransitionDistance();
-
- public void setExpandedHeightInternal(float h) {
- if (isNaN(h)) {
- Log.wtf(TAG, "ExpandedHeight set to NaN");
- }
- mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
- if (mExpandLatencyTracking && h != 0f) {
- DejankUtils.postAfterTraversal(
- () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
- mExpandLatencyTracking = false;
- }
- float maxPanelHeight = getMaxPanelTransitionDistance();
- if (mHeightAnimator == null) {
- // Split shade has its own overscroll logic
- if (mTracking && !mInSplitShade) {
- float overExpansionPixels = Math.max(0, h - maxPanelHeight);
- setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
- }
- }
- mExpandedHeight = Math.min(h, maxPanelHeight);
- // If we are closing the panel and we are almost there due to a slow decelerating
- // interpolator, abort the animation.
- if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
- mExpandedHeight = 0f;
- if (mHeightAnimator != null) {
- mHeightAnimator.end();
- }
- }
- mExpansionDragDownAmountPx = h;
- mExpandedFraction = Math.min(1f,
- maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
- mAmbientState.setExpansionFraction(mExpandedFraction);
- onHeightUpdated(mExpandedHeight);
- updatePanelExpansionAndVisibility();
- });
- }
-
- /**
- * @return true if the panel tracking should be temporarily blocked; this is used when a
- * conflicting gesture (opening QS) is happening
- */
- protected abstract boolean isTrackingBlocked();
-
- protected void setOverExpansion(float overExpansion) {
- mOverExpansion = overExpansion;
- }
-
- /**
- * Set the current overexpansion
- *
- * @param overExpansion the amount of overexpansion to apply
- * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
- */
- private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
- if (!isFromGesture) {
- mLastGesturedOverExpansion = -1;
- setOverExpansion(overExpansion);
- } else if (mLastGesturedOverExpansion != overExpansion) {
- mLastGesturedOverExpansion = overExpansion;
- final float heightForFullOvershoot = mView.getHeight() / 3.0f;
- float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
- newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
- setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
- }
- }
-
- protected abstract void onHeightUpdated(float expandedHeight);
-
- /**
- * This returns the maximum height of the panel. Children should override this if their
- * desired height is not the full height.
- *
- * @return the default implementation simply returns the maximum height.
- */
- protected abstract int getMaxPanelHeight();
-
- public void setExpandedFraction(float frac) {
- setExpandedHeight(getMaxPanelTransitionDistance() * frac);
- }
-
- public float getExpandedHeight() {
- return mExpandedHeight;
- }
-
- public float getExpandedFraction() {
- return mExpandedFraction;
- }
-
- public boolean isFullyExpanded() {
- return mExpandedHeight >= getMaxPanelHeight();
- }
-
- public boolean isFullyCollapsed() {
- return mExpandedFraction <= 0.0f;
- }
-
- public boolean isCollapsing() {
- return mClosing || mIsLaunchAnimationRunning;
- }
-
- public boolean isFlinging() {
- return mIsFlinging;
- }
-
- public boolean isTracking() {
- return mTracking;
- }
-
- public void collapse(boolean delayed, float speedUpFactor) {
- if (DEBUG) logf("collapse: " + this);
- if (canPanelBeCollapsed()) {
- cancelHeightAnimator();
- notifyExpandingStarted();
-
- // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
- setIsClosing(true);
- if (delayed) {
- mNextCollapseSpeedUpFactor = speedUpFactor;
- mView.postDelayed(mFlingCollapseRunnable, 120);
- } else {
- fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
- }
- }
- }
-
- public boolean canPanelBeCollapsed() {
- return !isFullyCollapsed() && !mTracking && !mClosing;
- }
-
- private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
- mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
-
- public void expand(final boolean animate) {
- if (!isFullyCollapsed() && !isCollapsing()) {
- return;
- }
-
- mInstantExpanding = true;
- mAnimateAfterExpanding = animate;
- mUpdateFlingOnLayout = false;
- abortAnimations();
- if (mTracking) {
- onTrackingStopped(true /* expands */); // The panel is expanded after this call.
- }
- if (mExpanding) {
- notifyExpandingFinished();
- }
- updatePanelExpansionAndVisibility();
-
- // Wait for window manager to pickup the change, so we know the maximum height of the panel
- // then.
- mView.getViewTreeObserver().addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- if (!mInstantExpanding) {
- mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- return;
- }
- if (mCentralSurfaces.getNotificationShadeWindowView().isVisibleToUser()) {
- mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- if (mAnimateAfterExpanding) {
- notifyExpandingStarted();
- beginJankMonitoring();
- fling(0, true /* expand */);
- } else {
- setExpandedFraction(1f);
- }
- mInstantExpanding = false;
- }
- }
- });
-
- // Make sure a layout really happens.
- mView.requestLayout();
- }
-
- public void instantCollapse() {
- abortAnimations();
- setExpandedFraction(0f);
- if (mExpanding) {
- notifyExpandingFinished();
- }
- if (mInstantExpanding) {
- mInstantExpanding = false;
- updatePanelExpansionAndVisibility();
- }
- }
-
- private void abortAnimations() {
- cancelHeightAnimator();
- mView.removeCallbacks(mFlingCollapseRunnable);
- }
-
- protected abstract void onClosingFinished();
-
- protected void startUnlockHintAnimation() {
-
- // We don't need to hint the user if an animation is already running or the user is changing
- // the expansion.
- if (mHeightAnimator != null || mTracking) {
- return;
- }
- notifyExpandingStarted();
- startUnlockHintAnimationPhase1(() -> {
- notifyExpandingFinished();
- onUnlockHintFinished();
- mHintAnimationRunning = false;
- });
- onUnlockHintStarted();
- mHintAnimationRunning = true;
- }
-
- protected void onUnlockHintFinished() {
- mCentralSurfaces.onHintFinished();
- }
-
- protected void onUnlockHintStarted() {
- mCentralSurfaces.onUnlockHintStarted();
- }
-
- public boolean isUnlockHintRunning() {
- return mHintAnimationRunning;
- }
-
- /**
- * Phase 1: Move everything upwards.
- */
- private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
- float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
- ValueAnimator animator = createHeightAnimator(target);
- animator.setDuration(250);
- animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- animator.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (mCancelled) {
- setAnimator(null);
- onAnimationFinished.run();
- } else {
- startUnlockHintAnimationPhase2(onAnimationFinished);
- }
- }
- });
- animator.start();
- setAnimator(animator);
-
- final List<ViewPropertyAnimator> indicationAnimators =
- mKeyguardBottomArea.getIndicationAreaAnimators();
- for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
- indicationAreaAnimator
- .translationY(-mHintDistance)
- .setDuration(250)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .withEndAction(() -> indicationAreaAnimator
- .translationY(0)
- .setDuration(450)
- .setInterpolator(mBounceInterpolator)
- .start())
- .start();
- }
- }
-
- private void setAnimator(ValueAnimator animator) {
- mHeightAnimator = animator;
- if (animator == null && mPanelUpdateWhenAnimatorEnds) {
- mPanelUpdateWhenAnimatorEnds = false;
- updateExpandedHeightToMaxHeight();
- }
- }
-
- /**
- * Phase 2: Bounce down.
- */
- private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
- ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
- animator.setDuration(450);
- animator.setInterpolator(mBounceInterpolator);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- setAnimator(null);
- onAnimationFinished.run();
- updatePanelExpansionAndVisibility();
- }
- });
- animator.start();
- setAnimator(animator);
- }
-
- private ValueAnimator createHeightAnimator(float targetHeight) {
- return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
- }
-
- /**
- * Create an animator that can also overshoot
- *
- * @param targetHeight the target height
- * @param overshootAmount the amount of overshoot desired
- */
- private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
- float startExpansion = mOverExpansion;
- ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
- animator.addUpdateListener(
- animation -> {
- if (overshootAmount > 0.0f
- // Also remove the overExpansion when collapsing
- || (targetHeight == 0.0f && startExpansion != 0)) {
- final float expansion = MathUtils.lerp(
- startExpansion,
- mPanelFlingOvershootAmount * overshootAmount,
- Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
- animator.getAnimatedFraction()));
- setOverExpansionInternal(expansion, false /* isFromGesture */);
- }
- setExpandedHeightInternal((float) animation.getAnimatedValue());
- });
- return animator;
- }
-
- /** Update the visibility of {@link NotificationPanelView} if necessary. */
- public void updateVisibility() {
- mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
- }
-
- /** Returns true if {@link NotificationPanelView} should be visible. */
- abstract protected boolean shouldPanelBeVisible();
-
- /**
- * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary.
- *
- * TODO(b/200063118): Could public calls to this method be replaced with calls to
- * {@link #updateVisibility()}? That would allow us to make this method private.
- */
- public void updatePanelExpansionAndVisibility() {
- mShadeExpansionStateManager.onPanelExpansionChanged(
- mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
- updateVisibility();
- }
-
- public boolean isExpanded() {
- return mExpandedFraction > 0f
- || mInstantExpanding
- || isPanelVisibleBecauseOfHeadsUp()
- || mTracking
- || mHeightAnimator != null
- && !mIsSpringBackAnimation;
- }
-
- protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
-
- /**
- * Gets called when the user performs a click anywhere in the empty area of the panel.
- *
- * @return whether the panel will be expanded after the action performed by this method
- */
- protected boolean onEmptySpaceClick() {
- if (mHintAnimationRunning) {
- return true;
- }
- return onMiddleClicked();
- }
-
- protected abstract boolean onMiddleClicked();
-
- protected abstract boolean isDozing();
-
- public void dump(PrintWriter pw, String[] args) {
- pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
- + " tracking=%s timeAnim=%s%s "
- + "touchDisabled=%s" + "]",
- this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
- mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
- ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
- mTouchDisabled ? "T" : "f"));
- }
-
- public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
- mHeadsUpManager = headsUpManager;
- }
-
- public void setIsLaunchAnimationRunning(boolean running) {
- mIsLaunchAnimationRunning = running;
- }
-
- protected void setIsClosing(boolean isClosing) {
- mClosing = isClosing;
- }
-
- protected boolean isClosing() {
- return mClosing;
- }
-
- public void collapseWithDuration(int animationDuration) {
- mFixedDuration = animationDuration;
- collapse(false /* delayed */, 1.0f /* speedUpFactor */);
- mFixedDuration = NO_FIXED_DURATION;
- }
-
- public ViewGroup getView() {
- // TODO: remove this method, or at least reduce references to it.
- return mView;
- }
-
- protected abstract OnLayoutChangeListener createLayoutChangeListener();
-
- protected abstract TouchHandler createTouchHandler();
-
- protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
- return new OnConfigurationChangedListener();
- }
-
- public class TouchHandler implements View.OnTouchListener {
-
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
- && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
- return false;
- }
-
- /*
- * If the user drags anywhere inside the panel we intercept it if the movement is
- * upwards. This allows closing the shade from anywhere inside the panel.
- *
- * We only do this if the current content is scrolled to the bottom,
- * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
- * gesture
- * possible.
- */
- int pointerIndex = event.findPointerIndex(mTrackingPointer);
- if (pointerIndex < 0) {
- pointerIndex = 0;
- mTrackingPointer = event.getPointerId(pointerIndex);
- }
- final float x = event.getX(pointerIndex);
- final float y = event.getY(pointerIndex);
- boolean canCollapsePanel = canCollapsePanelOnTouch();
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mCentralSurfaces.userActivity();
- mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
- mMinExpandHeight = 0.0f;
- mDownTime = mSystemClock.uptimeMillis();
- if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
- cancelHeightAnimator();
- mTouchSlopExceeded = true;
- return true;
- }
- mInitialExpandY = y;
- mInitialExpandX = x;
- mTouchStartedInEmptyArea = !isInContentBounds(x, y);
- mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
- mMotionAborted = false;
- mPanelClosedOnDown = isFullyCollapsed();
- mCollapsedAndHeadsUpOnDown = false;
- mHasLayoutedSinceDown = false;
- mUpdateFlingOnLayout = false;
- mTouchAboveFalsingThreshold = false;
- addMovement(event);
- break;
- case MotionEvent.ACTION_POINTER_UP:
- final int upPointer = event.getPointerId(event.getActionIndex());
- if (mTrackingPointer == upPointer) {
- // gesture is ongoing, find a new pointer to track
- final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
- mTrackingPointer = event.getPointerId(newIndex);
- mInitialExpandX = event.getX(newIndex);
- mInitialExpandY = event.getY(newIndex);
- }
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
- mMotionAborted = true;
- mVelocityTracker.clear();
- }
- break;
- case MotionEvent.ACTION_MOVE:
- final float h = y - mInitialExpandY;
- addMovement(event);
- final boolean openShadeWithoutHun =
- mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
- if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown
- || openShadeWithoutHun) {
- float hAbs = Math.abs(h);
- float touchSlop = getTouchSlop(event);
- if ((h < -touchSlop
- || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop))
- && hAbs > Math.abs(x - mInitialExpandX)) {
- cancelHeightAnimator();
- startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
- return true;
- }
- }
- break;
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- mVelocityTracker.clear();
- break;
- }
- return false;
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (mInstantExpanding) {
- mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
- return false;
- }
- if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
- mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
- return false;
- }
- if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
- mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted");
- return false;
- }
-
- // If dragging should not expand the notifications shade, then return false.
- if (!mNotificationsDragEnabled) {
- if (mTracking) {
- // Turn off tracking if it's on or the shade can get stuck in the down position.
- onTrackingStopped(true /* expand */);
- }
- mShadeLog.logMotionEvent(event, "onTouch: drag not enabled");
- return false;
- }
-
- // On expanding, single mouse click expands the panel instead of dragging.
- if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
- if (event.getAction() == MotionEvent.ACTION_UP) {
- expand(true);
- }
- return true;
- }
-
- /*
- * We capture touch events here and update the expand height here in case according to
- * the users fingers. This also handles multi-touch.
- *
- * Flinging is also enabled in order to open or close the shade.
- */
-
- int pointerIndex = event.findPointerIndex(mTrackingPointer);
- if (pointerIndex < 0) {
- pointerIndex = 0;
- mTrackingPointer = event.getPointerId(pointerIndex);
- }
- final float x = event.getX(pointerIndex);
- final float y = event.getY(pointerIndex);
-
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
- mIgnoreXTouchSlop = true;
- }
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
- mMinExpandHeight = 0.0f;
- mPanelClosedOnDown = isFullyCollapsed();
- mHasLayoutedSinceDown = false;
- mUpdateFlingOnLayout = false;
- mMotionAborted = false;
- mDownTime = mSystemClock.uptimeMillis();
- mTouchAboveFalsingThreshold = false;
- mCollapsedAndHeadsUpOnDown =
- isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
- addMovement(event);
- boolean regularHeightAnimationRunning = mHeightAnimator != null
- && !mHintAnimationRunning && !mIsSpringBackAnimation;
- if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
- mTouchSlopExceeded = regularHeightAnimationRunning
- || mTouchSlopExceededBeforeDown;
- cancelHeightAnimator();
- onTrackingStarted();
- }
- if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
- && !mCentralSurfaces.isBouncerShowing()) {
- startOpening(event);
- }
- break;
-
- case MotionEvent.ACTION_POINTER_UP:
- final int upPointer = event.getPointerId(event.getActionIndex());
- if (mTrackingPointer == upPointer) {
- // gesture is ongoing, find a new pointer to track
- final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
- final float newY = event.getY(newIndex);
- final float newX = event.getX(newIndex);
- mTrackingPointer = event.getPointerId(newIndex);
- mHandlingPointerUp = true;
- startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
- mHandlingPointerUp = false;
- }
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
- mMotionAborted = true;
- endMotionEvent(event, x, y, true /* forceCancel */);
- return false;
- }
- break;
- case MotionEvent.ACTION_MOVE:
- addMovement(event);
- if (!isFullyCollapsed()) {
- maybeVibrateOnOpening(true /* openingWithTouch */);
- }
- float h = y - mInitialExpandY;
-
- // If the panel was collapsed when touching, we only need to check for the
- // y-component of the gesture, as we have no conflicting horizontal gesture.
- if (Math.abs(h) > getTouchSlop(event)
- && (Math.abs(h) > Math.abs(x - mInitialExpandX)
- || mIgnoreXTouchSlop)) {
- mTouchSlopExceeded = true;
- if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
- if (mInitialOffsetOnTouch != 0f) {
- startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
- h = 0;
- }
- cancelHeightAnimator();
- onTrackingStarted();
- }
- }
- float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
- newHeight = Math.max(newHeight, mMinExpandHeight);
- if (-h >= getFalsingThreshold()) {
- mTouchAboveFalsingThreshold = true;
- mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
- }
- if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
- // Count h==0 as part of swipe-up,
- // otherwise {@link NotificationStackScrollLayout}
- // wrongly enables stack height updates at the start of lockscreen swipe-up
- mAmbientState.setSwipingUp(h <= 0);
- setExpandedHeightInternal(newHeight);
- }
- break;
-
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- addMovement(event);
- endMotionEvent(event, x, y, false /* forceCancel */);
- // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
- if (mHeightAnimator == null) {
- if (event.getActionMasked() == MotionEvent.ACTION_UP) {
- endJankMonitoring();
- } else {
- cancelJankMonitoring();
- }
- }
- break;
- }
- return !mGestureWaitForTouchSlop || mTracking;
- }
- }
-
- protected abstract class OnLayoutChangeListener implements View.OnLayoutChangeListener {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
- int oldTop, int oldRight, int oldBottom) {
- updateExpandedHeightToMaxHeight();
- mHasLayoutedSinceDown = true;
- if (mUpdateFlingOnLayout) {
- abortAnimations();
- fling(mUpdateFlingVelocity, true /* expands */);
- mUpdateFlingOnLayout = false;
- }
- }
- }
-
- public class OnConfigurationChangedListener implements
- NotificationPanelView.OnConfigurationChangedListener {
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- loadDimens();
- }
- }
-
- private void beginJankMonitoring() {
- if (mInteractionJankMonitor == null) {
- return;
- }
- InteractionJankMonitor.Configuration.Builder builder =
- InteractionJankMonitor.Configuration.Builder.withView(
- InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
- mView)
- .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
- mInteractionJankMonitor.begin(builder);
- }
-
- private void endJankMonitoring() {
- if (mInteractionJankMonitor == null) {
- return;
- }
- InteractionJankMonitor.getInstance().end(
- InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
- }
-
- private void cancelJankMonitoring() {
- if (mInteractionJankMonitor == null) {
- return;
- }
- InteractionJankMonitor.getInstance().cancel(
- InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
- }
-
- protected float getExpansionFraction() {
- return mExpandedFraction;
- }
-
- protected ShadeExpansionStateManager getPanelExpansionStateManager() {
- return mShadeExpansionStateManager;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
index 6abf339..ff26766 100644
--- a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
@@ -32,10 +32,10 @@
* Dispatches shortcut to System UI components
*/
@SysUISingleton
-public class ShortcutKeyDispatcher extends CoreStartable
- implements ShortcutKeyServiceProxy.Callbacks {
+public class ShortcutKeyDispatcher implements CoreStartable, ShortcutKeyServiceProxy.Callbacks {
private static final String TAG = "ShortcutKeyDispatcher";
+ private final Context mContext;
private ShortcutKeyServiceProxy mShortcutKeyServiceProxy = new ShortcutKeyServiceProxy(this);
private IWindowManager mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
@@ -50,7 +50,7 @@
@Inject
public ShortcutKeyDispatcher(Context context) {
- super(context);
+ mContext = context;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
index 7807738..59afb18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
@@ -36,8 +36,7 @@
/**
* Calculate and translate the QS Frame on the Y-axis.
*/
- public abstract void translateQsFrame(View qsFrame, QS qs, float overExpansion,
- float qsTranslationForFullShadeTransition);
+ public abstract void translateQsFrame(View qsFrame, QS qs, int bottomInset);
/**
* Calculate the top padding for notifications panel. This could be the supplied
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
index 33e2245..85b522c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
@@ -27,6 +27,8 @@
/**
* Default implementation of QS Translation. This by default does not do much.
+ * This class can be subclassed to allow System UI variants the flexibility to change position of
+ * the Quick Settings frame.
*/
@SysUISingleton
public class QsFrameTranslateImpl extends QsFrameTranslateController {
@@ -37,8 +39,8 @@
}
@Override
- public void translateQsFrame(View qsFrame, QS qs, float overExpansion,
- float qsTranslationForFullShadeTransition) {
+ public void translateQsFrame(View qsFrame, QS qs, int bottomInset) {
+ // Empty implementation by default, meant to be overridden by subclasses.
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
index 59022c0f..822840d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
@@ -66,11 +66,12 @@
* splitted screen.
*/
@SysUISingleton
-public class InstantAppNotifier extends CoreStartable
- implements CommandQueue.Callbacks, KeyguardStateController.Callback {
+public class InstantAppNotifier
+ implements CoreStartable, CommandQueue.Callbacks, KeyguardStateController.Callback {
private static final String TAG = "InstantAppNotifier";
public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;
+ private final Context mContext;
private final Handler mHandler = new Handler();
private final Executor mUiBgExecutor;
private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
@@ -83,7 +84,7 @@
CommandQueue commandQueue,
@UiBackground Executor uiBgExecutor,
KeyguardStateController keyguardStateController) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mUiBgExecutor = uiBgExecutor;
mKeyguardStateController = keyguardStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index ccf6fec..8f3eb4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -440,6 +440,42 @@
override fun onEntryCleanUp(entry: NotificationEntry) {
mHeadsUpViewBinder.abortBindCallback(entry)
}
+
+ /**
+ * Identify notifications whose heads-up state changes when the notification rankings are
+ * updated, and have those changed notifications alert if necessary.
+ *
+ * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
+ * handling of ranking changes needs to take into account that we may have just made a
+ * PostedEntry for some of these notifications.
+ */
+ override fun onRankingApplied() {
+ // Because a ranking update may cause some notifications that are no longer (or were
+ // never) in mPostedEntries to need to alert, we need to check every notification
+ // known to the pipeline.
+ for (entry in mNotifPipeline.allNotifs) {
+ // The only entries we can consider alerting for here are entries that have never
+ // interrupted and that now say they should heads up; if they've alerted in the
+ // past, we don't want to incorrectly alert a second time if there wasn't an
+ // explicit notification update.
+ if (entry.hasInterrupted()) continue
+
+ // The cases where we should consider this notification to be updated:
+ // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
+ // state
+ // - if it is present in PostedEntries and the previous state of shouldHeadsUp
+ // differs from the updated one
+ val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry,
+ /* log= */ false)
+ val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
+ val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+
+ if (shouldUpdateEntry) {
+ mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver)
+ onEntryUpdated(entry)
+ }
+ }
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 204a494..8625cdb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -59,4 +59,13 @@
" numPostedEntries=$int1 logicalGroupSize=$int2"
})
}
+
+ fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean) {
+ buffer.log(TAG, LogLevel.DEBUG, {
+ str1 = key
+ bool1 = shouldHun
+ }, {
+ "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1"
+ })
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index c956a2e..659df24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -72,7 +72,6 @@
@SysUISingleton
private class KeyguardNotificationVisibilityProviderImpl @Inject constructor(
- context: Context,
@Main private val handler: Handler,
private val keyguardStateController: KeyguardStateController,
private val lockscreenUserManager: NotificationLockscreenUserManager,
@@ -82,7 +81,7 @@
private val broadcastDispatcher: BroadcastDispatcher,
private val secureSettings: SecureSettings,
private val globalSettings: GlobalSettings
-) : CoreStartable(context), KeyguardNotificationVisibilityProvider {
+) : CoreStartable, KeyguardNotificationVisibilityProvider {
private val showSilentNotifsUri =
secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)
private val onStateChangedListeners = ListenerSet<Consumer<String>>()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 087dc71..1b00648 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1487,7 +1487,7 @@
l.setAlpha(alpha);
}
if (mChildrenContainer != null) {
- mChildrenContainer.setAlpha(alpha);
+ mChildrenContainer.setContentAlpha(alpha);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..0dda263 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -461,6 +461,20 @@
return mAttachedChildren;
}
+ /**
+ * Sets the alpha on the content, while leaving the background of the container itself as is.
+ *
+ * @param alpha alpha value to apply to the content
+ */
+ public void setContentAlpha(float alpha) {
+ for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+ mNotificationHeader.getChildAt(i).setAlpha(alpha);
+ }
+ for (ExpandableNotificationRow child : getAttachedChildren()) {
+ child.setContentAlpha(alpha);
+ }
+ }
+
/** To be called any time the rows have been updated */
public void updateExpansionStates() {
if (mChildrenExpanded || mUserLocked) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 836cacc..55c577f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1115,6 +1115,10 @@
updateAlgorithmLayoutMinHeight();
updateOwnTranslationZ();
+ // Give The Algorithm information regarding the QS height so it can layout notifications
+ // properly. Needed for some devices that grows notifications down-to-top
+ mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight());
+
// Once the layout has finished, we don't need to animate any scrolling clampings anymore.
mAnimateStackYForContentHeightChange = false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 8d28f75..0502159 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -417,12 +417,19 @@
}
/**
+ * Update the position of QS Frame.
+ */
+ public void updateQSFrameTop(int qsHeight) {
+ // Intentionally empty for sub-classes in other device form factors to override
+ }
+
+ /**
* Determine the positions for the views. This is the main part of the algorithm.
*
* @param algorithmState The state in which the current pass of the algorithm is currently in
* @param ambientState The current ambient state
*/
- private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
+ protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
if (!ambientState.isOnKeyguard()
|| (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
@@ -448,7 +455,7 @@
* @return Fraction to apply to view height and gap between views.
* Does not include shelf height even if shelf is showing.
*/
- private float getExpansionFractionWithoutShelf(
+ protected float getExpansionFractionWithoutShelf(
StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b6e658f..b81b3ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -268,8 +268,7 @@
* </b>
*/
@SysUISingleton
-public class CentralSurfacesImpl extends CoreStartable implements
- CentralSurfaces {
+public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
private static final String BANNER_ACTION_CANCEL =
"com.android.systemui.statusbar.banner_action_cancel";
@@ -290,6 +289,7 @@
private static final UiEventLogger sUiEventLogger = new UiEventLoggerImpl();
+ private final Context mContext;
private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
private float mTransitionToFullShadeProgress = 0f;
@@ -747,7 +747,7 @@
DeviceStateManager deviceStateManager,
WiredChargingRippleController wiredChargingRippleController,
IDreamManager dreamManager) {
- super(context);
+ mContext = context;
mNotificationsController = notificationsController;
mFragmentService = fragmentService;
mLightBarController = lightBarController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index e3e8572..5e26cf0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -47,7 +47,7 @@
private val asyncSensorManager: AsyncSensorManager,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dumpManager: DumpManager
-) : Dumpable, CoreStartable(context) {
+) : Dumpable, CoreStartable {
private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
private var isListening = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index 09298b6..b1b8341 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -37,19 +37,20 @@
* Serves as a collection of UI components, rather than showing its own UI.
*/
@SysUISingleton
-public class TvStatusBar extends CoreStartable implements CommandQueue.Callbacks {
+public class TvStatusBar implements CoreStartable, CommandQueue.Callbacks {
private static final String ACTION_SHOW_PIP_MENU =
"com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+ private final Context mContext;
private final CommandQueue mCommandQueue;
private final Lazy<AssistManager> mAssistManagerLazy;
@Inject
public TvStatusBar(Context context, CommandQueue commandQueue,
Lazy<AssistManager> assistManagerLazy) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mAssistManagerLazy = assistManagerLazy;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
index c199744..b938c90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
@@ -35,9 +35,9 @@
*/
@SysUISingleton
class VpnStatusObserver @Inject constructor(
- context: Context,
+ private val context: Context,
private val securityController: SecurityController
-) : CoreStartable(context),
+) : CoreStartable,
SecurityController.SecurityControllerCallback {
private var vpnConnected = false
@@ -102,7 +102,7 @@
.apply {
vpnName?.let {
setContentText(
- mContext.getString(
+ context.getString(
R.string.notification_disclosure_vpn_text, it
)
)
@@ -111,23 +111,23 @@
.build()
private fun createVpnConnectedNotificationBuilder() =
- Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN)
+ Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
.setSmallIcon(vpnIconId)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SYSTEM)
.extend(Notification.TvExtender())
.setOngoing(true)
- .setContentTitle(mContext.getString(R.string.notification_vpn_connected))
- .setContentIntent(VpnConfig.getIntentForStatusPanel(mContext))
+ .setContentTitle(context.getString(R.string.notification_vpn_connected))
+ .setContentIntent(VpnConfig.getIntentForStatusPanel(context))
private fun createVpnDisconnectedNotification() =
- Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN)
+ Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
.setSmallIcon(vpnIconId)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SYSTEM)
.extend(Notification.TvExtender())
.setTimeoutAfter(VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS)
- .setContentTitle(mContext.getString(R.string.notification_vpn_disconnected))
+ .setContentTitle(context.getString(R.string.notification_vpn_disconnected))
.build()
companion object {
@@ -137,4 +137,4 @@
private const val TAG = "TvVpnNotification"
private const val VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS = 5_000L
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java
index 8026ba5..b92725b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java
@@ -18,13 +18,13 @@
import android.annotation.Nullable;
import android.app.Notification;
-import android.content.Context;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.SparseArray;
import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.NotificationListener;
import javax.inject.Inject;
@@ -32,7 +32,8 @@
/**
* Keeps track of the notifications on TV.
*/
-public class TvNotificationHandler extends CoreStartable implements
+@SysUISingleton
+public class TvNotificationHandler implements CoreStartable,
NotificationListener.NotificationHandler {
private static final String TAG = "TvNotificationHandler";
private final NotificationListener mNotificationListener;
@@ -41,8 +42,7 @@
private Listener mUpdateListener;
@Inject
- public TvNotificationHandler(Context context, NotificationListener notificationListener) {
- super(context);
+ public TvNotificationHandler(NotificationListener notificationListener) {
mNotificationListener = notificationListener;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java
index 892fedc..dbbd0b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java
@@ -35,14 +35,15 @@
* Offers control methods for the notification panel handler on TV devices.
*/
@SysUISingleton
-public class TvNotificationPanel extends CoreStartable implements CommandQueue.Callbacks {
+public class TvNotificationPanel implements CoreStartable, CommandQueue.Callbacks {
private static final String TAG = "TvNotificationPanel";
+ private final Context mContext;
private final CommandQueue mCommandQueue;
private final String mNotificationHandlerPackage;
@Inject
public TvNotificationPanel(Context context, CommandQueue commandQueue) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mNotificationHandlerPackage = mContext.getResources().getString(
com.android.internal.R.string.config_notificationHandlerPackage);
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 4450b76..5cbdf7c 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -62,7 +62,7 @@
@LayoutRes private val viewLayoutRes: Int,
private val windowTitle: String,
private val wakeReason: String,
-) : CoreStartable(context) {
+) : CoreStartable {
/**
* Window layout params that will be used as a starting point for the [windowLayoutParams] of
* all subclasses.
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index a345d99..3d56f23 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -100,7 +100,7 @@
* associated work profiles
*/
@SysUISingleton
-public class ThemeOverlayController extends CoreStartable implements Dumpable {
+public class ThemeOverlayController implements CoreStartable, Dumpable {
protected static final String TAG = "ThemeOverlayController";
private static final boolean DEBUG = true;
@@ -114,6 +114,7 @@
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final Handler mBgHandler;
+ private final Context mContext;
private final boolean mIsMonetEnabled;
private final UserTracker mUserTracker;
private final DeviceProvisionedController mDeviceProvisionedController;
@@ -361,8 +362,7 @@
UserManager userManager, DeviceProvisionedController deviceProvisionedController,
UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
@Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
- super(context);
-
+ mContext = context;
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
mDeviceProvisionedController = deviceProvisionedController;
mBroadcastDispatcher = broadcastDispatcher;
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index 9eb34a4..ed14c8a 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -50,13 +50,14 @@
* Controls display of text toasts.
*/
@SysUISingleton
-public class ToastUI extends CoreStartable implements CommandQueue.Callbacks {
+public class ToastUI implements CoreStartable, CommandQueue.Callbacks {
// values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY
private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds
private static final int TOAST_SHORT_TIME = 2000; // 2 seconds
private static final String TAG = "ToastUI";
+ private final Context mContext;
private final CommandQueue mCommandQueue;
private final INotificationManager mNotificationManager;
private final IAccessibilityManager mIAccessibilityManager;
@@ -90,7 +91,7 @@
@Nullable IAccessibilityManager accessibilityManager,
ToastFactory toastFactory, ToastLogger toastLogger
) {
- super(context);
+ mContext = context;
mCommandQueue = commandQueue;
mNotificationManager = notificationManager;
mIAccessibilityManager = accessibilityManager;
@@ -179,7 +180,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
if (newConfig.orientation != mOrientation) {
mOrientation = newConfig.orientation;
if (mToast != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 3ce5ca3..10a09dd1 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -203,9 +203,9 @@
@Provides
@SysUISingleton
- static TvNotificationHandler provideTvNotificationHandler(Context context,
+ static TvNotificationHandler provideTvNotificationHandler(
NotificationListener notificationListener) {
- return new TvNotificationHandler(context, notificationListener);
+ return new TvNotificationHandler(notificationListener);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index fc20ac2..6ed3a09 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -26,8 +26,6 @@
import android.view.Choreographer
import android.view.Display
import android.view.DisplayInfo
-import android.view.IRotationWatcher
-import android.view.IWindowManager
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
@@ -40,6 +38,7 @@
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.util.traceSection
import com.android.wm.shell.displayareahelper.DisplayAreaHelper
import java.util.Optional
@@ -58,7 +57,7 @@
private val displayAreaHelper: Optional<DisplayAreaHelper>,
@Main private val executor: Executor,
@UiBackground private val backgroundExecutor: Executor,
- private val windowManagerInterface: IWindowManager
+ private val rotationChangeProvider: RotationChangeProvider,
) {
private val transitionListener = TransitionListener()
@@ -78,7 +77,7 @@
fun init() {
deviceStateManager.registerCallback(executor, FoldListener())
unfoldTransitionProgressProvider.addCallback(transitionListener)
- windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
+ rotationChangeProvider.addCallback(rotationWatcher)
val containerBuilder =
SurfaceControl.Builder(SurfaceSession())
@@ -86,7 +85,9 @@
.setName("unfold-overlay-container")
displayAreaHelper.get().attachToRootDisplayArea(
- Display.DEFAULT_DISPLAY, containerBuilder) { builder ->
+ Display.DEFAULT_DISPLAY,
+ containerBuilder
+ ) { builder ->
executor.execute {
overlayContainer = builder.build()
@@ -244,8 +245,8 @@
}
}
- private inner class RotationWatcher : IRotationWatcher.Stub() {
- override fun onRotationChanged(newRotation: Int) =
+ private inner class RotationWatcher : RotationChangeProvider.RotationListener {
+ override fun onRotationChanged(newRotation: Int) {
traceSection("UnfoldLightRevealOverlayAnimation#onRotationChanged") {
if (currentRotation != newRotation) {
currentRotation = newRotation
@@ -253,6 +254,7 @@
root?.relayout(getLayoutParams())
}
}
+ }
}
private inner class FoldListener :
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index eea6ac0..59ad24a 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -17,11 +17,11 @@
package com.android.systemui.unfold
import android.content.Context
-import android.view.IWindowManager
import com.android.systemui.keyguard.LifecycleScreenStatusProvider
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.SystemUnfoldSharedModule
import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
@@ -65,11 +65,11 @@
@Singleton
fun provideNaturalRotationProgressProvider(
context: Context,
- windowManager: IWindowManager,
+ rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
): Optional<NaturalRotationUnfoldProgressProvider> =
unfoldTransitionProgressProvider.map { provider ->
- NaturalRotationUnfoldProgressProvider(context, windowManager, provider)
+ NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, provider)
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
index 4dc78f9..bf70673 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
@@ -21,7 +21,6 @@
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -56,11 +55,12 @@
/** */
@SysUISingleton
-public class StorageNotification extends CoreStartable {
+public class StorageNotification implements CoreStartable {
private static final String TAG = "StorageNotification";
private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME";
private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD";
+ private final Context mContext;
// TODO: delay some notifications to avoid bumpy fast operations
@@ -69,7 +69,7 @@
@Inject
public StorageNotification(Context context) {
- super(context);
+ mContext = context;
}
private static class MoveInfo {
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 6e7b523..91c5921 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -48,7 +48,7 @@
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val interactor: UserInteractor,
private val featureFlags: FeatureFlags,
-) : CoreStartable(context) {
+) : CoreStartable {
private var currentDialog: Dialog? = null
diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
index 53da213..2efeda9 100644
--- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
+++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
@@ -32,7 +32,8 @@
import javax.inject.Inject;
// NOT Singleton. Started per-user.
-public class NotificationChannels extends CoreStartable {
+/** */
+public class NotificationChannels implements CoreStartable {
public static String ALERTS = "ALR";
public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP";
// Deprecated. Please use or create a more specific channel that users will better understand
@@ -45,9 +46,11 @@
public static String INSTANT = "INS";
public static String SETUP = "STP";
+ private final Context mContext;
+
@Inject
public NotificationChannels(Context context) {
- super(context);
+ mContext = context;
}
public static void createAll(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
index 619e50b..a0a0372 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -564,12 +564,13 @@
/** */
@SysUISingleton
- public static class Service extends CoreStartable implements Dumpable {
+ public static class Service implements CoreStartable, Dumpable {
+ private final Context mContext;
private final GarbageMonitor mGarbageMonitor;
@Inject
public Service(Context context, GarbageMonitor garbageMonitor) {
- super(context);
+ mContext = context;
mGarbageMonitor = garbageMonitor;
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index 87fb2a6..0b3521b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -31,18 +31,19 @@
import javax.inject.Inject;
@SysUISingleton
-public class VolumeUI extends CoreStartable {
+public class VolumeUI implements CoreStartable {
private static final String TAG = "VolumeUI";
private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
private final Handler mHandler = new Handler();
private boolean mEnabled;
+ private final Context mContext;
private VolumeDialogComponent mVolumeComponent;
@Inject
public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent) {
- super(context);
+ mContext = context;
mVolumeComponent = volumeDialogComponent;
}
@@ -59,8 +60,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
+ public void onConfigurationChanged(Configuration newConfig) {
if (!mEnabled) return;
mVolumeComponent.onConfigurationChanged(newConfig);
}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 3472cb1..fbc6a58 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -89,8 +89,10 @@
* -> WMShell starts and binds SysUI with Shell components via exported Shell interfaces
*/
@SysUISingleton
-public final class WMShell extends CoreStartable
- implements CommandQueue.Callbacks, ProtoTraceable<SystemUiTraceProto> {
+public final class WMShell implements
+ CoreStartable,
+ CommandQueue.Callbacks,
+ ProtoTraceable<SystemUiTraceProto> {
private static final String TAG = WMShell.class.getName();
private static final int INVALID_SYSUI_STATE_MASK =
SYSUI_STATE_DIALOG_SHOWING
@@ -102,6 +104,7 @@
| SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
+ private final Context mContext;
// Shell interfaces
private final ShellInterface mShell;
private final Optional<Pip> mPipOptional;
@@ -163,7 +166,8 @@
private WakefulnessLifecycle.Observer mWakefulnessObserver;
@Inject
- public WMShell(Context context,
+ public WMShell(
+ Context context,
ShellInterface shell,
Optional<Pip> pipOptional,
Optional<SplitScreen> splitScreenOptional,
@@ -179,7 +183,7 @@
WakefulnessLifecycle wakefulnessLifecycle,
UserTracker userTracker,
@Main Executor sysUiMainExecutor) {
- super(context);
+ mContext = context;
mShell = shell;
mCommandQueue = commandQueue;
mConfigurationController = configurationController;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
index aa671d1..91b544b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
@@ -17,7 +17,6 @@
package com.android.keyguard
import android.hardware.biometrics.BiometricSourceType
-import org.mockito.Mockito.verify
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
@@ -30,9 +29,10 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
-import org.mockito.Captor
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -63,7 +63,6 @@
whenever(keyguardUpdateMonitor.strongAuthTracker).thenReturn(strongAuthTracker)
whenever(sessionTracker.getSessionId(anyInt())).thenReturn(sessionId)
keyguardBiometricLockoutLogger = KeyguardBiometricLockoutLogger(
- mContext,
uiEventLogger,
keyguardUpdateMonitor,
sessionTracker)
@@ -195,4 +194,4 @@
verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallbackCaptor.capture())
updateMonitorCallback = updateMonitorCallbackCaptor.value
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index df10dfe..2319f43 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -231,7 +231,7 @@
}
@Override
- protected void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mExecutor.runAllReady();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
index 571dd3d..9f4a7c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java
@@ -71,7 +71,7 @@
MockitoAnnotations.initMocks(this);
when(mDreamBackend.getEnabledComplications()).thenReturn(new HashSet<>());
- mController = new ComplicationTypesUpdater(mContext, mDreamBackend, mExecutor,
+ mController = new ComplicationTypesUpdater(mDreamBackend, mExecutor,
mSecureSettings, mDreamOverlayStateController);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
index 314a30b..ec448f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java
@@ -82,7 +82,6 @@
public void testComplicationAdded() {
final DreamClockTimeComplication.Registrant registrant =
new DreamClockTimeComplication.Registrant(
- mContext,
mDreamOverlayStateController,
mComplication);
registrant.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index db6082d..aa8c93e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -115,7 +115,7 @@
@Test
public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
@@ -128,7 +128,7 @@
@Test
public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
@@ -141,7 +141,7 @@
@Test
public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
@@ -154,7 +154,7 @@
@Test
public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() {
final DreamHomeControlsComplication.Registrant registrant =
- new DreamHomeControlsComplication.Registrant(mContext, mComplication,
+ new DreamHomeControlsComplication.Registrant(mComplication,
mDreamOverlayStateController, mControlsComponent);
registrant.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
index fa8f88a..c8b2b25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
@@ -24,7 +24,6 @@
import static org.mockito.Mockito.when;
import android.app.smartspace.SmartspaceTarget;
-import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.view.View;
@@ -48,8 +47,6 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class SmartSpaceComplicationTest extends SysuiTestCase {
- @Mock
- private Context mContext;
@Mock
private DreamSmartspaceController mSmartspaceController;
@@ -80,7 +77,6 @@
private SmartSpaceComplication.Registrant getRegistrant() {
return new SmartSpaceComplication.Registrant(
- mContext,
mDreamOverlayStateController,
mComplication,
mSmartspaceController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
index b8e9cf4..dc5522e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
@@ -82,7 +82,6 @@
MockitoAnnotations.initMocks(this);
mSessionTracker = new SessionTracker(
- mContext,
mStatusBarService,
mAuthController,
mKeyguardUpdateMonitor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
index 5ad3542..f34c2ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -25,6 +25,11 @@
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
@@ -398,4 +403,24 @@
// added to the end because it was active less recently.
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
}
+
+ @Test
+ fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
+ val delta = 0.0001F
+ val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation(
+ (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION)
+ val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation(
+ (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ whenever(mediaHostStatesManager.mediaHostStates)
+ .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
+ whenever(mediaHostState.visible).thenReturn(true)
+ mediaCarouselController.currentEndLocation = LOCATION_QS
+ whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+ mediaCarouselController.updatePageIndicatorAlpha()
+ assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+
+ whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+ mediaCarouselController.updatePageIndicatorAlpha()
+ assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
new file mode 100644
index 0000000..622a512
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.animation.WidgetState
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.floatThat
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaViewControllerTest : SysuiTestCase() {
+ private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
+ private val mediaHostStatesManager = MediaHostStatesManager()
+ private val configurationController =
+ com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
+ private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+ private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+ @Mock lateinit var logger: MediaViewLogger
+ @Mock private lateinit var mockViewState: TransitionViewState
+ @Mock private lateinit var mockCopiedState: TransitionViewState
+ @Mock private lateinit var detailWidgetState: WidgetState
+ @Mock private lateinit var controlWidgetState: WidgetState
+ @Mock private lateinit var mediaTitleWidgetState: WidgetState
+ @Mock private lateinit var mediaContainerWidgetState: WidgetState
+
+ val delta = 0.0001F
+
+ private lateinit var mediaViewController: MediaViewController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mediaViewController =
+ MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+ }
+
+ @Test
+ fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
+ mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
+ player.measureState = TransitionViewState().apply { this.height = 100 }
+ mediaHostStateHolder.expansion = 1f
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ mediaHostStateHolder.measurementInput =
+ MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+ // Test no squish
+ mediaHostStateHolder.squishFraction = 1f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+ // Test half squish
+ mediaHostStateHolder.squishFraction = 0.5f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+ }
+
+ @Test
+ fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
+ mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
+ recommendation.measureState = TransitionViewState().apply { this.height = 100 }
+ mediaHostStateHolder.expansion = 1f
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ mediaHostStateHolder.measurementInput =
+ MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+ // Test no squish
+ mediaHostStateHolder.squishFraction = 1f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+ // Test half squish
+ mediaHostStateHolder.squishFraction = 0.5f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+ }
+
+ @Test
+ fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
+ whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+ whenever(mockCopiedState.widgetStates)
+ .thenReturn(
+ mutableMapOf(
+ R.id.media_progress_bar to controlWidgetState,
+ R.id.header_artist to detailWidgetState
+ )
+ )
+
+ val detailSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
+ verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val detailSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ mediaViewController.squishViewState(mockViewState, detailSquishEnd)
+ verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+ val controlSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+ verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val controlSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+ verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ }
+
+ @Test
+ fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
+ whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+ whenever(mockCopiedState.widgetStates)
+ .thenReturn(
+ mutableMapOf(
+ R.id.media_title1 to mediaTitleWidgetState,
+ R.id.media_cover1_container to mediaContainerWidgetState
+ )
+ )
+
+ val containerSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+ verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val containerSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+ verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+ val titleSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+ verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val titleSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+ verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index 2f52950..af53016 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -73,7 +73,7 @@
@Test
public void testOnMediaDataLoaded_complicationAddition() {
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
@@ -94,7 +94,7 @@
@Test
public void testOnMediaDataRemoved_complicationRemoval() {
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
@@ -114,7 +114,7 @@
@Test
public void testOnMediaDataLoaded_complicationRemoval() {
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
@@ -139,7 +139,7 @@
public void testOnMediaDataLoaded_mediaComplicationDisabled_doesNotAddComplication() {
when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(false);
- final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+ final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager,
mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
sentinel.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 5cb27a4..46a502a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -99,13 +99,14 @@
policy.getDefaultDisplayId(),
DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
- val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
// Request has topComponent added, but otherwise unchanged.
assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+ assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER)
assertThat(processedRequest.topComponent).isEqualTo(component)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index f9e279e..5b34a95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
@@ -3,72 +3,72 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
+import java.lang.Thread.UncaughtExceptionHandler
import org.junit.Assert.assertThrows
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.mockito.Mock
-import org.mockito.Mockito.only
import org.mockito.Mockito.any
+import org.mockito.Mockito.only
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.lang.Thread.UncaughtExceptionHandler
@SmallTest
class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
- private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
+ private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
- @Mock
- private lateinit var mockHandler: UncaughtExceptionHandler
+ @Mock private lateinit var mockHandler: UncaughtExceptionHandler
- @Mock
- private lateinit var mockHandler2: UncaughtExceptionHandler
+ @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- Thread.setUncaughtExceptionPreHandler(null)
- preHandlerManager = UncaughtExceptionPreHandlerManager()
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ Thread.setUncaughtExceptionPreHandler(null)
+ preHandlerManager = UncaughtExceptionPreHandlerManager()
+ }
+
+ @Test
+ fun registerHandler_registersOnceOnly() {
+ preHandlerManager.registerHandler(mockHandler)
+ preHandlerManager.registerHandler(mockHandler)
+ preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+ verify(mockHandler, only()).uncaughtException(any(), any())
+ }
+
+ @Test
+ fun registerHandler_setsUncaughtExceptionPreHandler() {
+ Thread.setUncaughtExceptionPreHandler(null)
+ preHandlerManager.registerHandler(mockHandler)
+ assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
+ }
+
+ @Test
+ fun registerHandler_preservesOriginalHandler() {
+ Thread.setUncaughtExceptionPreHandler(mockHandler)
+ preHandlerManager.registerHandler(mockHandler2)
+ preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+ verify(mockHandler, only()).uncaughtException(any(), any())
+ }
+
+ @Test
+ @Ignore
+ fun registerHandler_toleratesHandlersThatThrow() {
+ `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
+ preHandlerManager.registerHandler(mockHandler2)
+ preHandlerManager.registerHandler(mockHandler)
+ preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+ verify(mockHandler2, only()).uncaughtException(any(), any())
+ verify(mockHandler, only()).uncaughtException(any(), any())
+ }
+
+ @Test
+ fun registerHandler_doesNotSetUpTwice() {
+ UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
+ assertThrows(IllegalStateException::class.java) {
+ preHandlerManager.registerHandler(mockHandler)
}
-
- @Test
- fun registerHandler_registersOnceOnly() {
- preHandlerManager.registerHandler(mockHandler)
- preHandlerManager.registerHandler(mockHandler)
- preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
- verify(mockHandler, only()).uncaughtException(any(), any())
- }
-
- @Test
- fun registerHandler_setsUncaughtExceptionPreHandler() {
- Thread.setUncaughtExceptionPreHandler(null)
- preHandlerManager.registerHandler(mockHandler)
- assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
- }
-
- @Test
- fun registerHandler_preservesOriginalHandler() {
- Thread.setUncaughtExceptionPreHandler(mockHandler)
- preHandlerManager.registerHandler(mockHandler2)
- preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
- verify(mockHandler, only()).uncaughtException(any(), any())
- }
-
- @Test
- fun registerHandler_toleratesHandlersThatThrow() {
- `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
- preHandlerManager.registerHandler(mockHandler2)
- preHandlerManager.registerHandler(mockHandler)
- preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
- verify(mockHandler2, only()).uncaughtException(any(), any())
- verify(mockHandler, only()).uncaughtException(any(), any())
- }
-
- @Test
- fun registerHandler_doesNotSetUpTwice() {
- UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
- assertThrows(IllegalStateException::class.java) {
- preHandlerManager.registerHandler(mockHandler)
- }
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2970807..340bc96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -47,6 +47,8 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
+import java.util.ArrayList
+import java.util.function.Consumer
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -57,10 +59,8 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.ArrayList
-import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -671,8 +671,64 @@
verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
}
+ @Test
+ fun testOnRankingApplied_newEntryShouldAlert() {
+ // GIVEN that mEntry has never interrupted in the past, and now should
+ assertFalse(mEntry.hasInterrupted())
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+ // WHEN a ranking applied update occurs
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is shown
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
+ }
+
+ @Test
+ fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() {
+ // GIVEN that mEntry has alerted in the past
+ mEntry.setInterruption()
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+ // WHEN a ranking applied update occurs
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is never bound or shown
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ }
+
+ @Test
+ fun testOnRankingApplied_entryUpdatedToHun() {
+ // GIVEN that mEntry is added in a state where it should not HUN
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // and it is then updated such that it should now HUN
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+ // WHEN a ranking applied update occurs
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is shown
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
+ }
+
private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
+ whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
+ .thenReturn(should)
}
private fun finishBind(entry: NotificationEntry) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index 7e07040..e18dd3a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -25,16 +25,21 @@
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -48,6 +53,12 @@
@Mock
private lateinit var handler: Handler
+ @Mock
+ private lateinit var rotationChangeProvider: RotationChangeProvider
+
+ @Captor
+ private lateinit var rotationListener: ArgumentCaptor<RotationListener>
+
private val foldProvider = TestFoldProvider()
private val screenOnStatusProvider = TestScreenOnStatusProvider()
private val testHingeAngleProvider = TestHingeAngleProvider()
@@ -76,6 +87,7 @@
screenOnStatusProvider,
foldProvider,
activityTypeProvider,
+ rotationChangeProvider,
context.mainExecutor,
handler
)
@@ -92,6 +104,8 @@
})
foldStateProvider.start()
+ verify(rotationChangeProvider).addCallback(capture(rotationListener))
+
whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock ->
scheduledRunnable = invocationOnMock.getArgument<Runnable>(0)
scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1)
@@ -372,6 +386,27 @@
assertThat(testHingeAngleProvider.isStarted).isFalse()
}
+ @Test
+ fun onRotationChanged_whileInProgress_cancelled() {
+ setFoldState(folded = false)
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
+
+ rotationListener.value.onRotationChanged(1)
+
+ assertThat(foldUpdates).containsExactly(
+ FOLD_UPDATE_START_OPENING, FOLD_UPDATE_FINISH_HALF_OPEN)
+ }
+
+ @Test
+ fun onRotationChanged_whileNotInProgress_noUpdates() {
+ setFoldState(folded = true)
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+
+ rotationListener.value.onRotationChanged(1)
+
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+ }
+
private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt
new file mode 100644
index 0000000..85cfef7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold.updates
+
+import android.testing.AndroidTestingRunner
+import android.view.IRotationWatcher
+import android.view.IWindowManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class RotationChangeProviderTest : SysuiTestCase() {
+
+ private lateinit var rotationChangeProvider: RotationChangeProvider
+
+ @Mock lateinit var windowManagerInterface: IWindowManager
+ @Mock lateinit var listener: RotationListener
+ @Captor lateinit var rotationWatcher: ArgumentCaptor<IRotationWatcher>
+ private val fakeExecutor = FakeExecutor(FakeSystemClock())
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ rotationChangeProvider =
+ RotationChangeProvider(windowManagerInterface, context, fakeExecutor)
+ rotationChangeProvider.addCallback(listener)
+ fakeExecutor.runAllReady()
+ verify(windowManagerInterface).watchRotation(rotationWatcher.capture(), anyInt())
+ }
+
+ @Test
+ fun onRotationChanged_rotationUpdated_listenerReceivesIt() {
+ sendRotationUpdate(42)
+
+ verify(listener).onRotationChanged(42)
+ }
+
+ @Test
+ fun onRotationChanged_subscribersRemoved_noRotationChangeReceived() {
+ sendRotationUpdate(42)
+ verify(listener).onRotationChanged(42)
+
+ rotationChangeProvider.removeCallback(listener)
+ fakeExecutor.runAllReady()
+ sendRotationUpdate(43)
+
+ verify(windowManagerInterface).removeRotationWatcher(any())
+ verifyNoMoreInteractions(listener)
+ }
+
+ private fun sendRotationUpdate(newRotation: Int) {
+ rotationWatcher.value.onRotationChanged(newRotation)
+ fakeExecutor.runAllReady()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
index b2cedbf..a25469b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
@@ -16,18 +16,19 @@
package com.android.systemui.unfold.util
import android.testing.AndroidTestingRunner
-import android.view.IRotationWatcher
-import android.view.IWindowManager
import android.view.Surface
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
-import com.android.systemui.util.mockito.any
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.util.mockito.capture
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
@@ -38,32 +39,26 @@
@SmallTest
class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
- @Mock
- lateinit var windowManager: IWindowManager
+ @Mock lateinit var rotationChangeProvider: RotationChangeProvider
private val sourceProvider = TestUnfoldTransitionProvider()
- @Mock
- lateinit var transitionListener: TransitionProgressListener
+ @Mock lateinit var transitionListener: TransitionProgressListener
+
+ @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener>
lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
- private val rotationWatcherCaptor =
- ArgumentCaptor.forClass(IRotationWatcher.Stub::class.java)
-
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- progressProvider = NaturalRotationUnfoldProgressProvider(
- context,
- windowManager,
- sourceProvider
- )
+ progressProvider =
+ NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, sourceProvider)
progressProvider.init()
- verify(windowManager).watchRotation(rotationWatcherCaptor.capture(), any())
+ verify(rotationChangeProvider).addCallback(capture(rotationListenerCaptor))
progressProvider.addCallback(transitionListener)
}
@@ -127,6 +122,6 @@
}
private fun onRotationChanged(rotation: Int) {
- rotationWatcherCaptor.value.onRotationChanged(rotation)
+ rotationListenerCaptor.value.onRotationChanged(rotation)
}
}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
index a5ec0a4..5a868a4 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
@@ -20,10 +20,12 @@
import android.content.Context
import android.hardware.SensorManager
import android.os.Handler
+import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBackground
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix
@@ -39,11 +41,11 @@
*
* This component is meant to be used for places that don't use dagger. By providing those
* parameters to the factory, all dagger objects are correctly instantiated. See
- * [createUnfoldTransitionProgressProvider] for an example.
+ * [createUnfoldSharedComponent] for an example.
*/
@Singleton
@Component(modules = [UnfoldSharedModule::class])
-internal interface UnfoldSharedComponent {
+interface UnfoldSharedComponent {
@Component.Factory
interface Factory {
@@ -58,9 +60,11 @@
@BindsInstance @UnfoldMain executor: Executor,
@BindsInstance @UnfoldBackground backgroundExecutor: Executor,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
+ @BindsInstance windowManager: IWindowManager,
@BindsInstance contentResolver: ContentResolver = context.contentResolver
): UnfoldSharedComponent
}
val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
+ val rotationChangeProvider: RotationChangeProvider
}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
index 402dd84..a1ed178 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.hardware.SensorManager
import android.os.Handler
+import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
@@ -27,14 +28,15 @@
import java.util.concurrent.Executor
/**
- * Factory for [UnfoldTransitionProgressProvider].
+ * Factory for [UnfoldSharedComponent].
*
- * This is needed as Launcher has to create the object manually. If dagger is available, this object
- * is provided in [UnfoldSharedModule].
+ * This wraps the autogenerated factory (for discoverability), and is needed as Launcher has to
+ * create the object manually. If dagger is available, this object is provided in
+ * [UnfoldSharedModule].
*
* This should **never** be called from sysui, as the object is already provided in that process.
*/
-fun createUnfoldTransitionProgressProvider(
+fun createUnfoldSharedComponent(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: ScreenStatusProvider,
@@ -44,8 +46,9 @@
mainHandler: Handler,
mainExecutor: Executor,
backgroundExecutor: Executor,
- tracingTagPrefix: String
-): UnfoldTransitionProgressProvider =
+ tracingTagPrefix: String,
+ windowManager: IWindowManager,
+): UnfoldSharedComponent =
DaggerUnfoldSharedComponent.factory()
.create(
context,
@@ -57,9 +60,6 @@
mainHandler,
mainExecutor,
backgroundExecutor,
- tracingTagPrefix)
- .unfoldTransitionProvider
- .orElse(null)
- ?: throw IllegalStateException(
- "Trying to create " +
- "UnfoldTransitionProgressProvider when the transition is disabled")
+ tracingTagPrefix,
+ windowManager,
+ )
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
index d54481c..7117aaf 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
@@ -26,7 +26,8 @@
*
* onTransitionProgress callback could be called on each frame.
*
- * Use [createUnfoldTransitionProgressProvider] to create instances of this interface
+ * Use [createUnfoldSharedComponent] to create instances of this interface when dagger is not
+ * available.
*/
interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 19cfc80..07473b3 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -24,6 +24,7 @@
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES
import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
@@ -40,22 +41,24 @@
private val screenStatusProvider: ScreenStatusProvider,
private val foldProvider: FoldProvider,
private val activityTypeProvider: CurrentActivityTypeProvider,
+ private val rotationChangeProvider: RotationChangeProvider,
@UnfoldMain private val mainExecutor: Executor,
@UnfoldMain private val handler: Handler
) : FoldStateProvider {
private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
- @FoldUpdate
- private var lastFoldUpdate: Int? = null
+ @FoldUpdate private var lastFoldUpdate: Int? = null
- @FloatRange(from = 0.0, to = 180.0)
- private var lastHingeAngle: Float = 0f
+ @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
private val hingeAngleListener = HingeAngleListener()
private val screenListener = ScreenStatusListener()
private val foldStateListener = FoldStateListener()
- private val timeoutRunnable = TimeoutRunnable()
+ private val timeoutRunnable = Runnable { cancelAnimation() }
+ private val rotationListener = RotationListener {
+ if (isTransitionInProgress) cancelAnimation()
+ }
/**
* Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
@@ -72,6 +75,7 @@
foldProvider.registerCallback(foldStateListener, mainExecutor)
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
+ rotationChangeProvider.addCallback(rotationListener)
}
override fun stop() {
@@ -79,6 +83,7 @@
foldProvider.unregisterCallback(foldStateListener)
hingeAngleProvider.removeCallback(hingeAngleListener)
hingeAngleProvider.stop()
+ rotationChangeProvider.removeCallback(rotationListener)
}
override fun addCallback(listener: FoldUpdatesListener) {
@@ -90,14 +95,15 @@
}
override val isFinishedOpening: Boolean
- get() = !isFolded &&
+ get() =
+ !isFolded &&
(lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
- lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
+ lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
private val isTransitionInProgress: Boolean
get() =
lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
- lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+ lastFoldUpdate == FOLD_UPDATE_START_CLOSING
private fun onHingeAngle(angle: Float) {
if (DEBUG) {
@@ -168,7 +174,7 @@
private fun notifyFoldUpdate(@FoldUpdate update: Int) {
if (DEBUG) {
- Log.d(TAG, stateToString(update))
+ Log.d(TAG, update.name())
}
outputListeners.forEach { it.onFoldUpdate(update) }
lastFoldUpdate = update
@@ -185,6 +191,8 @@
handler.removeCallbacks(timeoutRunnable)
}
+ private fun cancelAnimation(): Unit = notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
+
private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
override fun onScreenTurnedOn() {
@@ -225,16 +233,10 @@
onHingeAngle(angle)
}
}
-
- private inner class TimeoutRunnable : Runnable {
- override fun run() {
- notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
- }
- }
}
-private fun stateToString(@FoldUpdate update: Int): String {
- return when (update) {
+fun @receiver:FoldUpdate Int.name() =
+ when (this) {
FOLD_UPDATE_START_OPENING -> "START_OPENING"
FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
@@ -243,15 +245,12 @@
FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
else -> "UNKNOWN"
}
-}
private const val TAG = "DeviceFoldProvider"
private const val DEBUG = false
/** Threshold after which we consider the device fully unfolded. */
-@VisibleForTesting
-const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
+@VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
/** Fold animation on top of apps only when the angle exceeds this threshold. */
-@VisibleForTesting
-const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
+@VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
new file mode 100644
index 0000000..0cf8224
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold.updates
+
+import android.content.Context
+import android.os.RemoteException
+import android.view.IRotationWatcher
+import android.view.IWindowManager
+import android.view.Surface.Rotation
+import com.android.systemui.unfold.dagger.UnfoldMain
+import com.android.systemui.unfold.util.CallbackController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Allows to subscribe to rotation changes.
+ *
+ * This is needed as rotation updates from [IWindowManager] are received in a binder thread, while
+ * most of the times we want them in the main one. Updates are provided for the display associated
+ * to [context].
+ */
+class RotationChangeProvider
+@Inject
+constructor(
+ private val windowManagerInterface: IWindowManager,
+ private val context: Context,
+ @UnfoldMain private val mainExecutor: Executor,
+) : CallbackController<RotationChangeProvider.RotationListener> {
+
+ private val listeners = mutableListOf<RotationListener>()
+
+ private val rotationWatcher = RotationWatcher()
+
+ override fun addCallback(listener: RotationListener) {
+ mainExecutor.execute {
+ if (listeners.isEmpty()) {
+ subscribeToRotation()
+ }
+ listeners += listener
+ }
+ }
+
+ override fun removeCallback(listener: RotationListener) {
+ mainExecutor.execute {
+ listeners -= listener
+ if (listeners.isEmpty()) {
+ unsubscribeToRotation()
+ }
+ }
+ }
+
+ private fun subscribeToRotation() {
+ try {
+ windowManagerInterface.watchRotation(rotationWatcher, context.displayId)
+ } catch (e: RemoteException) {
+ throw e.rethrowFromSystemServer()
+ }
+ }
+
+ private fun unsubscribeToRotation() {
+ try {
+ windowManagerInterface.removeRotationWatcher(rotationWatcher)
+ } catch (e: RemoteException) {
+ throw e.rethrowFromSystemServer()
+ }
+ }
+
+ /** Gets notified of rotation changes. */
+ fun interface RotationListener {
+ /** Called once rotation changes. */
+ fun onRotationChanged(@Rotation newRotation: Int)
+ }
+
+ private inner class RotationWatcher : IRotationWatcher.Stub() {
+ override fun onRotationChanged(rotation: Int) {
+ mainExecutor.execute { listeners.forEach { it.onRotationChanged(rotation) } }
+ }
+ }
+}
diff --git a/services/Android.bp b/services/Android.bp
index 637c4ee..76a1484 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -88,7 +88,6 @@
":services.appwidget-sources",
":services.autofill-sources",
":services.backup-sources",
- ":backuplib-sources",
":services.companion-sources",
":services.contentcapture-sources",
":services.contentsuggestions-sources",
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index ead8aff..b086406 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -19,5 +19,5 @@
defaults: ["platform_service_defaults"],
srcs: [":services.backup-sources"],
libs: ["services.core"],
- static_libs: ["backuplib", "app-compat-annotations"],
+ static_libs: ["app-compat-annotations"],
}
diff --git a/services/backup/backuplib/Android.bp b/services/backup/backuplib/Android.bp
deleted file mode 100644
index 5a28891..0000000
--- a/services/backup/backuplib/Android.bp
+++ /dev/null
@@ -1,21 +0,0 @@
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-filegroup {
- name: "backuplib-sources",
- srcs: ["java/**/*.java"],
- path: "java",
- visibility: ["//frameworks/base/services"],
-}
-
-java_library {
- name: "backuplib",
- srcs: [":backuplib-sources"],
- libs: ["services.core"],
-}
diff --git a/services/backup/backuplib/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/TransportManager.java
rename to services/backup/java/com/android/server/backup/TransportManager.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
similarity index 99%
rename from services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
rename to services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
index d75d648..237a3fa 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -376,7 +376,7 @@
try {
return future.get(600, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException
- | CancellationException e) {
+ | CancellationException e) {
Slog.w(TAG, "Failed to get result from transport:", e);
return null;
} finally {
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/OnTransportRegisteredListener.java b/services/backup/java/com/android/server/backup/transport/OnTransportRegisteredListener.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/OnTransportRegisteredListener.java
rename to services/backup/java/com/android/server/backup/transport/OnTransportRegisteredListener.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java b/services/backup/java/com/android/server/backup/transport/TransportConnection.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
rename to services/backup/java/com/android/server/backup/transport/TransportConnection.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java b/services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java
similarity index 89%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java
rename to services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java
index 1776c41..b218a29 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionListener.java
+++ b/services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -11,15 +11,13 @@
* 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
+ * limitations under the License.
*/
package com.android.server.backup.transport;
import android.annotation.Nullable;
-import com.android.server.backup.transport.BackupTransportClient;
-
/**
* Listener to be called by {@link TransportConnection#connectAsync(TransportConnectionListener,
* String)}.
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionManager.java b/services/backup/java/com/android/server/backup/transport/TransportConnectionManager.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportConnectionManager.java
rename to services/backup/java/com/android/server/backup/transport/TransportConnectionManager.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportNotAvailableException.java b/services/backup/java/com/android/server/backup/transport/TransportNotAvailableException.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportNotAvailableException.java
rename to services/backup/java/com/android/server/backup/transport/TransportNotAvailableException.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportNotRegisteredException.java b/services/backup/java/com/android/server/backup/transport/TransportNotRegisteredException.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportNotRegisteredException.java
rename to services/backup/java/com/android/server/backup/transport/TransportNotRegisteredException.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStats.java b/services/backup/java/com/android/server/backup/transport/TransportStats.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportStats.java
rename to services/backup/java/com/android/server/backup/transport/TransportStats.java
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java
similarity index 97%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
rename to services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java
index 99526b7..fb98825 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
+++ b/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 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.
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportUtils.java b/services/backup/java/com/android/server/backup/transport/TransportUtils.java
similarity index 100%
rename from services/backup/backuplib/java/com/android/server/backup/transport/TransportUtils.java
rename to services/backup/java/com/android/server/backup/transport/TransportUtils.java
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 80d9d97..a614b72 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -23,8 +23,10 @@
import static android.content.ComponentName.createRelative;
import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
+import static com.android.server.companion.MetricUtils.logCreateAssociation;
import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature;
import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
+import static com.android.server.companion.RolesUtils.addRoleHolderForAssociation;
import static com.android.server.companion.RolesUtils.isRoleHolder;
import static com.android.server.companion.Utils.prepareForIpc;
@@ -35,8 +37,10 @@
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
+import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
+import android.companion.CompanionDeviceManager;
import android.companion.IAssociationRequestCallback;
import android.content.ComponentName;
import android.content.Context;
@@ -87,7 +91,7 @@
* required.
*
* If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
- * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}
+ * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback, ResultReceiver)}
* which after calling to {@link CompanionDeviceManagerService} to create an association, notifies
* the requester via
* {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
@@ -99,7 +103,7 @@
* from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
* {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
* which one more time checks that the packages holds all necessary permissions before proceeding to
- * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}.
+ * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback, ResultReceiver)}.
*
* @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
* @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
@@ -132,10 +136,10 @@
private final @NonNull Context mContext;
private final @NonNull CompanionDeviceManagerService mService;
private final @NonNull PackageManagerInternal mPackageManager;
- private final @NonNull AssociationStore mAssociationStore;
+ private final @NonNull AssociationStoreImpl mAssociationStore;
AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
- @NonNull AssociationStore associationStore) {
+ @NonNull AssociationStoreImpl associationStore) {
mContext = service.getContext();
mService = service;
mPackageManager = service.mPackageManagerInternal;
@@ -174,7 +178,7 @@
&& !willAddRoleHolder(request, packageName, userId)) {
// 2a. Create association right away.
createAssociationAndNotifyApplication(request, packageName, userId,
- /*macAddress*/ null, callback);
+ /* macAddress */ null, callback, /* resultReceiver */ null);
return;
}
@@ -253,34 +257,110 @@
}
// 2. Create association and notify the application.
- final AssociationInfo association = createAssociationAndNotifyApplication(
- request, packageName, userId, macAddress, callback);
-
- // 3. Send the association back the Approval Activity, so that it can report back to the app
- // via Activity.setResult().
- final Bundle data = new Bundle();
- data.putParcelable(EXTRA_ASSOCIATION, association);
- resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
+ createAssociationAndNotifyApplication(request, packageName, userId, macAddress, callback,
+ resultReceiver);
}
- private AssociationInfo createAssociationAndNotifyApplication(
+ private void createAssociationAndNotifyApplication(
@NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
- @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback) {
- final AssociationInfo association;
+ @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback,
+ @NonNull ResultReceiver resultReceiver) {
final long callingIdentity = Binder.clearCallingIdentity();
try {
- association = mService.createAssociation(userId, packageName, macAddress,
+ createAssociation(userId, packageName, macAddress,
request.getDisplayName(), request.getDeviceProfile(),
- request.getAssociatedDevice(), request.isSelfManaged());
+ request.getAssociatedDevice(), request.isSelfManaged(),
+ callback, resultReceiver);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
+ }
- try {
- callback.onAssociationCreated(association);
- } catch (RemoteException ignore) { }
+ public void createAssociation(@UserIdInt int userId, @NonNull String packageName,
+ @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
+ @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice,
+ boolean selfManaged, @Nullable IAssociationRequestCallback callback,
+ @Nullable ResultReceiver resultReceiver) {
+ final int id = mService.getNewAssociationIdForPackage(userId, packageName);
+ final long timestamp = System.currentTimeMillis();
- return association;
+ final AssociationInfo association = new AssociationInfo(id, userId, packageName,
+ macAddress, displayName, deviceProfile, associatedDevice, selfManaged,
+ /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE);
+
+ if (deviceProfile != null) {
+ // If the "Device Profile" is specified, make the companion application a holder of the
+ // corresponding role.
+ addRoleHolderForAssociation(mService.getContext(), association, success -> {
+ if (success) {
+ addAssociationToStore(association, deviceProfile);
+
+ sendCallbackAndFinish(association, callback, resultReceiver);
+ } else {
+ Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName
+ + " to the list of " + deviceProfile + " holders.");
+
+ sendCallbackAndFinish(null, callback, resultReceiver);
+ }
+ });
+ } else {
+ addAssociationToStore(association, null);
+
+ sendCallbackAndFinish(association, callback, resultReceiver);
+ }
+
+ // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
+ // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
+ // that there are other devices with the same profile, so the role holder won't be removed.
+ }
+
+ private void addAssociationToStore(@NonNull AssociationInfo association,
+ @Nullable String deviceProfile) {
+ Slog.i(TAG, "New CDM association created=" + association);
+
+ mAssociationStore.addAssociation(association);
+
+ mService.updateSpecialAccessPermissionForAssociatedPackage(association);
+
+ logCreateAssociation(deviceProfile);
+ }
+
+ private void sendCallbackAndFinish(@Nullable AssociationInfo association,
+ @Nullable IAssociationRequestCallback callback,
+ @Nullable ResultReceiver resultReceiver) {
+ if (association != null) {
+ // Send the association back via the app's callback
+ if (callback != null) {
+ try {
+ callback.onAssociationCreated(association);
+ } catch (RemoteException ignore) {
+ }
+ }
+
+ // Send the association back to CompanionDeviceActivity, so that it can report
+ // back to the app via Activity.setResult().
+ if (resultReceiver != null) {
+ final Bundle data = new Bundle();
+ data.putParcelable(EXTRA_ASSOCIATION, association);
+ resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
+ }
+ } else {
+ // Send the association back via the app's callback
+ if (callback != null) {
+ try {
+ // TODO: update to INTERNAL_ERROR once it's added.
+ callback.onFailure(CompanionDeviceManager.REASON_CANCELED);
+ } catch (RemoteException ignore) {
+ }
+ }
+
+ // Send the association back to CompanionDeviceActivity, so that it can report
+ // back to the app via Activity.setResult().
+ if (resultReceiver != null) {
+ final Bundle data = new Bundle();
+ resultReceiver.send(CompanionDeviceManager.RESULT_INTERNAL_ERROR, data);
+ }
+ }
}
private boolean willAddRoleHolder(@NonNull AssociationRequest request,
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index f2cb602..d34fc59 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -28,7 +28,6 @@
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.companion.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
-import static com.android.server.companion.MetricUtils.logCreateAssociation;
import static com.android.server.companion.MetricUtils.logRemoveAssociation;
import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature;
import static com.android.server.companion.PackageUtils.getPackageInfo;
@@ -38,7 +37,6 @@
import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr;
import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks;
-import static com.android.server.companion.RolesUtils.addRoleHolderForAssociation;
import static com.android.server.companion.RolesUtils.removeRoleHolderForAssociation;
import static java.util.Objects.requireNonNull;
@@ -55,7 +53,6 @@
import android.app.AppOpsManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.DeviceNotAssociatedException;
@@ -819,7 +816,8 @@
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES, "createAssociation");
- legacyCreateAssociation(userId, macAddress, packageName, null);
+ final MacAddress macAddressObj = MacAddress.fromString(macAddress);
+ createNewAssociation(userId, packageName, macAddressObj, null, null, false);
}
private void checkCanCallNotificationApi(String callingPackage) {
@@ -871,45 +869,12 @@
}
}
- /**
- * @deprecated use
- * {@link #createAssociation(int, String, MacAddress, CharSequence, String, AssociatedDevice,
- * boolean)}
- */
- @Deprecated
- void legacyCreateAssociation(@UserIdInt int userId, @NonNull String deviceMacAddress,
- @NonNull String packageName, @Nullable String deviceProfile) {
- final MacAddress macAddress = MacAddress.fromString(deviceMacAddress);
- createAssociation(userId, packageName, macAddress, null, deviceProfile, null, false);
- }
-
- AssociationInfo createAssociation(@UserIdInt int userId, @NonNull String packageName,
+ void createNewAssociation(@UserIdInt int userId, @NonNull String packageName,
@Nullable MacAddress macAddress, @Nullable CharSequence displayName,
- @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice,
- boolean selfManaged) {
- final int id = getNewAssociationIdForPackage(userId, packageName);
- final long timestamp = System.currentTimeMillis();
-
- final AssociationInfo association = new AssociationInfo(id, userId, packageName,
- macAddress, displayName, deviceProfile, associatedDevice, selfManaged,
- /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE);
- Slog.i(TAG, "New CDM association created=" + association);
- mAssociationStore.addAssociation(association);
-
- // If the "Device Profile" is specified, make the companion application a holder of the
- // corresponding role.
- if (deviceProfile != null) {
- addRoleHolderForAssociation(getContext(), association);
- }
-
- updateSpecialAccessPermissionForAssociatedPackage(association);
- logCreateAssociation(deviceProfile);
-
- // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
- // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
- // that there are other devices with the same profile, so the role holder won't be removed.
-
- return association;
+ @Nullable String deviceProfile, boolean isSelfManaged) {
+ mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
+ displayName, deviceProfile, /* associatedDevice */ null, isSelfManaged,
+ /* callback */ null, /* resultReceiver */ null);
}
@NonNull
@@ -946,7 +911,7 @@
return usedIdsForPackage;
}
- private int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) {
+ int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) {
synchronized (mPreviouslyUsedIds) {
// First: collect all IDs currently in use for this user's Associations.
final SparseBooleanArray usedIds = new SparseBooleanArray();
@@ -1170,7 +1135,7 @@
}
}
- private void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
+ void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
final PackageInfo packageInfo =
getPackageInfo(getContext(), association.getUserId(), association.getPackageName());
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index d24f9e3..6889bcd 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -17,6 +17,7 @@
package com.android.server.companion;
import android.companion.AssociationInfo;
+import android.net.MacAddress;
import android.os.Binder;
import android.os.ShellCommand;
@@ -63,7 +64,9 @@
int userId = getNextIntArgRequired();
String packageName = getNextArgRequired();
String address = getNextArgRequired();
- mService.legacyCreateAssociation(userId, address, packageName, null);
+ final MacAddress macAddress = MacAddress.fromString(address);
+ mService.createNewAssociation(userId, packageName, macAddress,
+ null, null, false);
}
break;
diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java
index 0fff3f4..f674a7d 100644
--- a/services/companion/java/com/android/server/companion/RolesUtils.java
+++ b/services/companion/java/com/android/server/companion/RolesUtils.java
@@ -32,6 +32,7 @@
import android.util.Slog;
import java.util.List;
+import java.util.function.Consumer;
/** Utility methods for accessing {@link RoleManager} APIs. */
@SuppressLint("LongLogTag")
@@ -46,7 +47,8 @@
}
static void addRoleHolderForAssociation(
- @NonNull Context context, @NonNull AssociationInfo associationInfo) {
+ @NonNull Context context, @NonNull AssociationInfo associationInfo,
+ @NonNull Consumer<Boolean> roleGrantResult) {
if (DEBUG) {
Log.d(TAG, "addRoleHolderForAssociation() associationInfo=" + associationInfo);
}
@@ -62,12 +64,7 @@
roleManager.addRoleHolderAsUser(deviceProfile, packageName,
MANAGE_HOLDERS_FLAG_DONT_KILL_APP, userHandle, context.getMainExecutor(),
- success -> {
- if (!success) {
- Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName
- + " to the list of " + deviceProfile + " holders.");
- }
- });
+ roleGrantResult);
}
static void removeRoleHolderForAssociation(
diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index b3c3a4d..5e8291f 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -2,3 +2,4 @@
ogunwale@google.com
michaelwr@google.com
+vladokom@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 83d527e..0cf7915 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -361,7 +361,7 @@
private volatile boolean mChargingRequired;
private volatile int mMinGCSleepTime;
private volatile int mTargetDirtyRatio;
- private volatile boolean mNeedGC;
+ private volatile boolean mNeedGC = true;
private volatile boolean mPassedLifetimeThresh;
// Tracking storage write amounts in one period
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c837051..593e21a5 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -14888,7 +14888,7 @@
}
if (!Build.IS_DEBUGGABLE && callingUid != ROOT_UID && callingUid != SHELL_UID
- && callingUid != SYSTEM_UID) {
+ && callingUid != SYSTEM_UID && !hasActiveInstrumentationLocked(callingPid)) {
// If it's not debug build and not called from root/shell/system uid, reject it.
final String msg = "Permission Denial: instrumentation test "
+ className + " from pid=" + callingPid + ", uid=" + callingUid
@@ -14996,6 +14996,17 @@
}
@GuardedBy("this")
+ private boolean hasActiveInstrumentationLocked(int pid) {
+ if (pid == 0) {
+ return false;
+ }
+ synchronized (mPidsSelfLocked) {
+ ProcessRecord process = mPidsSelfLocked.get(pid);
+ return process != null && process.getActiveInstrumentation() != null;
+ }
+ }
+
+ @GuardedBy("this")
private boolean startInstrumentationOfSdkSandbox(
ComponentName className,
String profileFile,
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2be84d0..605a203 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -69,7 +69,6 @@
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
-import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -105,7 +104,6 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
-import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemClock;
@@ -190,7 +188,7 @@
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings;
import com.android.server.pm.UserManagerInternal;
-import com.android.server.statusbar.StatusBarManagerService;
+import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.utils.PriorityDump;
import com.android.server.wm.WindowManagerInternal;
@@ -338,8 +336,7 @@
// Ongoing notification
private NotificationManager mNotificationManager;
- KeyguardManager mKeyguardManager;
- private @Nullable StatusBarManagerService mStatusBar;
+ @Nullable private StatusBarManagerInternal mStatusBarManagerInternal;
private final Notification.Builder mImeSwitcherNotification;
private final PendingIntent mImeSwitchPendingIntent;
private boolean mShowOngoingImeSwitcherForPhones;
@@ -1650,9 +1647,7 @@
// Called on ActivityManager thread.
// TODO: Dispatch this to a worker thread as needed.
if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
- StatusBarManagerService statusBarService = (StatusBarManagerService) ServiceManager
- .getService(Context.STATUS_BAR_SERVICE);
- mService.systemRunning(statusBarService);
+ mService.systemRunning();
}
}
@@ -1933,7 +1928,7 @@
/**
* TODO(b/32343335): The entire systemRunning() method needs to be revisited.
*/
- public void systemRunning(StatusBarManagerService statusBar) {
+ public void systemRunning() {
synchronized (ImfLock.class) {
if (DEBUG) {
Slog.d(TAG, "--- systemReady");
@@ -1944,9 +1939,9 @@
final int currentUserId = mSettings.getCurrentUserId();
mSettings.switchCurrentUser(currentUserId,
!mUserManagerInternal.isUserUnlockingOrUnlocked(currentUserId));
- mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
mNotificationManager = mContext.getSystemService(NotificationManager.class);
- mStatusBar = statusBar;
+ mStatusBarManagerInternal =
+ LocalServices.getService(StatusBarManagerInternal.class);
hideStatusBarIconLocked();
updateSystemUiLocked(mImeWindowVis, mBackDisposition);
mShowOngoingImeSwitcherForPhones = mRes.getBoolean(
@@ -2965,11 +2960,11 @@
final CharSequence contentDescription = applicationInfo != null
? userAwarePackageManager.getApplicationLabel(applicationInfo)
: null;
- if (mStatusBar != null) {
- mStatusBar.setIcon(mSlotIme, packageName, iconId, 0,
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setIcon(mSlotIme, packageName, iconId, 0,
contentDescription != null
? contentDescription.toString() : null);
- mStatusBar.setIconVisibility(mSlotIme, true);
+ mStatusBarManagerInternal.setIconVisibility(mSlotIme, true);
}
}
} finally {
@@ -2980,8 +2975,8 @@
@GuardedBy("ImfLock.class")
private void hideStatusBarIconLocked() {
- if (mStatusBar != null) {
- mStatusBar.setIconVisibility(mSlotIme, false);
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setIconVisibility(mSlotIme, false);
}
}
@@ -3007,7 +3002,9 @@
if (!mShowOngoingImeSwitcherForPhones) return false;
if (mMenuController.getSwitchingDialogLocked() != null) return false;
if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded()
- && mKeyguardManager != null && mKeyguardManager.isKeyguardSecure()) return false;
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId())) {
+ return false;
+ }
if ((visibility & InputMethodService.IME_ACTIVE) == 0
|| (visibility & InputMethodService.IME_INVISIBLE) != 0) {
return false;
@@ -3160,9 +3157,9 @@
}
// mImeWindowVis should be updated before calling shouldShowImeSwitcherLocked().
final boolean needsToShowImeSwitcher = shouldShowImeSwitcherLocked(vis);
- if (mStatusBar != null) {
- mStatusBar.setImeWindowStatus(mCurTokenDisplayId, getCurTokenLocked(), vis,
- backDisposition, needsToShowImeSwitcher);
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId,
+ getCurTokenLocked(), vis, backDisposition, needsToShowImeSwitcher);
}
final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
if (imi != null && needsToShowImeSwitcher) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index a25630f..c212e8e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -21,7 +21,6 @@
import android.annotation.Nullable;
import android.app.AlertDialog;
-import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
@@ -56,7 +55,6 @@
private final InputMethodUtils.InputMethodSettings mSettings;
private final InputMethodSubtypeSwitchingController mSwitchingController;
private final ArrayMap<String, InputMethodInfo> mMethodMap;
- private final KeyguardManager mKeyguardManager;
private final WindowManagerInternal mWindowManagerInternal;
private AlertDialog.Builder mDialogBuilder;
@@ -76,7 +74,6 @@
mSettings = mService.mSettings;
mSwitchingController = mService.mSwitchingController;
mMethodMap = mService.mMethodMap;
- mKeyguardManager = mService.mKeyguardManager;
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
}
@@ -209,8 +206,8 @@
}
private boolean isScreenLocked() {
- return mKeyguardManager != null && mKeyguardManager.isKeyguardLocked()
- && mKeyguardManager.isKeyguardSecure();
+ return mWindowManagerInternal.isKeyguardLocked()
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId());
}
void updateKeyboardFromSettingsLocked() {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 58d677c..c899cf2 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -256,10 +256,10 @@
@GuardedBy("mUserCreationAndRemovalLock")
private boolean mBootComplete;
- // Current password metric for all users on the device. Updated when user unlocks
- // the device or changes password. Removed when user is stopped.
+ // Current password metrics for all secured users on the device. Updated when user unlocks the
+ // device or changes password. Removed when user is stopped.
@GuardedBy("this")
- final SparseArray<PasswordMetrics> mUserPasswordMetrics = new SparseArray<>();
+ private final SparseArray<PasswordMetrics> mUserPasswordMetrics = new SparseArray<>();
@VisibleForTesting
protected boolean mHasSecureLockScreen;
@@ -2274,8 +2274,11 @@
}
}
- private PasswordMetrics loadPasswordMetrics(SyntheticPassword sp, int userHandle) {
+ private @Nullable PasswordMetrics loadPasswordMetrics(SyntheticPassword sp, int userHandle) {
synchronized (mSpManager) {
+ if (!isUserSecure(userHandle)) {
+ return null;
+ }
return mSpManager.getPasswordMetrics(sp, getCurrentLskfBasedProtectorId(userHandle),
userHandle);
}
@@ -2703,14 +2706,13 @@
return handle;
}
- private void onCredentialVerified(SyntheticPassword sp, PasswordMetrics metrics, int userId) {
+ private void onCredentialVerified(SyntheticPassword sp, @Nullable PasswordMetrics metrics,
+ int userId) {
if (metrics != null) {
synchronized (this) {
mUserPasswordMetrics.put(userId, metrics);
}
- } else {
- Slog.wtf(TAG, "Null metrics after credential verification");
}
unlockKeystore(sp.deriveKeyStorePassword(), userId);
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index ab69c34..3fd488e 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -43,6 +43,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
@@ -97,9 +98,11 @@
* For each protector, stored under the corresponding protector ID:
* SP_BLOB_NAME: The encrypted SP secret (the SP itself or the P0 value). Always exists.
* PASSWORD_DATA_NAME: Data used for LSKF verification, such as the scrypt salt and
- * parameters. Only exists for LSKF-based protectors.
+ * parameters. Only exists for LSKF-based protectors. Doesn't exist when
+ * the LSKF is empty, except in old protectors.
* PASSWORD_METRICS_NAME: Metrics about the LSKF, encrypted by a key derived from the SP.
- * Only exists for LSKF-based protectors.
+ * Only exists for LSKF-based protectors. Doesn't exist when the LSKF
+ * is empty, except in old protectors.
* SECDISCARDABLE_NAME: A large number of random bytes that all need to be known in order to
* decrypt SP_BLOB_NAME. When the protector is deleted, this file is
* overwritten and deleted as a "best-effort" attempt to support secure
@@ -333,24 +336,15 @@
byte scryptLogP;
public int credentialType;
byte[] salt;
- // This is the Gatekeeper password handle that resulted from enrolling the stretched LSKF,
- // when applicable. This field isn't used if Weaver is available, or in new protectors when
- // the LSKF is empty.
+ // When Weaver is unavailable, this is the Gatekeeper password handle that resulted from
+ // enrolling the stretched LSKF.
public byte[] passwordHandle;
public static PasswordData create(int credentialType) {
PasswordData result = new PasswordData();
- if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
- // When the LSKF is empty, scrypt provides no security benefit, so just use the
- // minimum parameters (N=2, r=1, p=1).
- result.scryptLogN = 1;
- result.scryptLogR = 0;
- result.scryptLogP = 0;
- } else {
- result.scryptLogN = PASSWORD_SCRYPT_LOG_N;
- result.scryptLogR = PASSWORD_SCRYPT_LOG_R;
- result.scryptLogP = PASSWORD_SCRYPT_LOG_P;
- }
+ result.scryptLogN = PASSWORD_SCRYPT_LOG_N;
+ result.scryptLogR = PASSWORD_SCRYPT_LOG_R;
+ result.scryptLogP = PASSWORD_SCRYPT_LOG_P;
result.credentialType = credentialType;
result.salt = secureRandom(PASSWORD_SALT_LENGTH);
return result;
@@ -611,7 +605,6 @@
int getCredentialType(long protectorId, int userId) {
byte[] passwordData = loadState(PASSWORD_DATA_NAME, protectorId, userId);
if (passwordData == null) {
- Slog.w(TAG, "getCredentialType: encountered empty password data for user " + userId);
return LockPatternUtils.CREDENTIAL_TYPE_NONE;
}
return PasswordData.fromBytes(passwordData).credentialType;
@@ -783,7 +776,8 @@
public long createLskfBasedProtector(IGateKeeperService gatekeeper,
LockscreenCredential credential, SyntheticPassword sp, int userId) {
long protectorId = generateProtectorId();
- PasswordData pwd = PasswordData.create(credential.getType());
+ // There's no need to store password data about an empty LSKF.
+ PasswordData pwd = credential.isNone() ? null : PasswordData.create(credential.getType());
byte[] stretchedLskf = stretchLskf(credential, pwd);
long sid = GateKeeper.INVALID_SECURE_USER_ID;
final byte[] protectorSecret;
@@ -837,8 +831,10 @@
// No need to pass in quality since the credential type already encodes sufficient info
synchronizeFrpPassword(pwd, 0, userId);
}
- saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
- savePasswordMetrics(credential, sp, protectorId, userId);
+ if (!credential.isNone()) {
+ saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
+ savePasswordMetrics(credential, sp, protectorId, userId);
+ }
createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret,
sid, userId);
return protectorId;
@@ -883,26 +879,26 @@
public void migrateFrpPasswordLocked(long protectorId, UserInfo userInfo,
int requestedQuality) {
if (mStorage.getPersistentDataBlockManager() != null
- && LockPatternUtils.userOwnsFrpCredential(mContext, userInfo)) {
+ && LockPatternUtils.userOwnsFrpCredential(mContext, userInfo)
+ && getCredentialType(protectorId, userInfo.id) !=
+ LockPatternUtils.CREDENTIAL_TYPE_NONE) {
PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
userInfo.id));
- if (pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
- int weaverSlot = loadWeaverSlot(protectorId, userInfo.id);
- if (weaverSlot != INVALID_WEAVER_SLOT) {
- synchronizeWeaverFrpPassword(pwd, requestedQuality, userInfo.id, weaverSlot);
- } else {
- synchronizeFrpPassword(pwd, requestedQuality, userInfo.id);
- }
+ int weaverSlot = loadWeaverSlot(protectorId, userInfo.id);
+ if (weaverSlot != INVALID_WEAVER_SLOT) {
+ synchronizeWeaverFrpPassword(pwd, requestedQuality, userInfo.id, weaverSlot);
+ } else {
+ synchronizeFrpPassword(pwd, requestedQuality, userInfo.id);
}
}
}
- private void synchronizeFrpPassword(PasswordData pwd,
- int requestedQuality, int userId) {
+ private void synchronizeFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
+ int userId) {
if (mStorage.getPersistentDataBlockManager() != null
&& LockPatternUtils.userOwnsFrpCredential(mContext,
mUserManager.getUserInfo(userId))) {
- if (pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ if (pwd != null && pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
mStorage.writePersistentDataBlock(PersistentData.TYPE_SP, userId, requestedQuality,
pwd.toBytes());
} else {
@@ -911,12 +907,12 @@
}
}
- private void synchronizeWeaverFrpPassword(PasswordData pwd, int requestedQuality, int userId,
- int weaverSlot) {
+ private void synchronizeWeaverFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
+ int userId, int weaverSlot) {
if (mStorage.getPersistentDataBlockManager() != null
&& LockPatternUtils.userOwnsFrpCredential(mContext,
mUserManager.getUserInfo(userId))) {
- if (pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ if (pwd != null && pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, weaverSlot,
requestedQuality, pwd.toBytes());
} else {
@@ -1047,12 +1043,20 @@
return result;
}
- PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
- userId));
-
- if (!credential.checkAgainstStoredType(pwd.credentialType)) {
+ // Load the PasswordData file. If it doesn't exist, then the LSKF is empty (i.e.,
+ // CREDENTIAL_TYPE_NONE), and we'll skip the scrypt and Gatekeeper steps. If it exists,
+ // then either the LSKF is nonempty, or it's an old protector that uses scrypt and
+ // Gatekeeper even though the LSKF is empty.
+ byte[] pwdDataBytes = loadState(PASSWORD_DATA_NAME, protectorId, userId);
+ PasswordData pwd = null;
+ int storedType = LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ if (pwdDataBytes != null) {
+ pwd = PasswordData.fromBytes(pwdDataBytes);
+ storedType = pwd.credentialType;
+ }
+ if (!credential.checkAgainstStoredType(storedType)) {
Slog.e(TAG, TextUtils.formatSimple("Credential type mismatch: expected %d actual %d",
- pwd.credentialType, credential.getType()));
+ storedType, credential.getType()));
result.gkResponse = VerifyCredentialResponse.ERROR;
return result;
}
@@ -1078,7 +1082,7 @@
} else {
// Weaver is unavailable, so the protector uses Gatekeeper to verify the LSKF, unless
// the LSKF is empty in which case Gatekeeper might not have been used at all.
- if (pwd.passwordHandle == null) {
+ if (pwd == null || pwd.passwordHandle == null) {
if (!credential.isNone()) {
Slog.e(TAG, "Missing Gatekeeper password handle for nonempty LSKF");
result.gkResponse = VerifyCredentialResponse.ERROR;
@@ -1149,7 +1153,8 @@
// Upgrade case: store the metrics if the device did not have stored metrics before, should
// only happen once on old protectors.
- if (result.syntheticPassword != null && !hasPasswordMetrics(protectorId, userId)) {
+ if (result.syntheticPassword != null && !credential.isNone() &&
+ !hasPasswordMetrics(protectorId, userId)) {
savePasswordMetrics(credential, result.syntheticPassword, protectorId, userId);
}
return result;
@@ -1415,6 +1420,11 @@
}
}
+ @VisibleForTesting
+ boolean hasPasswordData(long protectorId, int userId) {
+ return hasState(PASSWORD_DATA_NAME, protectorId, userId);
+ }
+
/**
* Retrieves a user's saved password metrics from their LSKF-based SP protector. The
* SyntheticPassword itself is needed to decrypt the file containing the password metrics.
@@ -1422,10 +1432,16 @@
public @Nullable PasswordMetrics getPasswordMetrics(SyntheticPassword sp, long protectorId,
int userId) {
final byte[] encrypted = loadState(PASSWORD_METRICS_NAME, protectorId, userId);
- if (encrypted == null) return null;
+ if (encrypted == null) {
+ Slogf.e(TAG, "Failed to read password metrics file for user %d", userId);
+ return null;
+ }
final byte[] decrypted = SyntheticPasswordCrypto.decrypt(sp.deriveMetricsKey(),
/* personalization= */ new byte[0], encrypted);
- if (decrypted == null) return null;
+ if (decrypted == null) {
+ Slogf.e(TAG, "Failed to decrypt password metrics file for user %d", userId);
+ return null;
+ }
return VersionedPasswordMetrics.deserialize(decrypted).getMetrics();
}
@@ -1437,7 +1453,8 @@
saveState(PASSWORD_METRICS_NAME, encrypted, protectorId, userId);
}
- private boolean hasPasswordMetrics(long protectorId, int userId) {
+ @VisibleForTesting
+ boolean hasPasswordMetrics(long protectorId, int userId) {
return hasState(PASSWORD_METRICS_NAME, protectorId, userId);
}
@@ -1500,8 +1517,23 @@
return TextUtils.formatSimple("%s%x", PROTECTOR_KEY_ALIAS_PREFIX, protectorId);
}
- private byte[] stretchLskf(LockscreenCredential credential, PasswordData data) {
+ /**
+ * Stretches <code>credential</code>, if needed, using the parameters from <code>data</code>.
+ * <p>
+ * When the credential is empty, stetching provides no security benefit. Thus, new protectors
+ * for an empty credential use <code>null</code> {@link PasswordData} and skip the stretching.
+ * <p>
+ * However, old protectors always stored {@link PasswordData} and did the stretching, regardless
+ * of whether the credential was empty or not. For this reason, this method also continues to
+ * support stretching of empty credentials so that old protectors can still be unlocked.
+ */
+ @VisibleForTesting
+ byte[] stretchLskf(LockscreenCredential credential, @Nullable PasswordData data) {
final byte[] password = credential.isNone() ? DEFAULT_PASSWORD : credential.getCredential();
+ if (data == null) {
+ Preconditions.checkArgument(credential.isNone());
+ return Arrays.copyOf(password, STRETCHED_LSKF_LENGTH);
+ }
return scrypt(password, data.salt, 1 << data.scryptLogN, 1 << data.scryptLogR,
1 << data.scryptLogP, STRETCHED_LSKF_LENGTH);
}
diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
index b1e8b40..dcdb881 100644
--- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
+++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.BroadcastOptions;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -37,6 +38,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
import java.util.List;
/**
@@ -102,15 +104,19 @@
}
/**
- * Creates a new instance.
+ * Creates a new instance from a {@link PendingIntent}.
*
- * @param context context
+ * <p>This method assumes the session package name has been validated and effectively belongs to
+ * the media session's owner.
+ *
* @param userId userId
- * @param pendingIntent pending intent
- * @return Can be {@code null} if pending intent was null.
+ * @param pendingIntent pending intent that will receive media button events
+ * @param sessionPackageName package name of media session owner
+ * @return {@link MediaButtonReceiverHolder} instance or {@code null} if pending intent was
+ * null.
*/
- public static MediaButtonReceiverHolder create(Context context, int userId,
- PendingIntent pendingIntent, String sessionPackageName) {
+ public static MediaButtonReceiverHolder create(
+ int userId, @Nullable PendingIntent pendingIntent, String sessionPackageName) {
if (pendingIntent == null) {
return null;
}
@@ -315,7 +321,7 @@
}
private static ComponentName getComponentName(PendingIntent pendingIntent, int componentType) {
- List<ResolveInfo> resolveInfos = null;
+ List<ResolveInfo> resolveInfos = Collections.emptyList();
switch (componentType) {
case COMPONENT_TYPE_ACTIVITY:
resolveInfos = pendingIntent.queryIntentComponents(
@@ -333,32 +339,37 @@
PACKAGE_MANAGER_COMMON_FLAGS | PackageManager.GET_RECEIVERS);
break;
}
- if (resolveInfos != null && !resolveInfos.isEmpty()) {
- return createComponentName(resolveInfos.get(0));
+
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ ComponentInfo componentInfo = getComponentInfo(resolveInfo);
+ if (componentInfo != null && TextUtils.equals(componentInfo.packageName,
+ pendingIntent.getCreatorPackage())
+ && componentInfo.packageName != null && componentInfo.name != null) {
+ return new ComponentName(componentInfo.packageName, componentInfo.name);
+ }
}
+
return null;
}
- private static ComponentName createComponentName(ResolveInfo resolveInfo) {
- if (resolveInfo == null) {
- return null;
- }
- ComponentInfo componentInfo;
+ /**
+ * Retrieves the {@link ComponentInfo} from a {@link ResolveInfo} instance. Similar to {@link
+ * ResolveInfo#getComponentInfo()}, but returns {@code null} if this {@link ResolveInfo} points
+ * to a content provider.
+ *
+ * @param resolveInfo Where to extract the {@link ComponentInfo} from.
+ * @return Either a non-null {@link ResolveInfo#activityInfo} or {@link
+ * ResolveInfo#serviceInfo}. Otherwise {@code null} if {@link ResolveInfo#providerInfo} is
+ * not {@code null}.
+ */
+ private static ComponentInfo getComponentInfo(@NonNull ResolveInfo resolveInfo) {
// Code borrowed from ResolveInfo#getComponentInfo().
if (resolveInfo.activityInfo != null) {
- componentInfo = resolveInfo.activityInfo;
+ return resolveInfo.activityInfo;
} else if (resolveInfo.serviceInfo != null) {
- componentInfo = resolveInfo.serviceInfo;
+ return resolveInfo.serviceInfo;
} else {
- // We're not interested in content provider.
- return null;
- }
- // Code borrowed from ComponentInfo#getComponentName().
- try {
- return new ComponentName(componentInfo.packageName, componentInfo.name);
- } catch (IllegalArgumentException | NullPointerException e) {
- // This may be happen if resolveActivity() end up with matching multiple activities.
- // see PackageManager#resolveActivity().
+ // We're not interested in content providers.
return null;
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 28c7a39..16155a0 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -914,8 +914,7 @@
}
@Override
- public void setMediaButtonReceiver(PendingIntent pi, String sessionPackageName)
- throws RemoteException {
+ public void setMediaButtonReceiver(PendingIntent pi) throws RemoteException {
final long token = Binder.clearCallingIdentity();
try {
if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER)
@@ -923,7 +922,7 @@
return;
}
mMediaButtonReceiverHolder =
- MediaButtonReceiverHolder.create(mContext, mUserId, pi, sessionPackageName);
+ MediaButtonReceiverHolder.create(mUserId, pi, mPackageName);
mService.onMediaButtonReceiverChanged(MediaSessionRecord.this);
} finally {
Binder.restoreCallingIdentity(token);
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index b89147e..d08150c 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -2285,9 +2285,9 @@
PendingIntent pi = mCustomMediaKeyDispatcher.getMediaButtonReceiver(keyEvent,
uid, asSystemService);
if (pi != null) {
- mediaButtonReceiverHolder = MediaButtonReceiverHolder.create(mContext,
- mCurrentFullUserRecord.mFullUserId, pi,
- /* sessionPackageName= */ "");
+ mediaButtonReceiverHolder =
+ MediaButtonReceiverHolder.create(
+ mCurrentFullUserRecord.mFullUserId, pi, "");
}
}
}
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index 15cd639..423c2760 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -91,7 +91,7 @@
* other hand, not overriding in {@link ComputerLocked} may leave a function walking
* unstable data.
*/
-@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public interface Computer extends PackageDataSnapshot {
int getVersion();
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index ed846db..bef87c4 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -30,7 +30,6 @@
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER;
-import static android.content.pm.PackageManager.MATCH_ALL;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.content.pm.PackageManager.MATCH_APEX;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
@@ -49,7 +48,6 @@
import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_PARENT;
-import static com.android.server.pm.PackageManagerService.DEBUG_DOMAIN_VERIFICATION;
import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
import static com.android.server.pm.PackageManagerService.DEBUG_INSTANT;
import static com.android.server.pm.PackageManagerService.DEBUG_PACKAGE_INFO;
@@ -116,7 +114,6 @@
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
-import android.util.SparseBooleanArray;
import android.util.TypedXmlSerializer;
import android.util.Xml;
import android.util.proto.ProtoOutputStream;
@@ -145,7 +142,6 @@
import com.android.server.pm.pkg.component.ParsedService;
import com.android.server.pm.resolution.ComponentResolverApi;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
-import com.android.server.pm.verify.domain.DomainVerificationUtils;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.utils.WatchedArrayMap;
import com.android.server.utils.WatchedLongSparseArray;
@@ -411,6 +407,7 @@
private final CompilerStats mCompilerStats;
private final BackgroundDexOptService mBackgroundDexOptService;
private final PackageManagerInternal.ExternalSourcesPolicy mExternalSourcesPolicy;
+ private final CrossProfileIntentResolverEngine mCrossProfileIntentResolverEngine;
// PackageManagerService attributes that are primitives are referenced through the
// pms object directly. Primitives are the only attributes so referenced.
@@ -464,6 +461,8 @@
mCompilerStats = args.service.mCompilerStats;
mBackgroundDexOptService = args.service.mBackgroundDexOptService;
mExternalSourcesPolicy = args.service.mExternalSourcesPolicy;
+ mCrossProfileIntentResolverEngine = new CrossProfileIntentResolverEngine(
+ mUserManager, mDomainVerificationManager, mDefaultAppProvider);
// Used to reference PMS attributes that are primitives and which are not
// updated under control of the PMS lock.
@@ -735,101 +734,72 @@
// reader
boolean sortResult = false;
boolean addInstant = false;
- List<ResolveInfo> result = null;
+ List<ResolveInfo> result = new ArrayList<>();
+ // crossProfileResults will hold resolve infos from resolution across profiles.
+ List<CrossProfileDomainInfo> crossProfileResults = new ArrayList<>();
if (pkgName == null) {
- List<CrossProfileIntentFilter> matchingFilters =
- getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);
- // Check for results that need to skip the current profile.
- ResolveInfo skipProfileInfo = querySkipCurrentProfileIntents(matchingFilters,
- intent, resolvedType, flags, userId);
- if (skipProfileInfo != null) {
- List<ResolveInfo> xpResult = new ArrayList<>(1);
- xpResult.add(skipProfileInfo);
- return new QueryIntentActivitiesResult(
- applyPostResolutionFilter(
- filterIfNotSystemUser(xpResult, userId), instantAppPkgName,
- allowDynamicSplits, filterCallingUid, resolveForStart, userId,
- intent));
+ if (!mCrossProfileIntentResolverEngine.shouldSkipCurrentProfile(this, intent,
+ resolvedType, userId)) {
+ /*
+ Check for results in the current profile only if there is no
+ {@link CrossProfileIntentFilter} for user with flag
+ {@link PackageManager.SKIP_CURRENT_PROFILE} set.
+ */
+ result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this,
+ intent, resolvedType, flags, userId), userId));
}
-
- // Check for results in the current profile.
- result = filterIfNotSystemUser(mComponentResolver.queryActivities(this,
- intent, resolvedType, flags, userId), userId);
addInstant = isInstantAppResolutionAllowed(intent, result, userId,
false /*skipPackageCheck*/, flags);
- // Check for cross profile results.
+
boolean hasNonNegativePriorityResult = hasNonNegativePriority(result);
- CrossProfileDomainInfo specificXpInfo = queryCrossProfileIntents(
- matchingFilters, intent, resolvedType, flags, userId,
- hasNonNegativePriorityResult);
- if (intent.hasWebURI()) {
- CrossProfileDomainInfo generalXpInfo = null;
- final UserInfo parent = getProfileParent(userId);
- if (parent != null) {
- generalXpInfo = getCrossProfileDomainPreferredLpr(intent, resolvedType,
- flags, userId, parent.id);
- }
- // Generalized cross profile intents take precedence over specific.
- // Note that this is the opposite of the intuitive order.
- CrossProfileDomainInfo prioritizedXpInfo =
- generalXpInfo != null ? generalXpInfo : specificXpInfo;
-
- if (!addInstant) {
- if (result.isEmpty() && prioritizedXpInfo != null) {
- // No result in current profile, but found candidate in parent user.
- // And we are not going to add ephemeral app, so we can return the
- // result straight away.
- result.add(prioritizedXpInfo.mResolveInfo);
- return new QueryIntentActivitiesResult(
- applyPostResolutionFilter(result, instantAppPkgName,
- allowDynamicSplits, filterCallingUid, resolveForStart,
- userId, intent));
- } else if (result.size() <= 1 && prioritizedXpInfo == null) {
- // No result in parent user and <= 1 result in current profile, and we
- // are not going to add ephemeral app, so we can return the result
- // without further processing.
- return new QueryIntentActivitiesResult(
- applyPostResolutionFilter(result, instantAppPkgName,
- allowDynamicSplits, filterCallingUid, resolveForStart,
- userId, intent));
- }
- }
-
- // We have more than one candidate (combining results from current and parent
- // profile), so we need filtering and sorting.
- result = filterCandidatesWithDomainPreferredActivitiesLPr(
- intent, flags, result, prioritizedXpInfo, userId);
- sortResult = true;
- } else {
- // If not web Intent, just add result to candidate set and let ResolverActivity
- // figure it out.
- if (specificXpInfo != null) {
- result.add(specificXpInfo.mResolveInfo);
- sortResult = true;
- }
- }
+ /*
+ Calling {@link com.android.server.pm.CrossProfileIntentResolverEngine#resolveIntent} to
+ get list of {@link CrossProfileDomainInfo} which have {@link ResolveInfo}s from linked
+ profiles.
+ */
+ crossProfileResults = mCrossProfileIntentResolverEngine.resolveIntent(this, intent,
+ resolvedType, userId, flags, pkgName, hasNonNegativePriorityResult,
+ mSettings::getPackage);
+ if (intent.hasWebURI() || !crossProfileResults.isEmpty()) sortResult = true;
} else {
final PackageStateInternal setting =
getPackageStateInternal(pkgName, Process.SYSTEM_UID);
- result = null;
+
if (setting != null && setting.getAndroidPackage() != null && (resolveForStart
|| !shouldFilterApplication(setting, filterCallingUid, userId))) {
- result = filterIfNotSystemUser(mComponentResolver.queryActivities(this,
+ result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this,
intent, resolvedType, flags, setting.getAndroidPackage().getActivities(),
- userId), userId);
+ userId), userId));
}
if (result == null || result.size() == 0) {
// the caller wants to resolve for a particular package; however, there
// were no installed results, so, try to find an ephemeral result
addInstant = isInstantAppResolutionAllowed(intent, null /*result*/, userId,
true /*skipPackageCheck*/, flags);
- if (result == null) {
- result = new ArrayList<>();
- }
}
+ /*
+ Calling {@link com.android.server.pm.CrossProfileIntentResolverEngine#resolveIntent} to
+ get list of {@link CrossProfileDomainInfo} which have {@link ResolveInfo}s from linked
+ profiles.
+ */
+ crossProfileResults = mCrossProfileIntentResolverEngine.resolveIntent(this, intent,
+ resolvedType, userId, flags, pkgName, false,
+ mSettings::getPackage);
}
- return new QueryIntentActivitiesResult(sortResult, addInstant, result);
+
+ /*
+ Calling {@link com.android.server.pm.
+ CrossProfileIntentResolverEngine#combineFilterAndCreateQueryAcitivitesResponse} to
+ combine results from current and cross profiles. This also filters any resolve info
+ based on domain preference(if required).
+ */
+ return mCrossProfileIntentResolverEngine
+ .combineFilterAndCreateQueryActivitiesResponse(this, intent, resolvedType,
+ instantAppPkgName, pkgName, allowDynamicSplits, flags, userId,
+ filterCallingUid, resolveForStart, result, crossProfileResults,
+ areWebInstantAppsDisabled(userId), addInstant, sortResult,
+ mSettings::getPackage);
}
/**
@@ -1046,126 +1016,6 @@
return null;
}
- protected ArrayList<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPrBody(
- Intent intent, long matchFlags, List<ResolveInfo> candidates,
- CrossProfileDomainInfo xpDomainInfo, int userId, boolean debug) {
- final ArrayList<ResolveInfo> result = new ArrayList<>();
- final ArrayList<ResolveInfo> matchAllList = new ArrayList<>();
- final ArrayList<ResolveInfo> undefinedList = new ArrayList<>();
-
- // Blocking instant apps is usually done in applyPostResolutionFilter, but since
- // domain verification can resolve to a single result, which can be an instant app,
- // it will then be filtered to an empty list in that method. Instead, do blocking
- // here so that instant apps can be ignored for approval filtering and a lower
- // priority result chosen instead.
- final boolean blockInstant = intent.isWebIntent() && areWebInstantAppsDisabled(userId);
-
- final int count = candidates.size();
- // First, try to use approved apps.
- for (int n = 0; n < count; n++) {
- ResolveInfo info = candidates.get(n);
- if (blockInstant && (info.isInstantAppAvailable
- || isInstantAppInternal(info.activityInfo.packageName, userId,
- Process.SYSTEM_UID))) {
- continue;
- }
-
- // Add to the special match all list (Browser use case)
- if (info.handleAllWebDataURI) {
- matchAllList.add(info);
- } else {
- undefinedList.add(info);
- }
- }
-
- // We'll want to include browser possibilities in a few cases
- boolean includeBrowser = false;
-
- if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) {
- result.addAll(undefinedList);
- // Maybe add one for the other profile.
- if (xpDomainInfo != null && xpDomainInfo.mHighestApprovalLevel
- > DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE) {
- result.add(xpDomainInfo.mResolveInfo);
- }
- includeBrowser = true;
- } else {
- Pair<List<ResolveInfo>, Integer> infosAndLevel = mDomainVerificationManager
- .filterToApprovedApp(intent, undefinedList, userId,
- mSettings::getPackage);
- List<ResolveInfo> approvedInfos = infosAndLevel.first;
- Integer highestApproval = infosAndLevel.second;
-
- // If no apps are approved for the domain, resolve only to browsers
- if (approvedInfos.isEmpty()) {
- includeBrowser = true;
- if (xpDomainInfo != null && xpDomainInfo.mHighestApprovalLevel
- > DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE) {
- result.add(xpDomainInfo.mResolveInfo);
- }
- } else {
- result.addAll(approvedInfos);
-
- // If the other profile has an app that's higher approval, add it
- if (xpDomainInfo != null
- && xpDomainInfo.mHighestApprovalLevel > highestApproval) {
- result.add(xpDomainInfo.mResolveInfo);
- }
- }
- }
-
- if (includeBrowser) {
- // Also add browsers (all of them or only the default one)
- if (DEBUG_DOMAIN_VERIFICATION) {
- Slog.v(TAG, " ...including browsers in candidate set");
- }
- if ((matchFlags & MATCH_ALL) != 0) {
- result.addAll(matchAllList);
- } else {
- // Browser/generic handling case. If there's a default browser, go straight
- // to that (but only if there is no other higher-priority match).
- final String defaultBrowserPackageName = mDefaultAppProvider.getDefaultBrowser(
- userId);
- int maxMatchPrio = 0;
- ResolveInfo defaultBrowserMatch = null;
- final int numCandidates = matchAllList.size();
- for (int n = 0; n < numCandidates; n++) {
- ResolveInfo info = matchAllList.get(n);
- // track the highest overall match priority...
- if (info.priority > maxMatchPrio) {
- maxMatchPrio = info.priority;
- }
- // ...and the highest-priority default browser match
- if (info.activityInfo.packageName.equals(defaultBrowserPackageName)) {
- if (defaultBrowserMatch == null
- || (defaultBrowserMatch.priority < info.priority)) {
- if (debug) {
- Slog.v(TAG, "Considering default browser match " + info);
- }
- defaultBrowserMatch = info;
- }
- }
- }
- if (defaultBrowserMatch != null
- && defaultBrowserMatch.priority >= maxMatchPrio
- && !TextUtils.isEmpty(defaultBrowserPackageName)) {
- if (debug) {
- Slog.v(TAG, "Default browser match " + defaultBrowserMatch);
- }
- result.add(defaultBrowserMatch);
- } else {
- result.addAll(matchAllList);
- }
- }
-
- // If there is nothing selected, add all candidates
- if (result.size() == 0) {
- result.addAll(candidates);
- }
- }
- return result;
- }
-
/**
* Report the 'Home' activity which is currently set as "always use this one". If non is set
* then reports the most likely home activity or null if there are more than one.
@@ -1279,7 +1129,8 @@
if (result == null) {
result = new CrossProfileDomainInfo(createForwardingResolveInfoUnchecked(
- new WatchedIntentFilter(), sourceUserId, parentUserId), approvalLevel);
+ new WatchedIntentFilter(), sourceUserId, parentUserId), approvalLevel,
+ parentUserId);
} else {
result.mHighestApprovalLevel =
Math.max(approvalLevel, result.mHighestApprovalLevel);
@@ -1303,7 +1154,8 @@
Intent intent, String resolvedType, int userId) {
CrossProfileIntentResolver resolver = mSettings.getCrossProfileIntentResolver(userId);
if (resolver != null) {
- return resolver.queryIntent(this, intent, resolvedType, false /*defaultOnly*/, userId);
+ return resolver.queryIntent(this, intent, resolvedType, false /*defaultOnly*/,
+ userId);
}
return null;
}
@@ -1467,30 +1319,6 @@
return resolveInfos;
}
- private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Intent intent,
- long matchFlags, List<ResolveInfo> candidates, CrossProfileDomainInfo xpDomainInfo,
- int userId) {
- final boolean debug = (intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0;
-
- if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
- Slog.v(TAG, "Filtering results with preferred activities. Candidates count: "
- + candidates.size());
- }
-
- final ArrayList<ResolveInfo> result =
- filterCandidatesWithDomainPreferredActivitiesLPrBody(
- intent, matchFlags, candidates, xpDomainInfo, userId, debug);
-
- if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
- Slog.v(TAG, "Filtered results with preferred activities. New candidates count: "
- + result.size());
- for (ResolveInfo info : result) {
- Slog.v(TAG, " + " + info.activityInfo);
- }
- }
- return result;
- }
-
/**
* Filter out activities with systemUserOnly flag set, when current user is not System.
*
@@ -1930,63 +1758,6 @@
return new ParceledListSlice<>(list);
}
- /**
- * If the filter's target user can handle the intent and is enabled: a [ResolveInfo] that
- * will forward the intent to the filter's target user, along with the highest approval of
- * any handler in the target user. Otherwise, returns null.
- */
- @Nullable
- private CrossProfileDomainInfo createForwardingResolveInfo(
- @NonNull CrossProfileIntentFilter filter, @NonNull Intent intent,
- @Nullable String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
- int sourceUserId) {
- int targetUserId = filter.getTargetUserId();
- if (!isUserEnabled(targetUserId)) {
- return null;
- }
-
- List<ResolveInfo> resultTargetUser = mComponentResolver.queryActivities(this, intent,
- resolvedType, flags, targetUserId);
- if (CollectionUtils.isEmpty(resultTargetUser)) {
- return null;
- }
-
- ResolveInfo forwardingInfo = null;
- for (int i = resultTargetUser.size() - 1; i >= 0; i--) {
- ResolveInfo targetUserResolveInfo = resultTargetUser.get(i);
- if ((targetUserResolveInfo.activityInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_SUSPENDED) == 0) {
- forwardingInfo = createForwardingResolveInfoUnchecked(filter, sourceUserId,
- targetUserId);
- break;
- }
- }
-
- if (forwardingInfo == null) {
- // If all the matches in the target profile are suspended, return null.
- return null;
- }
-
- int highestApprovalLevel = DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE;
-
- int size = resultTargetUser.size();
- for (int i = 0; i < size; i++) {
- ResolveInfo riTargetUser = resultTargetUser.get(i);
- if (riTargetUser.handleAllWebDataURI) {
- continue;
- }
- String packageName = riTargetUser.activityInfo.packageName;
- PackageStateInternal ps = mSettings.getPackage(packageName);
- if (ps == null) {
- continue;
- }
- highestApprovalLevel = Math.max(highestApprovalLevel, mDomainVerificationManager
- .approvalLevelForDomain(ps, intent, flags, targetUserId));
- }
-
- return new CrossProfileDomainInfo(forwardingInfo, highestApprovalLevel);
- }
-
public final ResolveInfo createForwardingResolveInfoUnchecked(WatchedIntentFilter filter,
int sourceUserId, int targetUserId) {
ResolveInfo forwardingResolveInfo = new ResolveInfo();
@@ -2022,83 +1793,6 @@
return forwardingResolveInfo;
}
- // Return matching ResolveInfo in target user if any.
- @Nullable
- private CrossProfileDomainInfo queryCrossProfileIntents(
- List<CrossProfileIntentFilter> matchingFilters, Intent intent, String resolvedType,
- long flags, int sourceUserId, boolean matchInCurrentProfile) {
- if (matchingFilters == null) {
- return null;
- }
- // Two {@link CrossProfileIntentFilter}s can have the same targetUserId and
- // match the same intent. For performance reasons, it is better not to
- // run queryIntent twice for the same userId
- SparseBooleanArray alreadyTriedUserIds = new SparseBooleanArray();
-
- CrossProfileDomainInfo resultInfo = null;
-
- int size = matchingFilters.size();
- for (int i = 0; i < size; i++) {
- CrossProfileIntentFilter filter = matchingFilters.get(i);
- int targetUserId = filter.getTargetUserId();
- boolean skipCurrentProfile =
- (filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0;
- boolean skipCurrentProfileIfNoMatchFound =
- (filter.getFlags() & PackageManager.ONLY_IF_NO_MATCH_FOUND) != 0;
- if (!skipCurrentProfile && !alreadyTriedUserIds.get(targetUserId)
- && (!skipCurrentProfileIfNoMatchFound || !matchInCurrentProfile)) {
- // Checking if there are activities in the target user that can handle the
- // intent.
- CrossProfileDomainInfo info = createForwardingResolveInfo(filter, intent,
- resolvedType, flags, sourceUserId);
- if (info != null) {
- resultInfo = info;
- break;
- }
- alreadyTriedUserIds.put(targetUserId, true);
- }
- }
-
- if (resultInfo == null) {
- return null;
- }
-
- ResolveInfo forwardingResolveInfo = resultInfo.mResolveInfo;
- if (!isUserEnabled(forwardingResolveInfo.targetUserId)) {
- return null;
- }
-
- List<ResolveInfo> filteredResult =
- filterIfNotSystemUser(Collections.singletonList(forwardingResolveInfo),
- sourceUserId);
- if (filteredResult.isEmpty()) {
- return null;
- }
-
- return resultInfo;
- }
-
- private ResolveInfo querySkipCurrentProfileIntents(
- List<CrossProfileIntentFilter> matchingFilters, Intent intent, String resolvedType,
- long flags, int sourceUserId) {
- if (matchingFilters != null) {
- int size = matchingFilters.size();
- for (int i = 0; i < size; i++) {
- CrossProfileIntentFilter filter = matchingFilters.get(i);
- if ((filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0) {
- // Checking if there are activities in the target user that can handle the
- // intent.
- CrossProfileDomainInfo info = createForwardingResolveInfo(filter, intent,
- resolvedType, flags, sourceUserId);
- if (info != null) {
- return info.mResolveInfo;
- }
- }
- }
- }
- return null;
- }
-
public final ServiceInfo getServiceInfo(ComponentName component,
@PackageManager.ResolveInfoFlagsBits long flags, int userId) {
if (!mUserManager.exists(userId)) return null;
@@ -2740,16 +2434,6 @@
}
}
- private boolean isUserEnabled(int userId) {
- final long callingId = Binder.clearCallingIdentity();
- try {
- UserInfo userInfo = mUserManager.getUserInfo(userId);
- return userInfo != null && userInfo.isEnabled();
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
/**
* Returns whether or not access to the application should be filtered.
* <p>
@@ -5691,13 +5375,9 @@
@UserIdInt int sourceUserId, @UserIdInt int targetUserId) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
- List<CrossProfileIntentFilter> matches =
- getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
- if (matches != null) {
- int size = matches.size();
- for (int i = 0; i < size; i++) {
- if (matches.get(i).getTargetUserId() == targetUserId) return true;
- }
+ if (mCrossProfileIntentResolverEngine.canReachTo(this, intent, resolvedType,
+ sourceUserId, targetUserId)) {
+ return true;
}
if (intent.hasWebURI()) {
// cross-profile app linking works only towards the parent.
diff --git a/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java b/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java
index 31f4fa3..72f3afc 100644
--- a/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java
+++ b/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java
@@ -16,12 +16,23 @@
package com.android.server.pm;
+import android.annotation.UserIdInt;
import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
public final class CrossProfileDomainInfo {
/* ResolveInfo for IntentForwarderActivity to send the intent to the other profile */
ResolveInfo mResolveInfo;
int mHighestApprovalLevel;
+ @UserIdInt
+ int mTargetUserId = UserHandle.USER_CURRENT; // default as current user
+
+ CrossProfileDomainInfo(ResolveInfo resolveInfo, int highestApprovalLevel, @UserIdInt
+ int targetUserId) {
+ this.mResolveInfo = resolveInfo;
+ this.mHighestApprovalLevel = highestApprovalLevel;
+ this.mTargetUserId = targetUserId;
+ }
CrossProfileDomainInfo(ResolveInfo resolveInfo, int highestApprovalLevel) {
this.mResolveInfo = resolveInfo;
@@ -33,6 +44,7 @@
return "CrossProfileDomainInfo{"
+ "resolveInfo=" + mResolveInfo
+ ", highestApprovalLevel=" + mHighestApprovalLevel
+ + ", targetUserId= " + mTargetUserId
+ '}';
}
}
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
new file mode 100644
index 0000000..3f82923
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -0,0 +1,575 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static android.content.pm.PackageManager.MATCH_ALL;
+
+import static com.android.server.pm.PackageManagerService.DEBUG_DOMAIN_VERIFICATION;
+import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
+import static com.android.server.pm.PackageManagerService.TAG;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.server.LocalServices;
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
+import com.android.server.pm.verify.domain.DomainVerificationUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Rule based engine which decides strategy to be used for source,target pair and does cross profile
+ * intent resolution. Currently, we have only default and clone strategy. The major known use-case
+ * for default is work profile.
+ */
+public class CrossProfileIntentResolverEngine {
+
+ private final UserManagerService mUserManager;
+ private final DomainVerificationManagerInternal mDomainVerificationManager;
+ private final DefaultAppProvider mDefaultAppProvider;
+
+ public CrossProfileIntentResolverEngine(UserManagerService userManager,
+ DomainVerificationManagerInternal domainVerificationManager,
+ DefaultAppProvider defaultAppProvider) {
+ mUserManager = userManager;
+ mDomainVerificationManager = domainVerificationManager;
+ mDefaultAppProvider = defaultAppProvider;
+ }
+
+ /**
+ * Returns the list of {@link CrossProfileDomainInfo} which contains {@link ResolveInfo} from
+ * profiles linked directly/indirectly to user. Work-Owner as well as Clone-Owner
+ * are directly related as they are child of owner. Work-Clone are indirectly linked through
+ * owner profile.
+ * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param userId source user for which intent request is called
+ * @param flags used for intent resolution
+ * @param pkgName the application package name this Intent is limited to.
+ * @param hasNonNegativePriorityResult signifies if current profile have any non-negative(active
+ * and valid) ResolveInfo in current profile.
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return list of {@link CrossProfileDomainInfo} from linked profiles.
+ */
+ public List<CrossProfileDomainInfo> resolveIntent(@NonNull Computer computer, Intent intent,
+ String resolvedType, int userId, long flags, String pkgName,
+ boolean hasNonNegativePriorityResult,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+ return resolveIntentInternal(computer, intent, resolvedType, userId, flags, pkgName,
+ hasNonNegativePriorityResult, pkgSettingFunction);
+ }
+
+ /**
+ * Resolves intent in directly linked profiles and return list of {@link CrossProfileDomainInfo}
+ * which contains {@link ResolveInfo}. This would also recursively call profiles not directly
+ * linked.
+ *
+ * It first finds {@link CrossProfileIntentFilter} configured in current profile to find list of
+ * target user profiles that can serve current intent request. It uses corresponding strategy
+ * for each pair (source,target) user to resolve intent from target profile and returns combined
+ * results.
+ * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param userId source user for which intent request is called
+ * @param flags used for intent resolution
+ * @param pkgName the application package name this Intent is limited to.
+ * @param hasNonNegativePriorityResult signifies if current profile have any non-negative(active
+ * and valid) ResolveInfo in current profile.
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return list of {@link CrossProfileDomainInfo} from linked profiles.
+ */
+ private List<CrossProfileDomainInfo> resolveIntentInternal(@NonNull Computer computer,
+ Intent intent, String resolvedType, int userId, long flags, String pkgName,
+ boolean hasNonNegativePriorityResult,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+
+ List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+
+ List<CrossProfileIntentFilter> matchingFilters =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);
+
+ if (matchingFilters == null || matchingFilters.isEmpty()) return crossProfileDomainInfos;
+
+ UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
+ UserInfo sourceUserInfo = umInternal.getUserInfo(userId);
+
+ // Grouping the CrossProfileIntentFilters based on targerId
+ SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
+ new SparseArray<>();
+
+ for (int index = 0; index < matchingFilters.size(); index++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
+
+ if (!crossProfileIntentFiltersByUser
+ .contains(crossProfileIntentFilter.mTargetUserId)) {
+ crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
+ new ArrayList<>());
+ }
+ crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
+ .add(crossProfileIntentFilter);
+ }
+
+ /*
+ For each target user, we would call their corresponding strategy
+ {@link CrossProfileResolver} to resolve intent in corresponding user
+ */
+ for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
+
+ UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser
+ .keyAt(index));
+
+ // Choosing strategy based on source and target user
+ CrossProfileResolver crossProfileResolver =
+ chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
+
+ /*
+ If {@link CrossProfileResolver} is available for source,target pair we will call it to
+ get {@link CrossProfileDomainInfo}s from that user.
+ */
+ if (crossProfileResolver != null) {
+ List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
+ .resolveIntent(computer, intent, resolvedType, userId,
+ crossProfileIntentFiltersByUser.keyAt(index), flags, pkgName,
+ crossProfileIntentFiltersByUser.valueAt(index),
+ hasNonNegativePriorityResult, pkgSettingFunction);
+ crossProfileDomainInfos.addAll(crossProfileInfos);
+ }
+ }
+
+ return crossProfileDomainInfos;
+ }
+
+
+ /**
+ * Returns {@link CrossProfileResolver} strategy based on source and target user
+ * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
+ * @param sourceUserInfo source user
+ * @param targetUserInfo target user
+ * @return {@code CrossProfileResolver} which has value if source and target have
+ * strategy configured otherwise null.
+ */
+ @SuppressWarnings("unused")
+ private CrossProfileResolver chooseCrossProfileResolver(@NonNull Computer computer,
+ UserInfo sourceUserInfo, UserInfo targetUserInfo) {
+ return new DefaultCrossProfileResolver(computer.getComponentResolver(),
+ mUserManager, mDomainVerificationManager);
+ }
+
+ /**
+ * Returns true if we source user can reach target user for given intent. The source can
+ * directly or indirectly reach to target.
+ * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param sourceUserId source user
+ * @param targetUserId target user
+ * @return true if we source user can reach target user for given intent
+ */
+ public boolean canReachTo(@NonNull Computer computer, @NonNull Intent intent,
+ @Nullable String resolvedType, @UserIdInt int sourceUserId,
+ @UserIdInt int targetUserId) {
+ return canReachToInternal(computer, intent, resolvedType, sourceUserId, targetUserId);
+ }
+
+ /**
+ * Returns true if we source user can reach target user for given intent. The source can
+ * directly or indirectly reach to target. This will perform depth first search to check if
+ * source can reach target.
+ * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param sourceUserId source user
+ * @param targetUserId target user
+ * @return true if we source user can reach target user for given intent
+ */
+ private boolean canReachToInternal(@NonNull Computer computer, @NonNull Intent intent,
+ @Nullable String resolvedType, @UserIdInt int sourceUserId,
+ @UserIdInt int targetUserId) {
+ if (sourceUserId == targetUserId) return true;
+
+ List<CrossProfileIntentFilter> matches =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
+ if (matches != null) {
+ for (int index = 0; index < matches.size(); index++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
+ if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if any of the matching {@link CrossProfileIntentFilter} suggest we should skip the
+ * current profile based on flag {@link PackageManager#SKIP_CURRENT_PROFILE}.
+ * @param computer {@link Computer} instance used to find {@link CrossProfileIntentFilter}
+ * for user
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param sourceUserId id of initiating user space
+ * @return boolean if we should skip resolution in current/source profile.
+ */
+ public boolean shouldSkipCurrentProfile(Computer computer, Intent intent, String resolvedType,
+ int sourceUserId) {
+ List<CrossProfileIntentFilter> matches =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
+ if (matches != null) {
+ for (int matchIndex = 0; matchIndex < matches.size(); matchIndex++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matches.get(matchIndex);
+ if ((crossProfileIntentFilter.getFlags()
+ & PackageManager.SKIP_CURRENT_PROFILE) != 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Combines result from current and cross profile. This also does filtering based on domain(if
+ * required).
+ * @param computer {@link Computer} instance
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param instantAppPkgName package name if instant app is allowed
+ * @param pkgName the application package name this Intent is limited to.
+ * @param allowDynamicSplits true if dynamic splits is allowed
+ * @param matchFlags flags for intent request
+ * @param userId user id of source user
+ * @param filterCallingUid uid of calling process
+ * @param resolveForStart true if resolution occurs because an application is starting
+ * @param candidates resolveInfos from current profile
+ * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it has ResolveInfo
+ * @param areWebInstantAppsDisabled true if web instant apps are disabled
+ * @param addInstant true if instant apps are allowed
+ * @param sortResult true if caller would need to sort the results
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return QueryIntentActivitiesResult which contains resolveInfos
+ */
+ public QueryIntentActivitiesResult combineFilterAndCreateQueryActivitiesResponse(
+ Computer computer, Intent intent, String resolvedType, String instantAppPkgName,
+ String pkgName, boolean allowDynamicSplits, long matchFlags, int userId,
+ int filterCallingUid, boolean resolveForStart, List<ResolveInfo> candidates,
+ List<CrossProfileDomainInfo> crossProfileCandidates, boolean areWebInstantAppsDisabled,
+ boolean addInstant, boolean sortResult,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+
+ if (shouldSkipCurrentProfile(computer, intent, resolvedType, userId)) {
+ /*
+ if current profile is skipped return results from cross profile after filtering
+ ephemeral activities.
+ */
+ candidates = resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates);
+
+ return new QueryIntentActivitiesResult(computer.applyPostResolutionFilter(candidates,
+ instantAppPkgName, allowDynamicSplits, filterCallingUid, resolveForStart,
+ userId, intent));
+ }
+
+ if (pkgName == null && intent.hasWebURI()) {
+ // If instant apps are not allowed and there is result only from current or cross
+ // profile return it
+ if (!addInstant && ((candidates.size() <= 1 && crossProfileCandidates.isEmpty())
+ || (candidates.isEmpty() && !crossProfileCandidates.isEmpty()))) {
+ candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates));
+ return new QueryIntentActivitiesResult(computer.applyPostResolutionFilter(
+ candidates, instantAppPkgName, allowDynamicSplits, filterCallingUid,
+ resolveForStart, userId, intent));
+ }
+ /*
+ if there are multiple results from current and cross profile, combining and filtering
+ results based on domain priority.
+ */
+ candidates = filterCandidatesWithDomainPreferredActivitiesLPr(computer, intent,
+ matchFlags, candidates, crossProfileCandidates, userId,
+ areWebInstantAppsDisabled, pkgSettingFunction);
+ } else {
+ candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates));
+ }
+
+ return new QueryIntentActivitiesResult(sortResult, addInstant, candidates);
+ }
+
+ /**
+ * It filters and combines results from current and cross profile based on domain priority.
+ * @param computer {@link Computer} instance
+ * @param intent request
+ * @param matchFlags flags for intent request
+ * @param candidates resolveInfos from current profile
+ * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it have ResolveInfo
+ * @param userId user id of source user
+ * @param areWebInstantAppsDisabled true if web instant apps are disabled
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return list of ResolveInfo
+ */
+ private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Computer computer,
+ Intent intent, long matchFlags, List<ResolveInfo> candidates,
+ List<CrossProfileDomainInfo> crossProfileCandidates, int userId,
+ boolean areWebInstantAppsDisabled,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+ final boolean debug = (intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0;
+
+ if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
+ Slog.v(TAG, "Filtering results with preferred activities. Candidates count: "
+ + candidates.size());
+ }
+
+ final List<ResolveInfo> result =
+ filterCandidatesWithDomainPreferredActivitiesLPrBody(computer, intent, matchFlags,
+ candidates, crossProfileCandidates, userId, areWebInstantAppsDisabled,
+ debug, pkgSettingFunction);
+
+ if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
+ Slog.v(TAG, "Filtered results with preferred activities. New candidates count: "
+ + result.size());
+ for (ResolveInfo info : result) {
+ Slog.v(TAG, " + " + info.activityInfo);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Filters candidates satisfying domain criteria.
+ * @param computer {@link Computer} instance
+ * @param intent request
+ * @param matchFlags flags for intent request
+ * @param candidates resolveInfos from current profile
+ * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it have ResolveInfo
+ * @param userId user id of source user
+ * @param areWebInstantAppsDisabled true if web instant apps are disabled
+ * @param debug true if resolution logs needed to be printed
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return list of resolve infos
+ */
+ private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPrBody(
+ Computer computer, Intent intent, long matchFlags, List<ResolveInfo> candidates,
+ List<CrossProfileDomainInfo> crossProfileCandidates, int userId,
+ boolean areWebInstantAppsDisabled, boolean debug,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+ final ArrayList<ResolveInfo> result = new ArrayList<>();
+ final ArrayList<ResolveInfo> matchAllList = new ArrayList<>();
+ final ArrayList<ResolveInfo> undefinedList = new ArrayList<>();
+
+ // Blocking instant apps is usually done in applyPostResolutionFilter, but since
+ // domain verification can resolve to a single result, which can be an instant app,
+ // it will then be filtered to an empty list in that method. Instead, do blocking
+ // here so that instant apps can be ignored for approval filtering and a lower
+ // priority result chosen instead.
+ final boolean blockInstant = intent.isWebIntent() && areWebInstantAppsDisabled;
+
+ final int count = candidates.size();
+ // First, try to use approved apps.
+ for (int n = 0; n < count; n++) {
+ ResolveInfo info = candidates.get(n);
+ if (blockInstant && (info.isInstantAppAvailable
+ || computer.isInstantAppInternal(info.activityInfo.packageName, userId,
+ Process.SYSTEM_UID))) {
+ continue;
+ }
+
+ // Add to the special match all list (Browser use case)
+ if (info.handleAllWebDataURI) {
+ matchAllList.add(info);
+ } else {
+ undefinedList.add(info);
+ }
+ }
+
+ // We'll want to include browser possibilities in a few cases
+ boolean includeBrowser = false;
+
+ /**
+ * Grouping CrossProfileDomainInfo based on target user
+ */
+ SparseArray<List<CrossProfileDomainInfo>> categorizeResolveInfoByTargetUser =
+ new SparseArray<>();
+ if (crossProfileCandidates != null && !crossProfileCandidates.isEmpty()) {
+ for (int index = 0; index < crossProfileCandidates.size(); index++) {
+ CrossProfileDomainInfo crossProfileDomainInfo = crossProfileCandidates.get(index);
+ if (!categorizeResolveInfoByTargetUser
+ .contains(crossProfileDomainInfo.mTargetUserId)) {
+ categorizeResolveInfoByTargetUser.put(crossProfileDomainInfo.mTargetUserId,
+ new ArrayList<>());
+ }
+ categorizeResolveInfoByTargetUser.get(crossProfileDomainInfo.mTargetUserId)
+ .add(crossProfileDomainInfo);
+ }
+ }
+
+ if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) {
+ result.addAll(undefinedList);
+
+ // calling cross profile strategy to filter corresponding results
+ result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer,
+ intent, matchFlags, categorizeResolveInfoByTargetUser, userId,
+ DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE));
+ includeBrowser = true;
+ } else {
+ Pair<List<ResolveInfo>, Integer> infosAndLevel = mDomainVerificationManager
+ .filterToApprovedApp(intent, undefinedList, userId, pkgSettingFunction);
+ List<ResolveInfo> approvedInfos = infosAndLevel.first;
+ Integer highestApproval = infosAndLevel.second;
+
+ // If no apps are approved for the domain, resolve only to browsers
+ if (approvedInfos.isEmpty()) {
+ includeBrowser = true;
+ // calling cross profile strategy to filter corresponding results
+ result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer,
+ intent, matchFlags, categorizeResolveInfoByTargetUser, userId,
+ DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE));
+ } else {
+ result.addAll(approvedInfos);
+
+ // If the other profile has an app that's higher approval, add it
+ // calling cross profile strategy to filter corresponding results
+ result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer,
+ intent, matchFlags, categorizeResolveInfoByTargetUser, userId,
+ highestApproval));
+ }
+ }
+
+ if (includeBrowser) {
+ // Also add browsers (all of them or only the default one)
+ if (DEBUG_DOMAIN_VERIFICATION) {
+ Slog.v(TAG, " ...including browsers in candidate set");
+ }
+ if ((matchFlags & MATCH_ALL) != 0) {
+ result.addAll(matchAllList);
+ } else {
+ // Browser/generic handling case. If there's a default browser, go straight
+ // to that (but only if there is no other higher-priority match).
+ final String defaultBrowserPackageName = mDefaultAppProvider.getDefaultBrowser(
+ userId);
+ int maxMatchPrio = 0;
+ ResolveInfo defaultBrowserMatch = null;
+ final int numCandidates = matchAllList.size();
+ for (int n = 0; n < numCandidates; n++) {
+ ResolveInfo info = matchAllList.get(n);
+ // track the highest overall match priority...
+ if (info.priority > maxMatchPrio) {
+ maxMatchPrio = info.priority;
+ }
+ // ...and the highest-priority default browser match
+ if (info.activityInfo.packageName.equals(defaultBrowserPackageName)) {
+ if (defaultBrowserMatch == null
+ || (defaultBrowserMatch.priority < info.priority)) {
+ if (debug) {
+ Slog.v(TAG, "Considering default browser match " + info);
+ }
+ defaultBrowserMatch = info;
+ }
+ }
+ }
+ if (defaultBrowserMatch != null
+ && defaultBrowserMatch.priority >= maxMatchPrio
+ && !TextUtils.isEmpty(defaultBrowserPackageName)) {
+ if (debug) {
+ Slog.v(TAG, "Default browser match " + defaultBrowserMatch);
+ }
+ result.add(defaultBrowserMatch);
+ } else {
+ result.addAll(matchAllList);
+ }
+ }
+
+ // If there is nothing selected, add all candidates
+ if (result.size() == 0) {
+ result.addAll(candidates);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Filter cross profile results by calling their respective strategy
+ * @param computer {@link Computer} instance
+ * @param intent request
+ * @param flags for intent request
+ * @param categorizeResolveInfoByTargetUser group of targetuser and its corresponding
+ * CrossProfileDomainInfos
+ * @param sourceUserId user id for intent
+ * @param highestApprovalLevel domain approval level
+ * @return list of ResolveInfos
+ */
+ private List<ResolveInfo> filterCrossProfileCandidatesWithDomainPreferredActivities(
+ Computer computer, Intent intent, long flags, SparseArray<List<CrossProfileDomainInfo>>
+ categorizeResolveInfoByTargetUser, int sourceUserId, int highestApprovalLevel) {
+
+ List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+ UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
+ UserInfo sourceUserInfo = umInternal.getUserInfo(sourceUserId);
+
+ for (int index = 0; index < categorizeResolveInfoByTargetUser.size(); index++) {
+
+ // if resolve info does not target user or has default value, add results as they are.
+ if (categorizeResolveInfoByTargetUser.keyAt(index) == -2) {
+ crossProfileDomainInfos.addAll(categorizeResolveInfoByTargetUser.valueAt(index));
+ } else {
+ // finding cross profile strategy based on source and target user
+ CrossProfileResolver crossProfileIntentResolver =
+ chooseCrossProfileResolver(computer, sourceUserInfo, umInternal
+ .getUserInfo(categorizeResolveInfoByTargetUser.keyAt(index)));
+ // if strategy is available call it and add its filtered results
+ if (crossProfileIntentResolver != null) {
+ crossProfileDomainInfos.addAll(crossProfileIntentResolver
+ .filterResolveInfoWithDomainPreferredActivity(intent,
+ categorizeResolveInfoByTargetUser.valueAt(index),
+ flags, sourceUserId, categorizeResolveInfoByTargetUser
+ .keyAt(index), highestApprovalLevel));
+ } else {
+ // if strategy is not available call it, add the results
+ crossProfileDomainInfos.addAll(categorizeResolveInfoByTargetUser
+ .valueAt(index));
+ }
+ }
+ }
+ return resolveInfoFromCrossProfileDomainInfo(crossProfileDomainInfos);
+ }
+
+ /**
+ * Extract ResolveInfo from CrossProfileDomainInfo
+ * @param crossProfileDomainInfos cross profile results
+ * @return list of ResolveInfo
+ */
+ private List<ResolveInfo> resolveInfoFromCrossProfileDomainInfo(List<CrossProfileDomainInfo>
+ crossProfileDomainInfos) {
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+
+ for (int infoIndex = 0; infoIndex < crossProfileDomainInfos.size(); infoIndex++) {
+ resolveInfoList.add(crossProfileDomainInfos.get(infoIndex).mResolveInfo);
+ }
+
+ return resolveInfoList;
+ }
+}
diff --git a/services/core/java/com/android/server/pm/CrossProfileResolver.java b/services/core/java/com/android/server/pm/CrossProfileResolver.java
new file mode 100644
index 0000000..a8da818
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CrossProfileResolver.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.resolution.ComponentResolverApi;
+
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Abstract Class act as base class for Cross Profile strategy.
+ * This will be used by {@link CrossProfileIntentResolverEngine} to resolve intent across profile.
+ */
+public abstract class CrossProfileResolver {
+
+ protected ComponentResolverApi mComponentResolver;
+ protected UserManagerService mUserManager;
+
+ public CrossProfileResolver(ComponentResolverApi componentResolver,
+ UserManagerService userManager) {
+ mComponentResolver = componentResolver;
+ mUserManager = userManager;
+ }
+
+ /**
+ * This method would be overridden by concrete implementation. This method should define how to
+ * resolve given intent request in target profile.
+ * @param computer ComputerEngine instance that would be needed by ComponentResolverApi
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param userId source/initiating user
+ * @param targetUserId target user id
+ * @param flags of intent request
+ * @param pkgName package name if defined.
+ * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user,
+ * targeting the targetUserId
+ * @param hasNonNegativePriorityResult if source have any non-negative(active and valid)
+ * resolveInfo in their profile.
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return list of {@link CrossProfileDomainInfo}
+ */
+ public abstract List<CrossProfileDomainInfo> resolveIntent(Computer computer, Intent intent,
+ String resolvedType, int userId, int targetUserId, long flags,
+ String pkgName, List<CrossProfileIntentFilter> matchingFilters,
+ boolean hasNonNegativePriorityResult,
+ Function<String, PackageStateInternal> pkgSettingFunction);
+
+ /**
+ * Filters the CrossProfileDomainInfos, the filtering technique would be defined by concrete
+ * implementation class
+ * @param intent request
+ * @param crossProfileDomainInfos resolved in target user
+ * @param flags for intent resolution
+ * @param sourceUserId source user
+ * @param targetUserId target user
+ * @param highestApprovalLevel highest level of domain approval
+ * @return filtered list of {@link CrossProfileDomainInfo}
+ */
+ public abstract List<CrossProfileDomainInfo> filterResolveInfoWithDomainPreferredActivity(
+ Intent intent, List<CrossProfileDomainInfo> crossProfileDomainInfos, long flags,
+ int sourceUserId, int targetUserId, int highestApprovalLevel);
+
+ /**
+ * Checks if mentioned user is enabled
+ * @param userId of requested user
+ * @return true if user is enabled
+ */
+ protected final boolean isUserEnabled(int userId) {
+ final long callingId = Binder.clearCallingIdentity();
+ try {
+ UserInfo userInfo = mUserManager.getUserInfo(userId);
+ return userInfo != null && userInfo.isEnabled();
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
+ /**
+ * Filters out {@link CrossProfileDomainInfo} if they are not for any user apart from system
+ * user. If mentioned user is system user, then returns all responses.
+ * @param crossProfileDomainInfos result from resolution
+ * @param userId source user id
+ * @return filtered list of {@link CrossProfileDomainInfo}
+ */
+ protected final List<CrossProfileDomainInfo> filterIfNotSystemUser(
+ List<CrossProfileDomainInfo> crossProfileDomainInfos, int userId) {
+ if (userId == UserHandle.USER_SYSTEM) {
+ return crossProfileDomainInfos;
+ }
+
+ for (int i = CollectionUtils.size(crossProfileDomainInfos) - 1; i >= 0; i--) {
+ ResolveInfo info = crossProfileDomainInfos.get(i).mResolveInfo;
+ if ((info.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) {
+ crossProfileDomainInfos.remove(i);
+ }
+ }
+ return crossProfileDomainInfos;
+ }
+
+ /**
+ * Returns user info of parent profile is applicable
+ * @param userId requested user
+ * @return parent's user info, null if parent is not present
+ */
+ protected final UserInfo getProfileParent(int userId) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return mUserManager.getProfileParent(userId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java b/services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java
new file mode 100644
index 0000000..90d89c6
--- /dev/null
+++ b/services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.app.IntentForwarderActivity;
+import com.android.internal.util.CollectionUtils;
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.resolution.ComponentResolverApi;
+import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Cross profile resolver used as default strategy. Primary known use-case for this resolver is
+ * work/managed profile .
+ */
+public final class DefaultCrossProfileResolver extends CrossProfileResolver {
+
+ private final DomainVerificationManagerInternal mDomainVerificationManager;
+
+
+ public DefaultCrossProfileResolver(ComponentResolverApi componentResolver,
+ UserManagerService userManager,
+ DomainVerificationManagerInternal domainVerificationManager) {
+ super(componentResolver, userManager);
+ mDomainVerificationManager = domainVerificationManager;
+ }
+
+ /**
+ * This is Default resolution strategy primarily used by Work Profile.
+ * First, it checks if we have to skip source profile and just resolve in target profile. If
+ * yes, then it will return result from target profile.
+ * Secondly, it find specific resolve infos in target profile
+ * Thirdly, if it is web intent it finds if parent can also resolve it. The results of this
+ * stage gets higher priority as compared to second stage.
+ *
+ * @param computer ComputerEngine instance that would be needed by ComponentResolverApi
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param userId source/initiating user
+ * @param targetUserId target user id
+ * @param flags of intent request
+ * @param pkgName the application package name this Intent is limited to.
+ * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user,
+ * targeting the targetUserId
+ * @param hasNonNegativePriorityResult if source have any non-negative(active and valid)
+ * resolveInfo in their profile.
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return list of {@link CrossProfileDomainInfo}
+ */
+ @Override
+ public List<CrossProfileDomainInfo> resolveIntent(Computer computer, Intent intent,
+ String resolvedType, int userId, int targetUserId,
+ long flags, String pkgName, List<CrossProfileIntentFilter> matchingFilters,
+ boolean hasNonNegativePriorityResult,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+
+ List<CrossProfileDomainInfo> xpResult = new ArrayList<>();
+ if (pkgName != null) return xpResult;
+ CrossProfileDomainInfo skipProfileInfo = querySkipCurrentProfileIntents(computer,
+ matchingFilters, intent, resolvedType, flags, userId, pkgSettingFunction);
+
+ if (skipProfileInfo != null) {
+ xpResult.add(skipProfileInfo);
+ return filterIfNotSystemUser(xpResult, userId);
+ }
+
+ CrossProfileDomainInfo specificXpInfo = queryCrossProfileIntents(computer,
+ matchingFilters, intent, resolvedType, flags, userId,
+ hasNonNegativePriorityResult, pkgSettingFunction);
+
+ if (intent.hasWebURI()) {
+ CrossProfileDomainInfo generalXpInfo = null;
+ final UserInfo parent = getProfileParent(userId);
+ if (parent != null) {
+ generalXpInfo = computer.getCrossProfileDomainPreferredLpr(intent, resolvedType,
+ flags, userId, parent.id);
+ }
+ CrossProfileDomainInfo prioritizedXpInfo =
+ generalXpInfo != null ? generalXpInfo : specificXpInfo;
+ if (prioritizedXpInfo != null) {
+ xpResult.add(prioritizedXpInfo);
+ }
+ } else if (specificXpInfo != null) {
+ xpResult.add(specificXpInfo);
+ }
+
+ return xpResult;
+ }
+
+ /**
+ * Filters out CrossProfileDomainInfo if it does not have higher approval level as compared to
+ * given approval level
+ * @param intent request
+ * @param crossProfileDomainInfos resolved in target user
+ * @param flags for intent resolution
+ * @param sourceUserId source user
+ * @param targetUserId target user
+ * @param highestApprovalLevel highest level of domain approval
+ * @return filtered list of CrossProfileDomainInfo
+ */
+ @Override
+ public List<CrossProfileDomainInfo> filterResolveInfoWithDomainPreferredActivity(
+ Intent intent, List<CrossProfileDomainInfo> crossProfileDomainInfos, long flags,
+ int sourceUserId, int targetUserId, int highestApprovalLevel) {
+
+ List<CrossProfileDomainInfo> filteredCrossProfileDomainInfos = new ArrayList<>();
+
+ if (crossProfileDomainInfos != null && !crossProfileDomainInfos.isEmpty()) {
+ for (int index = 0; index < crossProfileDomainInfos.size(); index++) {
+ CrossProfileDomainInfo crossProfileDomainInfo = crossProfileDomainInfos.get(index);
+ if (crossProfileDomainInfo.mHighestApprovalLevel > highestApprovalLevel) {
+ filteredCrossProfileDomainInfos.add(crossProfileDomainInfo);
+ }
+ }
+ }
+
+ return filteredCrossProfileDomainInfos;
+ }
+
+ /**
+ * If current/source profile needs to be skipped, returns CrossProfileDomainInfo from target
+ * profile. If any of the matchingFilters have flag {@link PackageManager#SKIP_CURRENT_PROFILE}
+ * set that would signify that current profile needs to be skipped.
+ * @param computer ComputerEngine instance that would be needed by ComponentResolverApi
+ * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user,
+ * targeting the targetUserId
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param flags for intent resolution
+ * @param sourceUserId source user
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return CrossProfileDomainInfo if current profile needs to be skipped, else null
+ */
+ @Nullable
+ private CrossProfileDomainInfo querySkipCurrentProfileIntents(Computer computer,
+ List<CrossProfileIntentFilter> matchingFilters, Intent intent, String resolvedType,
+ long flags, int sourceUserId,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+ if (matchingFilters != null) {
+ int size = matchingFilters.size();
+ for (int i = 0; i < size; i++) {
+ CrossProfileIntentFilter filter = matchingFilters.get(i);
+ if ((filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0) {
+ // Checking if there are activities in the target user that can handle the
+ // intent.
+ CrossProfileDomainInfo info = createForwardingResolveInfo(computer, filter,
+ intent, resolvedType, flags, sourceUserId, pkgSettingFunction);
+ if (info != null) {
+ return info;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Resolves and returns CrossProfileDomainInfo(ForwardingResolveInfo) from target profile if
+ * current profile should be skipped when there is no result or if target profile should not
+ * be skipped.
+ *
+ * @param computer ComputerEngine instance that would be needed by ComponentResolverApi
+ * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user,
+ * targeting the targetUserId
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param flags for intent resolution
+ * @param sourceUserId source user
+ * @param matchInCurrentProfile true if current/source profile have some non-negative
+ * resolveInfo
+ * @param pkgSettingFunction function to find PackageStateInternal for given package
+ * @return CrossProfileDomainInfo returns forwarding intent resolver in CrossProfileDomainInfo.
+ * It returns null if there are no matching filters or no valid/active activity available
+ */
+ @Nullable
+ private CrossProfileDomainInfo queryCrossProfileIntents(Computer computer,
+ List<CrossProfileIntentFilter> matchingFilters, Intent intent, String resolvedType,
+ long flags, int sourceUserId, boolean matchInCurrentProfile,
+ Function<String, PackageStateInternal> pkgSettingFunction) {
+ if (matchingFilters == null) {
+ return null;
+ }
+ // Two {@link CrossProfileIntentFilter}s can have the same targetUserId and
+ // match the same intent. For performance reasons, it is better not to
+ // run queryIntent twice for the same userId
+ SparseBooleanArray alreadyTriedUserIds = new SparseBooleanArray();
+
+ CrossProfileDomainInfo resultInfo = null;
+
+ int size = matchingFilters.size();
+ for (int i = 0; i < size; i++) {
+ CrossProfileIntentFilter filter = matchingFilters.get(i);
+ int targetUserId = filter.getTargetUserId();
+ boolean skipCurrentProfile =
+ (filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0;
+ boolean skipCurrentProfileIfNoMatchFound =
+ (filter.getFlags() & PackageManager.ONLY_IF_NO_MATCH_FOUND) != 0;
+ if (!skipCurrentProfile && !alreadyTriedUserIds.get(targetUserId)
+ && (!skipCurrentProfileIfNoMatchFound || !matchInCurrentProfile)) {
+ // Checking if there are activities in the target user that can handle the
+ // intent.
+ CrossProfileDomainInfo info = createForwardingResolveInfo(computer, filter, intent,
+ resolvedType, flags, sourceUserId, pkgSettingFunction);
+ if (info != null) {
+ resultInfo = info;
+ break;
+ }
+ alreadyTriedUserIds.put(targetUserId, true);
+ }
+ }
+
+ if (resultInfo == null) {
+ return null;
+ }
+
+ ResolveInfo forwardingResolveInfo = resultInfo.mResolveInfo;
+ if (!isUserEnabled(forwardingResolveInfo.targetUserId)) {
+ return null;
+ }
+
+ List<CrossProfileDomainInfo> filteredResult =
+ filterIfNotSystemUser(Collections.singletonList(resultInfo), sourceUserId);
+ if (filteredResult.isEmpty()) {
+ return null;
+ }
+
+ return resultInfo;
+ }
+
+ /**
+ * Creates a Forwarding Resolve Info, used when we have to signify that target profile's
+ * resolveInfo should be considered without providing list of resolve infos.
+ * @param computer ComputerEngine instance that would be needed by ComponentResolverApi
+ * @param filter {@link CrossProfileIntentFilter} configured for source user,
+ * targeting the targetUserId
+ * @param intent request
+ * @param resolvedType the MIME data type of intent request
+ * @param flags for intent resolution
+ * @param sourceUserId source user
+ * @return CrossProfileDomainInfo whose ResolveInfo is forwarding. It would be resolved by
+ * {@link IntentForwarderActivity}. It returns null if there are no valid/active activities
+ */
+ @Nullable
+ protected CrossProfileDomainInfo createForwardingResolveInfo(Computer computer,
+ @NonNull CrossProfileIntentFilter filter, @NonNull Intent intent,
+ @Nullable String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
+ int sourceUserId, @NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
+ int targetUserId = filter.getTargetUserId();
+ if (!isUserEnabled(targetUserId)) {
+ return null;
+ }
+
+ List<ResolveInfo> resultTargetUser = mComponentResolver.queryActivities(computer, intent,
+ resolvedType, flags, targetUserId);
+ if (CollectionUtils.isEmpty(resultTargetUser)) {
+ return null;
+ }
+
+ ResolveInfo forwardingInfo = null;
+ for (int i = resultTargetUser.size() - 1; i >= 0; i--) {
+ ResolveInfo targetUserResolveInfo = resultTargetUser.get(i);
+ if ((targetUserResolveInfo.activityInfo.applicationInfo.flags
+ & ApplicationInfo.FLAG_SUSPENDED) == 0) {
+ forwardingInfo = computer.createForwardingResolveInfoUnchecked(filter, sourceUserId,
+ targetUserId);
+ break;
+ }
+ }
+
+ if (forwardingInfo == null) {
+ // If all the matches in the target profile are suspended, return null.
+ return null;
+ }
+
+ int highestApprovalLevel = DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE;
+
+ int size = resultTargetUser.size();
+ for (int i = 0; i < size; i++) {
+ ResolveInfo riTargetUser = resultTargetUser.get(i);
+ if (riTargetUser.handleAllWebDataURI) {
+ continue;
+ }
+ String packageName = riTargetUser.activityInfo.packageName;
+ PackageStateInternal ps = pkgSettingFunction.apply(packageName);
+ if (ps == null) {
+ continue;
+ }
+ highestApprovalLevel = Math.max(highestApprovalLevel, mDomainVerificationManager
+ .approvalLevelForDomain(ps, intent, flags, targetUserId));
+ }
+
+ return new CrossProfileDomainInfo(forwardingInfo, highestApprovalLevel, targetUserId);
+ }
+}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 690dd10..4111446 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -49,6 +49,31 @@
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
+ /**
+ * Used by InputMethodManagerService to notify the IME status.
+ *
+ * @param displayId The display to which the IME is bound to.
+ * @param token The IME token.
+ * @param vis Bit flags about the IME visibility.
+ * (e.g. {@link android.inputmethodservice.InputMethodService#IME_ACTIVE})
+ * @param backDisposition Bit flags about the IME back disposition.
+ * (e.g. {@link android.inputmethodservice.InputMethodService#BACK_DISPOSITION_DEFAULT})
+ * @param showImeSwitcher {@code true} when the IME switcher button should be shown.
+ */
+ void setImeWindowStatus(int displayId, IBinder token, int vis,
+ int backDisposition, boolean showImeSwitcher);
+
+ /**
+ * See {@link android.app.StatusBarManager#setIcon(String, int, int, String)}.
+ */
+ void setIcon(String slot, String iconPackage, int iconId, int iconLevel,
+ String contentDescription);
+
+ /**
+ * See {@link android.app.StatusBarManager#setIconVisibility(String, boolean)}.
+ */
+ void setIconVisibility(String slot, boolean visibility);
+
void showChargingAnimation(int batteryLevel);
/**
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 653b51a9..7ccf85f 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -484,6 +484,25 @@
}
@Override
+ public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
+ boolean showImeSwitcher) {
+ StatusBarManagerService.this.setImeWindowStatus(displayId, token, vis, backDisposition,
+ showImeSwitcher);
+ }
+
+ @Override
+ public void setIcon(String slot, String iconPackage, int iconId, int iconLevel,
+ String contentDescription) {
+ StatusBarManagerService.this.setIcon(slot, iconPackage, iconId, iconLevel,
+ contentDescription);
+ }
+
+ @Override
+ public void setIconVisibility(String slot, boolean visibility) {
+ StatusBarManagerService.this.setIconVisibility(slot, visibility);
+ }
+
+ @Override
public void showChargingAnimation(int batteryLevel) {
if (mBar != null) {
try {
diff --git a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
index 4803011..b96af89 100644
--- a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
+++ b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
@@ -172,9 +172,14 @@
mContext.enforceCallingPermission(
android.Manifest.permission.SET_TIME, "clear latest network time");
- mTime.clearCachedTimeResult();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mTime.clearCachedTimeResult();
- mLocalLog.log("clearTimeForTests");
+ mLocalLog.log("clearTimeForTests");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
/**
@@ -188,15 +193,19 @@
mContext.enforceCallingPermission(
android.Manifest.permission.SET_TIME, "force network time refresh");
- boolean success = mTime.forceRefresh();
- mLocalLog.log("forceRefreshForTests: success=" + success);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ boolean success = mTime.forceRefresh();
+ mLocalLog.log("forceRefreshForTests: success=" + success);
- if (success) {
- makeNetworkTimeSuggestion(mTime.getCachedTimeResult(),
- "Origin: NetworkTimeUpdateService: forceRefreshForTests");
+ if (success) {
+ makeNetworkTimeSuggestion(mTime.getCachedTimeResult(),
+ "Origin: NetworkTimeUpdateService: forceRefreshForTests");
+ }
+ return success;
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
-
- return success;
}
/**
@@ -207,8 +216,13 @@
mContext.enforceCallingPermission(
android.Manifest.permission.SET_TIME, "set NTP server config for tests");
- mLocalLog.log("Setting server config for tests: ntpConnectionInfo=" + ntpConfig);
- mTime.setServerConfigForTests(ntpConfig);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mLocalLog.log("Setting server config for tests: ntpConnectionInfo=" + ntpConfig);
+ mTime.setServerConfigForTests(ntpConfig);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
private void onPollNetworkTime(int event) {
diff --git a/services/core/java/com/android/server/utils/EventLogger.java b/services/core/java/com/android/server/utils/EventLogger.java
index 45b4ff8..aa8b94a7 100644
--- a/services/core/java/com/android/server/utils/EventLogger.java
+++ b/services/core/java/com/android/server/utils/EventLogger.java
@@ -23,8 +23,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.SimpleDateFormat;
+import java.util.ArrayDeque;
import java.util.Date;
-import java.util.LinkedList;
import java.util.Locale;
/**
@@ -36,7 +36,7 @@
private final String mTag;
/** Stores the events using a ring buffer. */
- private final LinkedList<Event> mEvents;
+ private final ArrayDeque<Event> mEvents;
/**
* The maximum number of events to keep in {@code mEvents}.
@@ -52,16 +52,18 @@
* @param tag the string displayed before the recorded log
*/
public EventLogger(int size, String tag) {
- mEvents = new LinkedList<Event>();
+ mEvents = new ArrayDeque<>(size);
mMemSize = size;
mTag = tag;
}
- public synchronized void log(Event evt) {
+ /** Enqueues {@code event} to be logged. */
+ public synchronized void log(Event event) {
if (mEvents.size() >= mMemSize) {
- mEvents.removeFirst();
+ mEvents.removeLast();
}
- mEvents.add(evt);
+
+ mEvents.addFirst(event);
}
/**
@@ -85,8 +87,10 @@
log(event.printLog(logType, tag));
}
+ /** Dumps events using {@link PrintWriter} */
public synchronized void dump(PrintWriter pw) {
pw.println("Events log: " + mTag);
+
for (Event evt : mEvents) {
pw.println(evt.toString());
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 78b657b..2232aa1 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4066,7 +4066,11 @@
// to the restarted activity.
nowVisible = mVisibleRequested;
}
- mTransitionController.requestCloseTransitionIfNeeded(this);
+ // upgrade transition trigger to task if this is the last activity since it means we are
+ // closing the task.
+ final WindowContainer trigger = remove && task != null && task.getChildCount() == 1
+ ? task : this;
+ mTransitionController.requestCloseTransitionIfNeeded(trigger);
cleanUp(true /* cleanServices */, true /* setState */);
if (remove) {
if (mStartingData != null && mVisible && task != null) {
@@ -7636,6 +7640,31 @@
ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
}
+ /**
+ * Returns the requested {@link Configuration.Orientation} for the current activity.
+ *
+ * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the
+ * requested orientation for the activity below which is the first activity with an explicit
+ * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link
+ * SCREEN_ORIENTATION_BEHIND}.
+ */
+ @Configuration.Orientation
+ @Override
+ int getRequestedConfigurationOrientation(boolean forDisplay) {
+ if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) {
+ // We use Task here because we want to be consistent with what happens in
+ // multi-window mode where other tasks orientations are ignored.
+ final ActivityRecord belowCandidate = task.getActivity(
+ a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing
+ && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this,
+ false /* includeBoundary */, true /* traverseTopToBottom */);
+ if (belowCandidate != null) {
+ return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
+ }
+ }
+ return super.getRequestedConfigurationOrientation(forDisplay);
+ }
+
@Override
void onCancelFixedRotationTransform(int originalDisplayRotation) {
if (this != mDisplayContent.getLastOrientationSource()) {
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
new file mode 100644
index 0000000..a6f8557
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.function.Consumer;
+
+/**
+ * Class that registers callbacks with the {@link DeviceStateManager} and
+ * responds to fold state changes by forwarding such events to a delegate.
+ */
+final class DeviceStateController {
+ private final DeviceStateManager mDeviceStateManager;
+ private final Context mContext;
+
+ private FoldStateListener mDeviceStateListener;
+
+ public enum FoldState {
+ UNKNOWN, OPEN, FOLDED, HALF_FOLDED
+ }
+
+ DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) {
+ mContext = context;
+ mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class);
+ if (mDeviceStateManager != null) {
+ mDeviceStateListener = new FoldStateListener(mContext, delegate);
+ mDeviceStateManager
+ .registerCallback(new HandlerExecutor(handler),
+ mDeviceStateListener);
+ }
+ }
+
+ void unregisterFromDeviceStateManager() {
+ if (mDeviceStateListener != null) {
+ mDeviceStateManager.unregisterCallback(mDeviceStateListener);
+ }
+ }
+
+ /**
+ * A listener for half-fold device state events that dispatches state changes to a delegate.
+ */
+ static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+
+ private final int[] mHalfFoldedDeviceStates;
+ private final int[] mFoldedDeviceStates;
+
+ @Nullable
+ private FoldState mLastResult;
+ private final Consumer<FoldState> mDelegate;
+
+ FoldStateListener(Context context, Consumer<FoldState> delegate) {
+ mFoldedDeviceStates = context.getResources().getIntArray(
+ com.android.internal.R.array.config_foldedDeviceStates);
+ mHalfFoldedDeviceStates = context.getResources().getIntArray(
+ com.android.internal.R.array.config_halfFoldedDeviceStates);
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state);
+ FoldState result;
+ if (halfFolded) {
+ result = FoldState.HALF_FOLDED;
+ } else {
+ final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
+ result = folded ? FoldState.FOLDED : FoldState.OPEN;
+ }
+ if (mLastResult == null || !mLastResult.equals(result)) {
+ mLastResult = result;
+ mDelegate.accept(result);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 6c5222b..3c847ce 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -570,6 +570,7 @@
final FixedRotationTransitionListener mFixedRotationTransitionListener =
new FixedRotationTransitionListener();
+ private final DeviceStateController mDeviceStateController;
private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
final RemoteDisplayChangeController mRemoteDisplayChangeController;
@@ -1125,6 +1126,13 @@
mDisplayPolicy = new DisplayPolicy(mWmService, this);
mDisplayRotation = new DisplayRotation(mWmService, this);
+
+ mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
+ newFoldState -> {
+ mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
+ mDisplayRotation.foldStateChanged(newFoldState);
+ });
+
mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat(
R.dimen.config_closeToSquareDisplayMaxAspectRatio);
if (isDefaultDisplay) {
@@ -3253,7 +3261,7 @@
mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
handleAnimatingStoppedAndTransition();
mWmService.stopFreezingDisplayLocked();
- mDisplaySwitchTransitionLauncher.destroy();
+ mDeviceStateController.unregisterFromDeviceStateManager();
super.removeImmediately();
if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this);
mPointerEventDispatcher.dispose();
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 97609a7..a8d13c5 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -40,6 +40,7 @@
import android.annotation.AnimRes;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -108,6 +109,8 @@
private OrientationListener mOrientationListener;
private StatusBarManagerInternal mStatusBarManagerInternal;
private SettingsObserver mSettingsObserver;
+ @Nullable
+ private FoldController mFoldController;
@ScreenOrientation
private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -238,6 +241,10 @@
mOrientationListener.setCurrentRotation(mRotation);
mSettingsObserver = new SettingsObserver(uiHandler);
mSettingsObserver.observe();
+ if (mSupportAutoRotation && mContext.getResources().getBoolean(
+ R.bool.config_windowManagerHalfFoldAutoRotateOverride)) {
+ mFoldController = new FoldController();
+ }
}
}
@@ -436,7 +443,17 @@
final int oldRotation = mRotation;
final int lastOrientation = mLastOrientation;
- final int rotation = rotationForOrientation(lastOrientation, oldRotation);
+ int rotation = rotationForOrientation(lastOrientation, oldRotation);
+ // Use the saved rotation for tabletop mode, if set.
+ if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) {
+ int prevRotation = rotation;
+ rotation = mFoldController.revertOverriddenRotation();
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Reverting orientation. Rotating to %s from %s rather than %s.",
+ Surface.rotationToString(rotation),
+ Surface.rotationToString(oldRotation),
+ Surface.rotationToString(prevRotation));
+ }
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and "
+ "oldRotation=%s (%d)",
@@ -1138,7 +1155,8 @@
// If we don't support auto-rotation then bail out here and ignore
// the sensor and any rotation lock settings.
preferredRotation = -1;
- } else if ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
+ } else if (((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
+ || isTabletopAutoRotateOverrideEnabled())
&& (orientation == ActivityInfo.SCREEN_ORIENTATION_USER
|| orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|| orientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
@@ -1292,10 +1310,17 @@
return false;
}
+ private boolean isTabletopAutoRotateOverrideEnabled() {
+ return mFoldController != null && mFoldController.overrideFrozenRotation();
+ }
+
private boolean isRotationChoicePossible(int orientation) {
// Rotation choice is only shown when the user is in locked mode.
if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false;
+ // Don't show rotation choice if we are in tabletop or book modes.
+ if (isTabletopAutoRotateOverrideEnabled()) return false;
+
// We should only enable rotation choice if the rotation isn't forced by the lid, dock,
// demo, hdmi, vr, etc mode.
@@ -1496,6 +1521,74 @@
proto.end(token);
}
+ /**
+ * Called by the DeviceStateManager callback when the device state changes.
+ */
+ void foldStateChanged(DeviceStateController.FoldState foldState) {
+ if (mFoldController != null) {
+ synchronized (mLock) {
+ mFoldController.foldStateChanged(foldState);
+ }
+ }
+ }
+
+ private class FoldController {
+ @Surface.Rotation
+ private int mHalfFoldSavedRotation = -1; // No saved rotation
+ private DeviceStateController.FoldState mFoldState =
+ DeviceStateController.FoldState.UNKNOWN;
+
+ boolean overrideFrozenRotation() {
+ return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
+ }
+
+ boolean shouldRevertOverriddenRotation() {
+ return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+ && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
+ && mUserRotationMode
+ == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked.
+ }
+
+ int revertOverriddenRotation() {
+ int savedRotation = mHalfFoldSavedRotation;
+ mHalfFoldSavedRotation = -1;
+ return savedRotation;
+ }
+
+ void foldStateChanged(DeviceStateController.FoldState newState) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "foldStateChanged: displayId %d, halfFoldStateChanged %s, "
+ + "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, "
+ + "mLastOrientation: %d, mRotation: %d",
+ mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation,
+ mUserRotation, mLastSensorRotation, mLastOrientation, mRotation);
+ if (mFoldState == DeviceStateController.FoldState.UNKNOWN) {
+ mFoldState = newState;
+ return;
+ }
+ if (newState == DeviceStateController.FoldState.HALF_FOLDED
+ && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) {
+ // The device has transitioned to HALF_FOLDED state: save the current rotation and
+ // update the device rotation.
+ mHalfFoldSavedRotation = mRotation;
+ mFoldState = newState;
+ // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will
+ // return true, so rotation is unlocked.
+ mService.updateRotation(false /* alwaysSendConfiguration */,
+ false /* forceRelayout */);
+ } else {
+ // Revert the rotation to our saved value if we transition from HALF_FOLDED.
+ mRotation = mHalfFoldSavedRotation;
+ // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here
+ // so we will override USER_ROTATION_LOCKED and allow a rotation).
+ mService.updateRotation(false /* alwaysSendConfiguration */,
+ false /* forceRelayout */);
+ // Once we are rotated, set mFoldstate, effectively removing the lock override.
+ mFoldState = newState;
+ }
+ }
+ }
+
private class OrientationListener extends WindowOrientationListener implements Runnable {
transient boolean mEnabled;
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index a89894d..30bdc34 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -24,10 +24,7 @@
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.Context;
import android.graphics.Rect;
-import android.hardware.devicestate.DeviceStateManager;
-import android.os.HandlerExecutor;
import android.window.DisplayAreaInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
@@ -36,11 +33,8 @@
private final DisplayContent mDisplayContent;
private final WindowManagerService mService;
- private final DeviceStateManager mDeviceStateManager;
private final TransitionController mTransitionController;
- private DeviceStateListener mDeviceStateListener;
-
/**
* If on a foldable device represents whether the device is folded or not
*/
@@ -52,21 +46,15 @@
mDisplayContent = displayContent;
mService = displayContent.mWmService;
mTransitionController = transitionController;
-
- mDeviceStateManager = mService.mContext.getSystemService(DeviceStateManager.class);
-
- if (mDeviceStateManager != null) {
- mDeviceStateListener = new DeviceStateListener(mService.mContext);
- mDeviceStateManager
- .registerCallback(new HandlerExecutor(mDisplayContent.mWmService.mH),
- mDeviceStateListener);
- }
}
- public void destroy() {
- if (mDeviceStateManager != null) {
- mDeviceStateManager.unregisterCallback(mDeviceStateListener);
- }
+ /**
+ * Called by the DeviceStateManager callback when the state changes.
+ */
+ void foldStateChanged(DeviceStateController.FoldState newFoldState) {
+ // Ignore transitions to/from half-folded.
+ if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return;
+ mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED;
}
/**
@@ -143,10 +131,4 @@
mTransition = null;
}
- class DeviceStateListener extends DeviceStateManager.FoldStateListener {
-
- DeviceStateListener(Context context) {
- super(context, newIsFolded -> mIsFolded = newIsFolded);
- }
- }
}
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index ab38ed23..4459d45 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1844,15 +1844,15 @@
flags |= FLAG_TRANSLUCENT;
}
final Task task = wc.asTask();
- if (task != null && task.voiceSession != null) {
- flags |= FLAG_IS_VOICE_INTERACTION;
- }
if (task != null) {
final ActivityRecord topActivity = task.getTopNonFinishingActivity();
if (topActivity != null && topActivity.mStartingData != null
&& topActivity.mStartingData.hasImeSurface()) {
flags |= FLAG_WILL_IME_SHOWN;
}
+ if (task.voiceSession != null) {
+ flags |= FLAG_IS_VOICE_INTERACTION;
+ }
}
Task parentTask = null;
final ActivityRecord record = wc.asActivityRecord();
@@ -1880,20 +1880,26 @@
// Whether the container fills its parent Task bounds.
flags |= FLAG_FILLS_TASK;
}
- }
- final DisplayContent dc = wc.asDisplayContent();
- if (dc != null) {
- flags |= FLAG_IS_DISPLAY;
- if (dc.hasAlertWindowSurfaces()) {
- flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+ } else {
+ final DisplayContent dc = wc.asDisplayContent();
+ if (dc != null) {
+ flags |= FLAG_IS_DISPLAY;
+ if (dc.hasAlertWindowSurfaces()) {
+ flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+ }
+ } else if (isWallpaper(wc)) {
+ flags |= FLAG_IS_WALLPAPER;
+ } else if (isInputMethod(wc)) {
+ flags |= FLAG_IS_INPUT_METHOD;
+ } else {
+ // In this condition, the wc can only be WindowToken or DisplayArea.
+ final int type = wc.getWindowType();
+ if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
+ && type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
+ flags |= TransitionInfo.FLAG_IS_SYSTEM_WINDOW;
+ }
}
}
- if (isWallpaper(wc)) {
- flags |= FLAG_IS_WALLPAPER;
- }
- if (isInputMethod(wc)) {
- flags |= FLAG_IS_INPUT_METHOD;
- }
if (occludesKeyguard(wc)) {
flags |= FLAG_OCCLUDES_KEYGUARD;
}
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 77d0f37..ac85c9a 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -126,19 +126,27 @@
mTransitionTracer = transitionTracer;
mTransitionPlayerDeath = () -> {
synchronized (mAtm.mGlobalLock) {
- // Clean-up/finish any playing transitions.
- for (int i = 0; i < mPlayingTransitions.size(); ++i) {
- mPlayingTransitions.get(i).cleanUpOnFailure();
- }
- mPlayingTransitions.clear();
- mTransitionPlayer = null;
- mTransitionPlayerProc = null;
- mRemotePlayer.clear();
- mRunningLock.doNotifyLocked();
+ detachPlayer();
}
};
}
+ private void detachPlayer() {
+ if (mTransitionPlayer == null) return;
+ // Clean-up/finish any playing transitions.
+ for (int i = 0; i < mPlayingTransitions.size(); ++i) {
+ mPlayingTransitions.get(i).cleanUpOnFailure();
+ }
+ mPlayingTransitions.clear();
+ if (mCollectingTransition != null) {
+ mCollectingTransition.abort();
+ }
+ mTransitionPlayer = null;
+ mTransitionPlayerProc = null;
+ mRemotePlayer.clear();
+ mRunningLock.doNotifyLocked();
+ }
+
/** @see #createTransition(int, int) */
@NonNull
Transition createTransition(int type) {
@@ -193,7 +201,7 @@
if (mTransitionPlayer.asBinder() != null) {
mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
}
- mTransitionPlayer = null;
+ detachPlayer();
}
if (player.asBinder() != null) {
player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 1e6c720..32feb6c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.content.ClipData;
import android.content.Context;
import android.graphics.Matrix;
@@ -452,6 +453,15 @@
public abstract boolean isKeyguardShowingAndNotOccluded();
/**
+ * Return whether the keyguard is secured by a PIN, pattern or password or a SIM card is
+ * currently locked.
+ *
+ * @param userId User ID to be queried about.
+ * @return {@code true} if a PIN, pattern or password is set or a SIM card is locked.
+ */
+ public abstract boolean isKeyguardSecure(@UserIdInt int userId);
+
+ /**
* Gets the frame of a window given its token.
*
* @param token The token.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 961c320..5ac034b 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7763,6 +7763,11 @@
}
@Override
+ public boolean isKeyguardSecure(@UserIdInt int userId) {
+ return mPolicy.isKeyguardSecure(userId);
+ }
+
+ @Override
public void showGlobalActions() {
WindowManagerService.this.showGlobalActions();
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 2304ab4..3590e9c2 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1128,10 +1128,13 @@
final LauncherAppsServiceInternal launcherApps = LocalServices.getService(
LauncherAppsServiceInternal.class);
- launcherApps.startShortcut(caller.mUid, caller.mPid, callingPackage,
- hop.getShortcutInfo().getPackage(), null /* default featureId */,
+ final boolean success = launcherApps.startShortcut(caller.mUid, caller.mPid,
+ callingPackage, hop.getShortcutInfo().getPackage(), null /* featureId */,
hop.getShortcutInfo().getId(), null /* sourceBounds */, launchOpts,
hop.getShortcutInfo().getUserId());
+ if (success) {
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
+ }
break;
}
case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: {
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 7e93d62..5fa8dcc 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -168,6 +168,8 @@
"android.hardware.memtrack-V1-ndk",
"android.hardware.power@1.0",
"android.hardware.power@1.1",
+ "android.hardware.power@1.2",
+ "android.hardware.power@1.3",
"android.hardware.power-V3-cpp",
"android.hardware.power.stats@1.0",
"android.hardware.power.stats-V1-ndk",
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index ebdef8a..a09d994 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -58,6 +58,7 @@
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",
+ "truth-prebuilt",
"junit",
"junit-params",
"platform-compat-test-rules",
diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
index 1171518..581a2a7 100644
--- a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
@@ -41,7 +41,6 @@
import org.junit.runner.RunWith;
import java.util.List;
-import java.util.concurrent.CancellationException;
@Presubmit
@RunWith(AndroidJUnit4.class)
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 1e67c12..3f4bec6 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
@@ -49,6 +50,8 @@
import com.android.server.locksettings.SyntheticPasswordManager.PasswordData;
import com.android.server.locksettings.SyntheticPasswordManager.SyntheticPassword;
+import libcore.util.HexEncoding;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -84,6 +87,8 @@
long protectorId = manager.createLskfBasedProtector(mGateKeeperService,
LockscreenCredential.createNone(), sp, USER_ID);
assertFalse(lskfGatekeeperHandleExists(USER_ID));
+ assertFalse(manager.hasPasswordData(protectorId, USER_ID));
+ assertFalse(manager.hasPasswordMetrics(protectorId, USER_ID));
AuthenticationResult result = manager.unlockLskfBasedProtector(mGateKeeperService,
protectorId, LockscreenCredential.createNone(), USER_ID, null);
@@ -103,6 +108,8 @@
long protectorId = manager.createLskfBasedProtector(mGateKeeperService, password, sp,
USER_ID);
assertTrue(lskfGatekeeperHandleExists(USER_ID));
+ assertTrue(manager.hasPasswordData(protectorId, USER_ID));
+ assertTrue(manager.hasPasswordMetrics(protectorId, USER_ID));
AuthenticationResult result = manager.unlockLskfBasedProtector(mGateKeeperService,
protectorId, password, USER_ID, null);
@@ -452,19 +459,46 @@
mService.getHashFactor(profilePassword, MANAGED_PROFILE_USER_ID));
}
+ // Tests stretching of a nonempty LSKF.
@Test
- public void testPasswordData_scryptParams() {
- // CREDENTIAL_TYPE_NONE should result in the minimum scrypt params being used.
- PasswordData data = PasswordData.create(CREDENTIAL_TYPE_NONE);
- assertEquals(1, data.scryptLogN);
- assertEquals(0, data.scryptLogR);
- assertEquals(0, data.scryptLogP);
+ public void testStretchLskf_enabled() {
+ byte[] actual = mSpManager.stretchLskf(newPin("12345"), createTestPasswordData());
+ String expected = "467986710DE8F0D4F4A3668DFF58C9B7E5DB96A79B7CCF415BBD4D7767F8CFFA";
+ assertEquals(expected, HexEncoding.encodeToString(actual));
+ }
- // Any other credential type should result in the real scrypt params being used.
- data = PasswordData.create(CREDENTIAL_TYPE_PASSWORD);
- assertTrue(data.scryptLogN > 1);
- assertTrue(data.scryptLogR > 0);
- assertTrue(data.scryptLogP > 0);
+ // Tests the case where stretching is disabled for an empty LSKF.
+ @Test
+ public void testStretchLskf_disabled() {
+ byte[] actual = mSpManager.stretchLskf(nonePassword(), null);
+ // "default-password", zero padded
+ String expected = "64656661756C742D70617373776F726400000000000000000000000000000000";
+ assertEquals(expected, HexEncoding.encodeToString(actual));
+ }
+
+ // Tests the legacy case where stretching is enabled for an empty LSKF.
+ @Test
+ public void testStretchLskf_emptyButEnabled() {
+ byte[] actual = mSpManager.stretchLskf(nonePassword(), createTestPasswordData());
+ String expected = "9E6DDCC1EC388BB1E1CD54097AF924CA80BCB90993196FA8F6122FF58EB333DE";
+ assertEquals(expected, HexEncoding.encodeToString(actual));
+ }
+
+ // Tests the forbidden case where stretching is disabled for a nonempty LSKF.
+ @Test
+ public void testStretchLskf_nonEmptyButDisabled() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mSpManager.stretchLskf(newPin("12345"), null));
+ }
+
+ private PasswordData createTestPasswordData() {
+ PasswordData data = new PasswordData();
+ // For the unit test, the scrypt parameters have to be constant; the salt can't be random.
+ data.scryptLogN = 11;
+ data.scryptLogR = 3;
+ data.scryptLogP = 1;
+ data.salt = "abcdefghijklmnop".getBytes();
+ return data;
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
new file mode 100644
index 0000000..0b27f87
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2022 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.utils;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Collection;
+
+@SmallTest
+@RunWith(Enclosed.class)
+public class EventLoggerTest {
+
+ private static final int EVENTS_LOGGER_SIZE = 3;
+ private static final String EVENTS_LOGGER_TAG = "TestLogger";
+
+ private static final TestEvent TEST_EVENT_1 = new TestEvent();
+ private static final TestEvent TEST_EVENT_2 = new TestEvent();
+ private static final TestEvent TEST_EVENT_3 = new TestEvent();
+ private static final TestEvent TEST_EVENT_4 = new TestEvent();
+ private static final TestEvent TEST_EVENT_5 = new TestEvent();
+
+ @RunWith(JUnit4.class)
+ public static class BasicOperationsTest {
+
+ private StringWriter mTestStringWriter;
+ private PrintWriter mTestPrintWriter;
+
+ private EventLogger mEventLogger;
+
+ @Before
+ public void setUp() {
+ mTestStringWriter = new StringWriter();
+ mTestPrintWriter = new PrintWriter(mTestStringWriter);
+ mEventLogger = new EventLogger(EVENTS_LOGGER_SIZE, EVENTS_LOGGER_TAG);
+ }
+
+ @Test
+ public void testThatConsumeOfEmptyLoggerProducesEmptyList() {
+ mEventLogger.dump(mTestPrintWriter);
+ assertThat(mTestStringWriter.toString()).isEmpty();
+ }
+ }
+
+ @RunWith(Parameterized.class)
+ public static class LoggingOperationTest {
+
+ @Parameterized.Parameters
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ {
+ // insertion order, max size is 3
+ new EventLogger.Event[] { TEST_EVENT_1, TEST_EVENT_2 },
+ // expected events
+ new EventLogger.Event[] { TEST_EVENT_2, TEST_EVENT_1 }
+ },
+ {
+ // insertion order, max size is 3
+ new EventLogger.Event[] { TEST_EVENT_1, TEST_EVENT_3, TEST_EVENT_2 },
+ // expected events
+ new EventLogger.Event[] { TEST_EVENT_2, TEST_EVENT_3, TEST_EVENT_1 }
+ },
+ {
+ // insertion order, max size is 3
+ new EventLogger.Event[] { TEST_EVENT_1, TEST_EVENT_2, TEST_EVENT_3,
+ TEST_EVENT_4 },
+ // expected events
+ new EventLogger.Event[] { TEST_EVENT_4, TEST_EVENT_3, TEST_EVENT_2 }
+ },
+ {
+ // insertion order, max size is 3
+ new EventLogger.Event[] { TEST_EVENT_1, TEST_EVENT_2, TEST_EVENT_3,
+ TEST_EVENT_4, TEST_EVENT_5 },
+ // expected events
+ new EventLogger.Event[] { TEST_EVENT_5, TEST_EVENT_4, TEST_EVENT_3 }
+ }
+ });
+ }
+
+ private EventLogger mEventLogger;
+
+ private final StringWriter mTestStringWriter;
+ private final PrintWriter mTestPrintWriter;
+ private final EventLogger.Event[] mEventsToInsert;
+ private final EventLogger.Event[] mExpectedEvents;
+
+ public LoggingOperationTest(EventLogger.Event[] eventsToInsert,
+ EventLogger.Event[] expectedEvents) {
+ mTestStringWriter = new StringWriter();
+ mTestPrintWriter = new PrintWriter(mTestStringWriter);
+ mEventsToInsert = eventsToInsert;
+ mExpectedEvents = expectedEvents;
+ }
+
+ @Before
+ public void setUp() {
+ mEventLogger = new EventLogger(EVENTS_LOGGER_SIZE, EVENTS_LOGGER_TAG);
+ }
+
+ @Test
+ public void testThatLoggingWorksAsExpected() {
+ for (EventLogger.Event event: mEventsToInsert) {
+ mEventLogger.log(event);
+ }
+
+ mEventLogger.dump(mTestPrintWriter);
+
+ assertThat(mTestStringWriter.toString())
+ .isEqualTo(convertEventsToString(mExpectedEvents));
+ }
+
+ }
+
+ private static String convertEventsToString(EventLogger.Event[] events) {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+
+ printWriter.println("Events log: " + EVENTS_LOGGER_TAG);
+
+ for (EventLogger.Event event: events) {
+ printWriter.println(event.toString());
+ }
+
+ return stringWriter.toString();
+ }
+
+ private static class TestEvent extends EventLogger.Event {
+
+ @Override
+ public String eventToString() {
+ return getClass().getName() + "@" + Integer.toHexString(hashCode());
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/utils/OWNERS b/services/tests/servicestests/src/com/android/server/utils/OWNERS
index 1853220..d1a36bb 100644
--- a/services/tests/servicestests/src/com/android/server/utils/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/utils/OWNERS
@@ -2,3 +2,5 @@
per-file WatchableTester.java = shombert@google.com
per-file WatcherTest.java = file:/services/core/java/com/android/server/pm/OWNERS
per-file WatcherTest.java = shombert@google.com
+per-file EventLoggerTest.java = file:/platform/frameworks/av:/media/janitors/media_solutions_OWNERS
+per-file EventLoggerTest.java = jmtrivi@google.com
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 2f23e7f..8a18912 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2319,6 +2319,22 @@
assertTrue(activity1.getTask().getTaskInfo().launchCookies.contains(launchCookie));
}
+ @Test
+ public void testOrientationForScreenOrientationBehind() {
+ final Task task = createTask(mDisplayContent);
+ // Activity below
+ new ActivityBuilder(mAtm)
+ .setTask(task)
+ .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+ .build();
+ final ActivityRecord activityTop = new ActivityBuilder(mAtm)
+ .setTask(task)
+ .setScreenOrientation(SCREEN_ORIENTATION_BEHIND)
+ .build();
+ final int topOrientation = activityTop.getRequestedConfigurationOrientation();
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation);
+ }
+
private void verifyProcessInfoUpdate(ActivityRecord activity, State state,
boolean shouldUpdate, boolean activityChange) {
reset(activity.app);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
new file mode 100644
index 0000000..86732c9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link DeviceStateController}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DeviceStateControllerTests
+ */
+@SmallTest
+@Presubmit
+public class DeviceStateControllerTests {
+
+ private DeviceStateController.FoldStateListener mFoldStateListener;
+ private DeviceStateController mTarget;
+ private DeviceStateControllerBuilder mBuilder;
+
+ private Context mMockContext;
+ private Handler mMockHandler;
+ private Resources mMockRes;
+ private DeviceStateManager mMockDeviceStateManager;
+
+ private Consumer<DeviceStateController.FoldState> mDelegate;
+ private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+
+ @Before
+ public void setUp() {
+ mBuilder = new DeviceStateControllerBuilder();
+ mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+ }
+
+ private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception {
+ mBuilder.setSupportFold(supportFold, supportHalfFold);
+ mDelegate = (newFoldState) -> {
+ mCurrentState = newFoldState;
+ };
+ mBuilder.setDelegate(mDelegate);
+ mBuilder.build();
+ verifyFoldStateListenerRegistration(1);
+ }
+
+ @Test
+ public void testInitialization() throws Exception {
+ initialize(true /* supportFold */, true /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ }
+
+ @Test
+ public void testInitializationWithNoFoldSupport() throws Exception {
+ initialize(false /* supportFold */, false /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ // Note that the folded state is ignored.
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ }
+
+ @Test
+ public void testWithFoldSupported() throws Exception {
+ initialize(true /* supportFold */, false /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
+ mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored
+ }
+
+ @Test
+ public void testWithHalfFoldSupported() throws Exception {
+ initialize(true /* supportFold */, true /* supportHalfFolded */);
+ mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
+ mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
+ assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED);
+ }
+
+
+ private final int[] mFoldedStates = {0};
+ private final int[] mUnfoldedStates = {1};
+ private final int[] mHalfFoldedStates = {2};
+
+
+ private void verifyFoldStateListenerRegistration(int numOfInvocation) {
+ final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor =
+ ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class);
+ verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback(
+ any(),
+ listenerCaptor.capture());
+ if (numOfInvocation > 0) {
+ mFoldStateListener = listenerCaptor.getValue();
+ }
+ }
+
+ private class DeviceStateControllerBuilder {
+ private boolean mSupportFold = false;
+ private boolean mSupportHalfFold = false;
+ private Consumer<DeviceStateController.FoldState> mDelegate;
+
+ DeviceStateControllerBuilder setSupportFold(
+ boolean supportFold, boolean supportHalfFold) {
+ mSupportFold = supportFold;
+ mSupportHalfFold = supportHalfFold;
+ return this;
+ }
+
+ DeviceStateControllerBuilder setDelegate(
+ Consumer<DeviceStateController.FoldState> delegate) {
+ mDelegate = delegate;
+ return this;
+ }
+
+ private void mockFold(boolean enableFold, boolean enableHalfFold) {
+ if (enableFold) {
+ when(mMockContext.getResources().getIntArray(
+ com.android.internal.R.array.config_foldedDeviceStates))
+ .thenReturn(mFoldedStates);
+ }
+ if (enableHalfFold) {
+ when(mMockContext.getResources().getIntArray(
+ com.android.internal.R.array.config_halfFoldedDeviceStates))
+ .thenReturn(mHalfFoldedStates);
+ }
+ }
+
+ private void build() throws Exception {
+ mMockContext = mock(Context.class);
+ mMockRes = mock(Resources.class);
+ when(mMockContext.getResources()).thenReturn((mMockRes));
+ mMockDeviceStateManager = mock(DeviceStateManager.class);
+ when(mMockContext.getSystemService(DeviceStateManager.class))
+ .thenReturn(mMockDeviceStateManager);
+ mockFold(mSupportFold, mSupportHalfFold);
+ mMockHandler = mock(Handler.class);
+ mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate);
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index 89f7111..b45c37f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -28,6 +28,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.atMost;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
@@ -103,7 +104,7 @@
private Context mMockContext;
private Resources mMockRes;
private SensorManager mMockSensorManager;
- private Sensor mFakeSensor;
+ private Sensor mFakeOrientationSensor;
private DisplayWindowSettings mMockDisplayWindowSettings;
private ContentResolver mMockResolver;
private FakeSettingsProvider mFakeSettingsProvider;
@@ -323,7 +324,7 @@
waitForUiHandler();
verify(mMockSensorManager, times(numOfInvocation)).registerListener(
listenerCaptor.capture(),
- same(mFakeSensor),
+ same(mFakeOrientationSensor),
anyInt(),
any());
if (numOfInvocation > 0) {
@@ -460,7 +461,7 @@
SensorEvent.class.getDeclaredConstructor(int.class);
constructor.setAccessible(true);
final SensorEvent event = constructor.newInstance(1);
- event.sensor = mFakeSensor;
+ event.sensor = mFakeOrientationSensor;
event.values[0] = rotation;
event.timestamp = SystemClock.elapsedRealtimeNanos();
return event;
@@ -691,6 +692,43 @@
SCREEN_ORIENTATION_SENSOR, Surface.ROTATION_0));
}
+ // ====================================================
+ // Tests for half-fold auto-rotate override of rotation
+ // ====================================================
+ @Test
+ public void testUpdatesRotationWhenSensorUpdates_RotationLocked_HalfFolded() throws Exception {
+ mBuilder.setSupportHalfFoldAutoRotateOverride(true);
+ mBuilder.build();
+ configureDisplayRotation(SCREEN_ORIENTATION_LANDSCAPE, false, false);
+
+ enableOrientationSensor();
+
+ mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+ freezeRotation(Surface.ROTATION_270);
+
+ mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
+ assertTrue(waitForUiHandler());
+ // No rotation...
+ assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ // ... until half-fold
+ mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED);
+ assertTrue(waitForUiHandler());
+ verify(sMockWm).updateRotation(false, false);
+ assertTrue(waitForUiHandler());
+ assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ // ... then transition back to flat
+ mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+ assertTrue(waitForUiHandler());
+ verify(sMockWm, atLeast(1)).updateRotation(false, false);
+ assertTrue(waitForUiHandler());
+ assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+ }
+
// =================================
// Tests for Policy based Rotation
// =================================
@@ -884,6 +922,7 @@
private class DisplayRotationBuilder {
private boolean mIsDefaultDisplay = true;
private boolean mSupportAutoRotation = true;
+ private boolean mSupportHalfFoldAutoRotateOverride = false;
private int mLidOpenRotation = WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT;
private int mCarDockRotation;
@@ -920,6 +959,12 @@
return this;
}
+ private DisplayRotationBuilder setSupportHalfFoldAutoRotateOverride(
+ boolean supportHalfFoldAutoRotateOverride) {
+ mSupportHalfFoldAutoRotateOverride = supportHalfFoldAutoRotateOverride;
+ return this;
+ }
+
private void captureObservers() {
ArgumentCaptor<ContentObserver> captor = ArgumentCaptor.forClass(
ContentObserver.class);
@@ -1032,9 +1077,13 @@
mMockSensorManager = mock(SensorManager.class);
when(mMockContext.getSystemService(Context.SENSOR_SERVICE))
.thenReturn(mMockSensorManager);
- mFakeSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION);
+ mFakeOrientationSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION);
when(mMockSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION)).thenReturn(
- Collections.singletonList(mFakeSensor));
+ Collections.singletonList(mFakeOrientationSensor));
+
+ when(mMockContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_windowManagerHalfFoldAutoRotateOverride))
+ .thenReturn(mSupportHalfFoldAutoRotateOverride);
mMockResolver = mock(ContentResolver.class);
when(mMockContext.getContentResolver()).thenReturn(mMockResolver);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 0622612..f43f0a5 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -4840,11 +4840,32 @@
* The max acceptable value of this config is 24 hours.
*
* @hide
+ * @deprecated Use {@link #KEY_DATA_SWITCH_VALIDATION_MIN_INTERVAL_MILLIS_LONG} instead.
*/
+ @Deprecated
public static final String KEY_DATA_SWITCH_VALIDATION_MIN_GAP_LONG =
"data_switch_validation_min_gap_long";
/**
+ * Data switch validation minimal interval, in milliseconds.
+ *
+ * If a connection to the default (Internet) PDN for the current subscription is validated on
+ * a given operator within a given tracking area, re-validations to that matching operator will
+ * be skipped if they would occur within the specified interval. Instead, the connection will
+ * automatically considered validated.
+ *
+ * If the network was validated within the interval but the latest validation result was false,
+ * the validation will not be skipped. If not set or set to 0, validation will not be skipped.
+ *
+ * The valid range of value is between 0 millisecond and 24 hours, inclusive in both sides. The
+ * default value is 24 hours.
+ *
+ * @see android.net.NetworkCapabilities#NET_CAPABILITY_VALIDATED
+ */
+ public static final String KEY_DATA_SWITCH_VALIDATION_MIN_INTERVAL_MILLIS_LONG =
+ KEY_DATA_SWITCH_VALIDATION_MIN_GAP_LONG;
+
+ /**
* A boolean property indicating whether this subscription should be managed as an opportunistic
* subscription.
*
@@ -9360,7 +9381,8 @@
sDefaults.putInt(KEY_GBA_UA_TLS_CIPHER_SUITE_INT, TlsParams.TLS_NULL_WITH_NULL_NULL);
sDefaults.putBoolean(KEY_SHOW_FORWARDED_NUMBER_BOOL, false);
- sDefaults.putLong(KEY_DATA_SWITCH_VALIDATION_MIN_GAP_LONG, TimeUnit.DAYS.toMillis(1));
+ sDefaults.putLong(KEY_DATA_SWITCH_VALIDATION_MIN_INTERVAL_MILLIS_LONG,
+ TimeUnit.DAYS.toMillis(1));
sDefaults.putStringArray(KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY,
new String[0]);
sDefaults.putStringArray(KEY_APN_PRIORITY_STRING_ARRAY, new String[] {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 8818ac2..f3d48a8 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -9113,7 +9113,8 @@
* <p>Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling
* app has carrier privileges (see {@link #hasCarrierPrivileges})
- * and {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
+ * and {@link android.Manifest.permission#ACCESS_FINE_LOCATION} if includeLocationData is
+ * set to {@link #INCLUDE_LOCATION_DATA_FINE}.
*
* If the system-wide location switch is off, apps may still call this API, with the
* following constraints:
@@ -9127,7 +9128,10 @@
* </ol>
*
* @param includeLocationData Specifies if the caller would like to receive
- * location related information.
+ * location related information. If this parameter is set to
+ * {@link #INCLUDE_LOCATION_DATA_FINE} then the application will be checked for
+ * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permission and available
+ * location related information received during network scan will be sent to the caller.
* @param request Contains all the RAT with bands/channels that need to be scanned.
* @param executor The executor through which the callback should be invoked. Since the scan
* request may trigger multiple callbacks and they must be invoked in the same order as
@@ -9138,8 +9142,7 @@
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(allOf = {
- android.Manifest.permission.MODIFY_PHONE_STATE,
- Manifest.permission.ACCESS_FINE_LOCATION
+ android.Manifest.permission.MODIFY_PHONE_STATE
})
public @Nullable NetworkScan requestNetworkScan(
@IncludeLocationData int includeLocationData,
diff --git a/tests/FlickerTests/res/anim/show_2000ms.xml b/tests/FlickerTests/res/anim/show_hide_show_3000ms.xml
similarity index 64%
rename from tests/FlickerTests/res/anim/show_2000ms.xml
rename to tests/FlickerTests/res/anim/show_hide_show_3000ms.xml
index 76e375f..7b3f07e 100644
--- a/tests/FlickerTests/res/anim/show_2000ms.xml
+++ b/tests/FlickerTests/res/anim/show_hide_show_3000ms.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
@@ -15,7 +14,18 @@
~ limitations under the License.
-->
-<translate xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="2000"
- android:fromXDelta="0"
- android:toXDelta="0" />
\ No newline at end of file
+<set
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true">
+
+ <alpha
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:duration="1000" />
+
+ <alpha
+ android:startOffset="2000"
+ android:fromAlpha="1.0"
+ android:toAlpha="1.0"
+ android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
index bc2fe46..e1fd5a7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
@@ -30,13 +30,11 @@
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.helpers.StandardAppHelper
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.WindowManagerConditionsFactory
-import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -50,7 +48,7 @@
*
* Actions:
* ```
- * Launches SimpleActivity with alpha_2000ms animation
+ * Launches SimpleActivity with a special animation.
* ```
*/
@RequiresDevice
@@ -89,21 +87,31 @@
@Presubmit
@Test
fun testSimpleActivityIsShownDirectly() {
- Assume.assumeFalse(isShellTransitionsEnabled)
testSpec.assertLayers {
+ // Before the app launches, only the launcher is visible.
isVisible(ComponentNameMatcher.LAUNCHER)
- .isInvisible(ComponentNameMatcher.SPLASH_SCREEN)
- .isInvisible(testApp)
- .then()
- // The custom animation should block the entire launcher from the very beginning
- .isInvisible(ComponentNameMatcher.LAUNCHER)
+ .isInvisible(testApp)
+ .then()
+ // Animation starts, but the app may not be drawn yet which means the Splash
+ // may be visible.
+ .isInvisible(testApp, isOptional = true)
+ .isVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+ .then()
+ // App shows up with the custom animation starting at alpha=1.
+ .isVisible(testApp)
+ .then()
+ // App custom animation continues to alpha=0 (invisible).
+ .isInvisible(testApp)
+ .then()
+ // App custom animation ends with it being visible.
+ .isVisible(testApp)
}
}
private fun createCustomTaskAnimation(): Bundle {
return android.app.ActivityOptions.makeCustomTaskAnimation(
instrumentation.context,
- R.anim.show_2000ms,
+ R.anim.show_hide_show_3000ms,
0,
Handler.getMain(),
null,
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 7dc9d26..318b8b6 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -173,8 +173,7 @@
// Sparse encode if forced or sdk version is not set in context and config.
} else {
// Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
- sparse_encode = sparse_encode &&
- (context_->GetMinSdkVersion() >= SDK_S_V2 || config.sdkVersion >= SDK_S_V2);
+ sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
}
// Only sparse encode if the offsets are representable in 2 bytes.
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 2097a63..d08b4a3 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -326,7 +326,7 @@
return table;
}
-TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkO) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder()
.SetCompilationPackage("android")
.SetPackageId(0x01)
@@ -369,7 +369,7 @@
EXPECT_EQ(4u, value->value.data);
}
-TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionO) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder()
.SetCompilationPackage("android")
.SetPackageId(0x01)
@@ -388,7 +388,7 @@
std::string sparse_contents;
ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
- EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+ EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
}
TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) {
diff --git a/tools/lint/fix/lint_fix.py b/tools/lint/fix/lint_fix.py
index 3ff8131..0f94bd2 100644
--- a/tools/lint/fix/lint_fix.py
+++ b/tools/lint/fix/lint_fix.py
@@ -50,6 +50,7 @@
"cd $ANDROID_BUILD_TOP",
"source build/envsetup.sh",
f"rm {target}", # remove the file first so soong doesn't think there is no work to do
+ f"rm {path}/{FIX_DIR}.zip", # remove in case there are fixes from a prior run that we don't want applied if this run fails
f"m {target}",
]