Merge "Update lint_fix to remove stale suggested-fixes.zip"
diff --git a/ApiDocs.bp b/ApiDocs.bp
index 4bba338..6a323d6 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -123,17 +123,30 @@
}
droidstubs {
- name: "framework-doc-system-stubs",
- defaults: ["framework-doc-stubs-sources-default"],
- args: metalava_framework_docs_args +
- " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) ",
- api_levels_annotations_enabled: true,
- api_levels_annotations_dirs: [
- "sdk-dir",
- "api-versions-jars-dir",
+ name: "android-non-updatable-doc-stubs-module-lib",
+ defaults: [
+ "android-non-updatable-doc-stubs-defaults",
+ "module-classpath-stubs-defaults",
],
- api_levels_sdk_type: "system",
- extensions_info_file: ":sdk-extensions-info",
+ args: metalava_framework_docs_args +
+ " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) " +
+ " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\) ",
+ generate_stubs: false, // We're only using this module for the annotations.zip output, disable doc-stubs.
+ write_sdk_values: false,
+}
+
+droidstubs {
+ name: "android-non-updatable-doc-stubs-system-server",
+ defaults: [
+ "android-non-updatable-doc-stubs-defaults",
+ "module-classpath-stubs-defaults",
+ ],
+ args: metalava_framework_docs_args +
+ " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) " +
+ " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\) " +
+ " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.SYSTEM_SERVER\\) ",
+ generate_stubs: false, // We're only using this module for the annotations.zip output, disable doc-stubs.
+ write_sdk_values: false,
}
droidstubs {
@@ -151,6 +164,20 @@
extensions_info_file: ":sdk-extensions-info",
}
+droidstubs {
+ name: "framework-doc-system-stubs",
+ defaults: ["framework-doc-stubs-sources-default"],
+ args: metalava_framework_docs_args +
+ " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) ",
+ api_levels_annotations_enabled: true,
+ api_levels_annotations_dirs: [
+ "sdk-dir",
+ "api-versions-jars-dir",
+ ],
+ api_levels_sdk_type: "system",
+ extensions_info_file: ":sdk-extensions-info",
+}
+
/////////////////////////////////////////////////////////////////////
// API docs are created from the generated stub source files
// using droiddoc
diff --git a/StubLibraries.bp b/StubLibraries.bp
index bc2e6dd..0e08496 100644
--- a/StubLibraries.bp
+++ b/StubLibraries.bp
@@ -411,6 +411,36 @@
],
}
+java_library {
+ name: "android_module_stubs_current_with_test_libs",
+ static_libs: [
+ "android_module_lib_stubs_current",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
+ "android.test.runner.stubs",
+ ],
+ defaults: ["android.jar_defaults"],
+ visibility: [
+ "//visibility:override",
+ "//visibility:private",
+ ],
+}
+
+java_library {
+ name: "android_system_server_stubs_current_with_test_libs",
+ static_libs: [
+ "android_system_server_stubs_current",
+ "android.test.base.stubs.system",
+ "android.test.mock.stubs.system",
+ "android.test.runner.stubs.system",
+ ],
+ defaults: ["android.jar_defaults"],
+ visibility: [
+ "//visibility:override",
+ "//visibility:private",
+ ],
+}
+
droidstubs {
name: "api_versions_public",
srcs: [":android_stubs_current_with_test_libs{.jar}"],
@@ -420,6 +450,7 @@
"sdk-dir",
"api-versions-jars-dir",
],
+ api_levels_sdk_type: "public",
extensions_info_file: ":sdk-extensions-info",
}
@@ -436,6 +467,32 @@
extensions_info_file: ":sdk-extensions-info",
}
+droidstubs {
+ name: "api_versions_module_lib",
+ srcs: [":android_module_stubs_current_with_test_libs{.jar}"],
+ generate_stubs: false,
+ api_levels_annotations_enabled: true,
+ api_levels_annotations_dirs: [
+ "sdk-dir",
+ "api-versions-jars-dir",
+ ],
+ api_levels_sdk_type: "module-lib",
+ extensions_info_file: ":sdk-extensions-info",
+}
+
+droidstubs {
+ name: "api_versions_system_server",
+ srcs: [":android_system_server_stubs_current_with_test_libs{.jar}"],
+ generate_stubs: false,
+ api_levels_annotations_enabled: true,
+ api_levels_annotations_dirs: [
+ "sdk-dir",
+ "api-versions-jars-dir",
+ ],
+ api_levels_sdk_type: "system-server",
+ extensions_info_file: ":sdk-extensions-info",
+}
+
/////////////////////////////////////////////////////////////////////
// hwbinder.stubs provides APIs required for building HIDL Java
// libraries.
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 901796b..bedfa7f 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -87,6 +87,7 @@
import android.database.ContentObserver;
import android.net.Uri;
import android.os.BatteryManager;
+import android.os.BatteryStatsInternal;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -116,6 +117,7 @@
import android.util.ArraySet;
import android.util.EventLog;
import android.util.IndentingPrintWriter;
+import android.util.IntArray;
import android.util.Log;
import android.util.LongArrayQueue;
import android.util.Pair;
@@ -249,6 +251,7 @@
private ActivityManagerInternal mActivityManagerInternal;
private final EconomyManagerInternal mEconomyManagerInternal;
private PackageManagerInternal mPackageManagerInternal;
+ private BatteryStatsInternal mBatteryStatsInternal;
private RoleManager mRoleManager;
private volatile PermissionManagerServiceInternal mLocalPermissionManager;
@@ -2113,6 +2116,8 @@
LocalServices.getService(AppStandbyInternal.class);
appStandbyInternal.addListener(new AppStandbyTracker());
+ mBatteryStatsInternal = LocalServices.getService(BatteryStatsInternal.class);
+
mRoleManager = getContext().getSystemService(RoleManager.class);
mMetricsHelper.registerPuller(() -> mAlarmStore);
@@ -4783,8 +4788,12 @@
}
final ArraySet<Pair<String, Integer>> triggerPackages =
new ArraySet<>();
+ final IntArray wakeupUids = new IntArray();
for (int i = 0; i < triggerList.size(); i++) {
final Alarm a = triggerList.get(i);
+ if (a.wakeup) {
+ wakeupUids.add(a.uid);
+ }
if (mConstants.USE_TARE_POLICY) {
if (!isExemptFromTare(a)) {
triggerPackages.add(Pair.create(
@@ -4796,6 +4805,11 @@
a.sourcePackage, UserHandle.getUserId(a.creatorUid)));
}
}
+ if (wakeupUids.size() > 0 && mBatteryStatsInternal != null) {
+ mBatteryStatsInternal.noteCpuWakingActivity(
+ BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM, nowELAPSED,
+ wakeupUids.toArray());
+ }
deliverAlarmsLocked(triggerList, nowELAPSED);
mTemporaryQuotaReserve.cleanUpExpiredQuotas(nowELAPSED);
if (mConstants.USE_TARE_POLICY) {
diff --git a/api/Android.bp b/api/Android.bp
index 07fd850..9306671 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -213,6 +213,24 @@
}
genrule {
+ name: "sdk-annotations-module-lib.zip",
+ defaults: ["sdk-annotations-defaults"],
+ srcs: [
+ ":android-non-updatable-doc-stubs-module-lib{.annotations.zip}",
+ ":all-modules-module-lib-annotations",
+ ],
+}
+
+genrule {
+ name: "sdk-annotations-system-server.zip",
+ defaults: ["sdk-annotations-defaults"],
+ srcs: [
+ ":android-non-updatable-doc-stubs-system-server{.annotations.zip}",
+ ":all-modules-system-server-annotations",
+ ],
+}
+
+genrule {
name: "combined-removed-dex",
visibility: [
"//frameworks/base/boot",
diff --git a/api/api.go b/api/api.go
index f158041..6a6c493 100644
--- a/api/api.go
+++ b/api/api.go
@@ -148,18 +148,35 @@
ctx.CreateModule(genrule.GenRuleFactory, &props)
}
-func createMergedPublicAnnotationsFilegroup(ctx android.LoadHookContext, modules []string) {
- props := fgProps{}
- props.Name = proptools.StringPtr("all-modules-public-annotations")
- props.Srcs = createSrcs(modules, "{.public.annotations.zip}")
- ctx.CreateModule(android.FileGroupFactory, &props)
-}
-
-func createMergedSystemAnnotationsFilegroup(ctx android.LoadHookContext, modules []string) {
- props := fgProps{}
- props.Name = proptools.StringPtr("all-modules-system-annotations")
- props.Srcs = createSrcs(modules, "{.system.annotations.zip}")
- ctx.CreateModule(android.FileGroupFactory, &props)
+func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, system_server_modules []string) {
+ for _, i := range []struct{
+ name string
+ tag string
+ modules []string
+ }{
+ {
+ name: "all-modules-public-annotations",
+ tag: "{.public.annotations.zip}",
+ modules: modules,
+ }, {
+ name: "all-modules-system-annotations",
+ tag: "{.system.annotations.zip}",
+ modules: modules,
+ }, {
+ name: "all-modules-module-lib-annotations",
+ tag: "{.module-lib.annotations.zip}",
+ modules: modules,
+ }, {
+ name: "all-modules-system-server-annotations",
+ tag: "{.system-server.annotations.zip}",
+ modules: system_server_modules,
+ },
+ } {
+ props := fgProps{}
+ props.Name = proptools.StringPtr(i.name)
+ props.Srcs = createSrcs(i.modules, i.tag)
+ ctx.CreateModule(android.FileGroupFactory, &props)
+ }
}
func createFilteredApiVersions(ctx android.LoadHookContext, modules []string) {
@@ -172,17 +189,43 @@
// difficult to achieve.
modules = remove(modules, art)
- props := genruleProps{}
- props.Name = proptools.StringPtr("api-versions-xml-public-filtered")
- props.Tools = []string{"api_versions_trimmer"}
- props.Out = []string{"api-versions-public-filtered.xml"}
- props.Cmd = proptools.StringPtr("$(location api_versions_trimmer) $(out) $(in)")
- // Note: order matters: first parameter is the full api-versions.xml
- // after that the stubs files in any order
- // stubs files are all modules that export API surfaces EXCEPT ART
- props.Srcs = append([]string{":api_versions_public{.api_versions.xml}"}, createSrcs(modules, ".stubs{.jar}")...)
- props.Dists = []android.Dist{{Targets: []string{"sdk"}}}
- ctx.CreateModule(genrule.GenRuleFactory, &props)
+ for _, i := range []struct{
+ name string
+ out string
+ in string
+ }{
+ {
+ // We shouldn't need public-filtered or system-filtered.
+ // public-filtered is currently used to lint things that
+ // use the module sdk or the system server sdk, but those
+ // should be switched over to module-filtered and
+ // system-server-filtered, and then public-filtered can
+ // be removed.
+ name: "api-versions-xml-public-filtered",
+ out: "api-versions-public-filtered.xml",
+ in: ":api_versions_public{.api_versions.xml}",
+ }, {
+ name: "api-versions-xml-module-lib-filtered",
+ out: "api-versions-module-lib-filtered.xml",
+ in: ":api_versions_module_lib{.api_versions.xml}",
+ }, {
+ name: "api-versions-xml-system-server-filtered",
+ out: "api-versions-system-server-filtered.xml",
+ in: ":api_versions_system_server{.api_versions.xml}",
+ },
+ } {
+ props := genruleProps{}
+ props.Name = proptools.StringPtr(i.name)
+ props.Out = []string{i.out}
+ // Note: order matters: first parameter is the full api-versions.xml
+ // after that the stubs files in any order
+ // stubs files are all modules that export API surfaces EXCEPT ART
+ props.Srcs = append([]string{i.in}, createSrcs(modules, ".stubs{.jar}")...)
+ props.Tools = []string{"api_versions_trimmer"}
+ props.Cmd = proptools.StringPtr("$(location api_versions_trimmer) $(out) $(in)")
+ props.Dists = []android.Dist{{Targets: []string{"sdk"}}}
+ ctx.CreateModule(genrule.GenRuleFactory, &props)
+ }
}
func createMergedPublicStubs(ctx android.LoadHookContext, modules []string) {
@@ -279,11 +322,12 @@
func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) {
bootclasspath := a.properties.Bootclasspath
+ system_server_classpath := a.properties.System_server_classpath
if ctx.Config().VendorConfig("ANDROID").Bool("include_nonpublic_framework_api") {
bootclasspath = append(bootclasspath, a.properties.Conditional_bootclasspath...)
sort.Strings(bootclasspath)
}
- createMergedTxts(ctx, bootclasspath, a.properties.System_server_classpath)
+ createMergedTxts(ctx, bootclasspath, system_server_classpath)
createMergedStubsSrcjar(ctx, bootclasspath)
@@ -292,8 +336,7 @@
createMergedFrameworkModuleLibStubs(ctx, bootclasspath)
createMergedFrameworkImpl(ctx, bootclasspath)
- createMergedPublicAnnotationsFilegroup(ctx, bootclasspath)
- createMergedSystemAnnotationsFilegroup(ctx, bootclasspath)
+ createMergedAnnotationsFilegroups(ctx, bootclasspath, system_server_classpath)
createFilteredApiVersions(ctx, bootclasspath)
diff --git a/core/api/current.txt b/core/api/current.txt
index 9676451..431f06c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -11596,6 +11596,7 @@
field public static final String ACTION_SESSION_UPDATED = "android.content.pm.action.SESSION_UPDATED";
field public static final String EXTRA_OTHER_PACKAGE_NAME = "android.content.pm.extra.OTHER_PACKAGE_NAME";
field public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";
+ field public static final String EXTRA_PRE_APPROVAL = "android.content.pm.extra.PRE_APPROVAL";
field public static final String EXTRA_SESSION = "android.content.pm.extra.SESSION";
field public static final String EXTRA_SESSION_ID = "android.content.pm.extra.SESSION_ID";
field public static final String EXTRA_STATUS = "android.content.pm.extra.STATUS";
@@ -11652,6 +11653,7 @@
method public void removeChildSessionId(int);
method public void removeSplit(@NonNull String) throws java.io.IOException;
method public void requestChecksums(@NonNull String, int, @NonNull java.util.List<java.security.cert.Certificate>, @NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageManager.OnChecksumsReadyListener) throws java.security.cert.CertificateEncodingException, java.io.FileNotFoundException;
+ method public void requestUserPreapproval(@NonNull android.content.pm.PackageInstaller.PreapprovalDetails, @NonNull android.content.IntentSender);
method @Deprecated public void setChecksums(@NonNull String, @NonNull java.util.List<android.content.pm.Checksum>, @Nullable byte[]) throws java.io.IOException;
method public void setStagingProgress(float);
method public void transfer(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5e1c234..0b428d9 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11162,6 +11162,7 @@
public final class ActivityEvent implements android.os.Parcelable {
method public int describeContents();
+ method @NonNull public android.app.assist.ActivityId getActivityId();
method @NonNull public android.content.ComponentName getComponentName();
method public int getEventType();
method public void writeToParcel(@NonNull android.os.Parcel, int);
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 7d19ed4..81aa6da 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -23,6 +23,7 @@
import android.annotation.UserIdInt;
import android.app.ActivityManager.ProcessCapability;
import android.app.ActivityManager.RestrictionLevel;
+import android.app.assist.ActivityId;
import android.content.ComponentName;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
@@ -343,7 +344,7 @@
*/
public abstract void updateActivityUsageStats(
ComponentName activity, @UserIdInt int userId, int event, IBinder appToken,
- ComponentName taskRoot);
+ ComponentName taskRoot, ActivityId activityId);
public abstract void updateForegroundTimeIfOnBattery(
String packageName, int uid, long cpuTimeDiff);
public abstract void sendForegroundProfileChanged(@UserIdInt int userId);
diff --git a/core/java/android/app/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java
index 033cffe..e2082f7 100644
--- a/core/java/android/app/EnterTransitionCoordinator.java
+++ b/core/java/android/app/EnterTransitionCoordinator.java
@@ -650,7 +650,7 @@
if (decorView != null) {
Drawable drawable = decorView.getBackground();
if (drawable != null) {
- drawable.setAlpha(1);
+ drawable.setAlpha(255);
}
}
}
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index ddfbc68..302d146 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -159,6 +159,7 @@
void clearRequestedListenerHints(in INotificationListener token);
void requestHintsFromListener(in INotificationListener token, int hints);
int getHintsFromListener(in INotificationListener token);
+ int getHintsFromListenerNoToken();
void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter);
int getInterruptionFilterFromListener(in INotificationListener token);
void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 392f52a..f6d27ad 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -66,9 +66,9 @@
/**
* Class to notify the user of events that happen. This is how you tell
- * the user that something has happened in the background. {@more}
+ * the user that something has happened in the background.
*
- * Notifications can take different forms:
+ * <p>Notifications can take different forms:
* <ul>
* <li>A persistent icon that goes in the status bar and is accessible
* through the launcher, (when the user selects it, a designated Intent
diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl
index 8d6c8e8d..1fc6bda 100644
--- a/core/java/android/content/pm/IPackageInstallerSession.aidl
+++ b/core/java/android/content/pm/IPackageInstallerSession.aidl
@@ -20,6 +20,7 @@
import android.content.pm.DataLoaderParamsParcel;
import android.content.pm.IOnChecksumsReadyListener;
import android.content.pm.IPackageInstallObserver2;
+import android.content.pm.PackageInstaller;
import android.content.IntentSender;
import android.os.ParcelFileDescriptor;
@@ -58,4 +59,6 @@
boolean isStaged();
int getInstallFlags();
+
+ void requestUserPreapproval(in PackageInstaller.PreapprovalDetails details, in IntentSender statusReceiver);
}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 5b18273..d2fb1fb 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -170,6 +170,10 @@
/** {@hide} */
public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
+ /** @hide */
+ public static final String ACTION_CONFIRM_PRE_APPROVAL =
+ "android.content.pm.action.CONFIRM_PRE_APPROVAL";
+
/**
* An integer session ID that an operation is working with.
*
@@ -207,6 +211,17 @@
public static final String EXTRA_STATUS = "android.content.pm.extra.STATUS";
/**
+ * Indicate if the status is for a pre-approval request.
+ *
+ * If callers use the same {@link IntentSender} for both
+ * {@link Session#requestUserPreapproval(PreapprovalDetails, IntentSender)} and
+ * {@link Session#commit(IntentSender)}, they can use this to differentiate between them.
+ *
+ * @see Intent#getBooleanExtra(String, boolean)
+ */
+ public static final String EXTRA_PRE_APPROVAL = "android.content.pm.extra.PRE_APPROVAL";
+
+ /**
* Detailed string representation of the status, including raw details that
* are useful for debugging.
*
@@ -1667,6 +1682,41 @@
e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Attempt to request the approval before committing this session.
+ *
+ * For installers that have been granted the
+ * {@link android.Manifest.permission#REQUEST_INSTALL_PACKAGES REQUEST_INSTALL_PACKAGES}
+ * permission, they can request the approval from users before
+ * {@link Session#commit(IntentSender)} is called. This may require user intervention as
+ * well. The result of the request will be reported through the given callback.
+ *
+ * @param details the adequate context to this session for requesting the approval from
+ * users prior to commit.
+ * @param statusReceiver called when the state of the session changes.
+ * Intents sent to this receiver contain
+ * {@link #EXTRA_STATUS}. Refer to the individual
+ * status codes on how to handle them.
+ *
+ * @throws IllegalArgumentException when {@link PreapprovalDetails} is {@code null}.
+ * @throws IllegalArgumentException if {@link IntentSender} is {@code null}.
+ * @throws IllegalStateException if called on a multi-package session (no matter
+ * the parent session or any of the children sessions).
+ * @throws IllegalStateException if called again after this method has been called on
+ * this session.
+ * @throws SecurityException when the caller does not own this session.
+ */
+ public void requestUserPreapproval(@NonNull PreapprovalDetails details,
+ @NonNull IntentSender statusReceiver) {
+ Preconditions.checkArgument(details != null, "preapprovalDetails cannot be null.");
+ Preconditions.checkArgument(statusReceiver != null, "statusReceiver cannot be null.");
+ try {
+ mSession.requestUserPreapproval(details, statusReceiver);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
}
/**
@@ -2631,6 +2681,9 @@
/** {@hide} */
public int installerUid;
+ /** @hide */
+ public boolean isPreapprovalRequested;
+
/** {@hide} */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public SessionInfo() {
@@ -2678,6 +2731,7 @@
mSessionErrorCode = source.readInt();
mSessionErrorMessage = source.readString();
isCommitted = source.readBoolean();
+ isPreapprovalRequested = source.readBoolean();
rollbackDataPolicy = source.readInt();
createdMillis = source.readLong();
requireUserAction = source.readInt();
@@ -3099,7 +3153,7 @@
}
/**
- * Returns the set of session IDs that will be committed when this session is commited if
+ * Returns the set of session IDs that will be committed when this session is committed if
* this session is a multi-package session.
*/
@NonNull
@@ -3257,6 +3311,7 @@
dest.writeInt(mSessionErrorCode);
dest.writeString(mSessionErrorMessage);
dest.writeBoolean(isCommitted);
+ dest.writeBoolean(isPreapprovalRequested);
dest.writeInt(rollbackDataPolicy);
dest.writeLong(createdMillis);
dest.writeInt(requireUserAction);
diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
new file mode 100644
index 0000000..122c54a
--- /dev/null
+++ b/core/java/android/credentials/ui/Entry.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 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.credentials.ui;
+
+import android.annotation.NonNull;
+import android.app.slice.Slice;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * A credential, save, or action entry to be rendered.
+ *
+ * @hide
+ */
+public class Entry implements Parcelable {
+ // TODO: move to jetpack.
+ public static final String VERSION = "v1";
+ public static final Uri CREDENTIAL_MANAGER_ENTRY_URI = Uri.parse("credentialmanager.slice");
+ public static final String HINT_TITLE = "hint_title";
+ public static final String HINT_SUBTITLE = "hint_subtitle";
+ public static final String HINT_ICON = "hint_icon";
+
+ /**
+ * The intent extra key for the action chip {@code Entry} list when launching the UX activities.
+ */
+ public static final String EXTRA_ENTRY_LIST_ACTION_CHIP =
+ "android.credentials.ui.extra.ENTRY_LIST_ACTION_CHIP";
+ /**
+ * The intent extra key for the credential / save {@code Entry} list when launching the UX
+ * activities.
+ */
+ public static final String EXTRA_ENTRY_LIST_CREDENTIAL =
+ "android.credentials.ui.extra.ENTRY_LIST_CREDENTIAL";
+ /**
+ * The intent extra key for the authentication action {@code Entry} when launching the UX
+ * activities.
+ */
+ public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION =
+ "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION";
+
+ // TODO: may be changed to other type depending on the service implementation.
+ private final int mId;
+
+ @NonNull
+ private final Slice mSlice;
+
+ protected Entry(@NonNull Parcel in) {
+ int entryId = in.readInt();
+ Slice slice = Slice.CREATOR.createFromParcel(in);
+
+ mId = entryId;
+ mSlice = slice;
+ AnnotationValidations.validate(NonNull.class, null, mSlice);
+ }
+
+ public Entry(int id, @NonNull Slice slice) {
+ mId = id;
+ mSlice = slice;
+ }
+
+ /**
+ * Returns the id of this entry that's unique within the context of the CredentialManager
+ * request.
+ */
+ public int getEntryId() {
+ return mId;
+ }
+
+ /**
+ * Returns the Slice to be rendered.
+ */
+ @NonNull
+ public Slice getSlice() {
+ return mSlice;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mId);
+ mSlice.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<Entry> CREATOR = new Creator<Entry>() {
+ @Override
+ public Entry createFromParcel(@NonNull Parcel in) {
+ return new Entry(in);
+ }
+
+ @Override
+ public Entry[] newArray(int size) {
+ return new Entry[size];
+ }
+ };
+}
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 49e5e49..18e6ba4 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -17,11 +17,15 @@
package android.credentials.ui;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.AnnotationValidations;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Holds metadata and credential entries for a single provider.
*
@@ -36,13 +40,24 @@
public static final String EXTRA_PROVIDER_DATA_LIST =
"android.credentials.ui.extra.PROVIDER_DATA_LIST";
- // TODO: add entry data.
-
@NonNull
private final String mPackageName;
+ @NonNull
+ private final List<Entry> mCredentialEntries;
+ @NonNull
+ private final List<Entry> mActionChips;
+ @Nullable
+ private final Entry mAuthenticationEntry;
- public ProviderData(@NonNull String packageName) {
+ public ProviderData(
+ @NonNull String packageName,
+ @NonNull List<Entry> credentialEntries,
+ @NonNull List<Entry> actionChips,
+ @Nullable Entry authenticationEntry) {
mPackageName = packageName;
+ mCredentialEntries = credentialEntries;
+ mActionChips = actionChips;
+ mAuthenticationEntry = authenticationEntry;
}
/** Returns the provider package name. */
@@ -51,15 +66,46 @@
return mPackageName;
}
+ @NonNull
+ public List<Entry> getCredentialEntries() {
+ return mCredentialEntries;
+ }
+
+ @NonNull
+ public List<Entry> getActionChips() {
+ return mActionChips;
+ }
+
+ @Nullable
+ public Entry getAuthenticationEntry() {
+ return mAuthenticationEntry;
+ }
+
protected ProviderData(@NonNull Parcel in) {
String packageName = in.readString8();
mPackageName = packageName;
AnnotationValidations.validate(NonNull.class, null, mPackageName);
+
+ List<Entry> credentialEntries = new ArrayList<>();
+ in.readTypedList(credentialEntries, Entry.CREATOR);
+ mCredentialEntries = credentialEntries;
+ AnnotationValidations.validate(NonNull.class, null, mCredentialEntries);
+
+ List<Entry> actionChips = new ArrayList<>();
+ in.readTypedList(actionChips, Entry.CREATOR);
+ mActionChips = actionChips;
+ AnnotationValidations.validate(NonNull.class, null, mActionChips);
+
+ Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
+ mAuthenticationEntry = authenticationEntry;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mPackageName);
+ dest.writeTypedList(mCredentialEntries);
+ dest.writeTypedList(mActionChips);
+ dest.writeTypedObject(mAuthenticationEntry, flags);
}
@Override
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
index 8305843..bc63686 100644
--- a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
+++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -299,7 +299,7 @@
if (isLegacyCompatibilityWalEnabled()) {
return SQLiteCompatibilityWalFlags.getWALSyncMode();
} else {
- return SQLiteGlobal.getDefaultSyncMode();
+ return SQLiteGlobal.getWALSyncMode();
}
} else {
return SQLiteGlobal.getDefaultSyncMode();
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/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index adeb722..7a55a5c 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -2017,7 +2017,7 @@
public static final int EVENT_PACKAGE_INSTALLED = 0x000b;
// Event for a package being uninstalled.
public static final int EVENT_PACKAGE_UNINSTALLED = 0x000c;
- // Event for a package being uninstalled.
+ // Event for an alarm being sent out to an app.
public static final int EVENT_ALARM = 0x000d;
// Record that we have decided we need to collect new stats data.
public static final int EVENT_COLLECT_EXTERNAL_STATS = 0x000e;
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 2c0be87..3bf9ca0 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -115,6 +115,7 @@
private final int mMaxStreamVolume;
private boolean mAffectedByRingerMode;
private boolean mNotificationOrRing;
+ private final boolean mNotifAliasRing;
private final Receiver mReceiver = new Receiver();
private Handler mHandler;
@@ -179,6 +180,8 @@
if (mNotificationOrRing) {
mRingerMode = mAudioManager.getRingerModeInternal();
}
+ mNotifAliasRing = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_alias_ring_notif_stream_types);
mZenMode = mNotificationManager.getZenMode();
if (hasAudioProductStrategies()) {
@@ -280,7 +283,15 @@
if (zenMuted) {
mSeekBar.setProgress(mLastAudibleStreamVolume, true);
} else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
- mSeekBar.setProgress(0, true);
+ /**
+ * the first variable above is preserved and the conditions below are made explicit
+ * so that when user attempts to slide the notification seekbar out of vibrate the
+ * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
+ */
+ if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING
+ || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
+ mSeekBar.setProgress(0, true);
+ }
} else if (mMuted) {
mSeekBar.setProgress(0, true);
} else {
@@ -354,6 +365,7 @@
// set the time of stop volume
if ((mStreamType == AudioManager.STREAM_VOICE_CALL
|| mStreamType == AudioManager.STREAM_RING
+ || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION)
|| mStreamType == AudioManager.STREAM_ALARM)) {
sStopVolumeTime = java.lang.System.currentTimeMillis();
}
@@ -632,8 +644,8 @@
}
private void updateVolumeSlider(int streamType, int streamValue) {
- final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
- : (streamType == mStreamType);
+ final boolean streamMatch = mNotifAliasRing && mNotificationOrRing
+ ? isNotificationOrRing(streamType) : streamType == mStreamType;
if (mSeekBar != null && streamMatch && streamValue != -1) {
final boolean muted = mAudioManager.isStreamMute(mStreamType)
|| streamValue == 0;
diff --git a/core/java/android/service/contentcapture/ActivityEvent.java b/core/java/android/service/contentcapture/ActivityEvent.java
index d286942..699f42b 100644
--- a/core/java/android/service/contentcapture/ActivityEvent.java
+++ b/core/java/android/service/contentcapture/ActivityEvent.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.app.assist.ActivityId;
import android.app.usage.UsageEvents.Event;
import android.content.ComponentName;
import android.os.Parcel;
@@ -80,14 +81,25 @@
private final @NonNull ComponentName mComponentName;
private final @ActivityEventType int mType;
+ private final @NonNull ActivityId mActivityId;
/** @hide */
- public ActivityEvent(@NonNull ComponentName componentName, @ActivityEventType int type) {
+ public ActivityEvent(@NonNull ActivityId activityId,
+ @NonNull ComponentName componentName, @ActivityEventType int type) {
+ mActivityId = activityId;
mComponentName = componentName;
mType = type;
}
/**
+ * Gets the ActivityId of the activity associated with the event.
+ */
+ @NonNull
+ public ActivityId getActivityId() {
+ return mActivityId;
+ }
+
+ /**
* Gests the {@link ComponentName} of the activity associated with the event.
*/
@NonNull
@@ -129,6 +141,7 @@
@Override
public String toString() {
return new StringBuilder("ActivityEvent[").append(mComponentName.toShortString())
+ .append(", ActivityId: ").append(mActivityId)
.append("]:").append(getTypeAsString(mType)).toString();
}
@@ -141,6 +154,7 @@
public void writeToParcel(@NonNull Parcel parcel, int flags) {
parcel.writeParcelable(mComponentName, flags);
parcel.writeInt(mType);
+ parcel.writeParcelable(mActivityId, flags);
}
public static final @android.annotation.NonNull Creator<ActivityEvent> CREATOR =
@@ -149,9 +163,12 @@
@Override
@NonNull
public ActivityEvent createFromParcel(@NonNull Parcel parcel) {
- final ComponentName componentName = parcel.readParcelable(null, android.content.ComponentName.class);
+ final ComponentName componentName =
+ parcel.readParcelable(null, ComponentName.class);
final int eventType = parcel.readInt();
- return new ActivityEvent(componentName, eventType);
+ final ActivityId activityId =
+ parcel.readParcelable(null, ActivityId.class);
+ return new ActivityEvent(activityId, componentName, eventType);
}
@Override
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 4324442..aa45c20 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -42,8 +42,11 @@
private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
@Override
public void startDream(WindowManager.LayoutParams layoutParams,
- IDreamOverlayCallback callback) {
+ IDreamOverlayCallback callback, String dreamComponent,
+ boolean shouldShowComplications) {
mDreamOverlayCallback = callback;
+ mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
+ mShowComplications = shouldShowComplications;
onStartDream(layoutParams);
}
};
@@ -56,10 +59,6 @@
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
- mShowComplications = intent.getBooleanExtra(DreamService.EXTRA_SHOW_COMPLICATIONS,
- DreamService.DEFAULT_SHOW_COMPLICATIONS);
- mDreamComponent = intent.getParcelableExtra(DreamService.EXTRA_DREAM_COMPONENT,
- ComponentName.class);
return mDreamOverlay.asBinder();
}
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 3b7698e3..3c1fef0 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -214,19 +214,6 @@
private static final String DREAM_META_DATA_ROOT_TAG = "dream";
/**
- * Extra containing a boolean for whether to show complications on the overlay.
- * @hide
- */
- public static final String EXTRA_SHOW_COMPLICATIONS =
- "android.service.dreams.SHOW_COMPLICATIONS";
-
- /**
- * Extra containing the component name for the active dream.
- * @hide
- */
- public static final String EXTRA_DREAM_COMPONENT = "android.service.dreams.DREAM_COMPONENT";
-
- /**
* The default value for whether to show complications on the overlay.
*
* @hide
@@ -252,6 +239,9 @@
private boolean mDebug = false;
+ private ComponentName mDreamComponent;
+ private boolean mShouldShowComplications;
+
private DreamServiceWrapper mDreamServiceWrapper;
private Runnable mDispatchAfterOnAttachedToWindow;
@@ -947,6 +937,11 @@
@Override
public void onCreate() {
if (mDebug) Slog.v(mTag, "onCreate()");
+
+ mDreamComponent = new ComponentName(this, getClass());
+ mShouldShowComplications = fetchShouldShowComplications(this /*context*/,
+ fetchServiceInfo(this /*context*/, mDreamComponent));
+
super.onCreate();
}
@@ -994,14 +989,7 @@
// Connect to the overlay service if present.
if (!mWindowless && overlayComponent != null) {
final Resources resources = getResources();
- final ComponentName dreamService = new ComponentName(this, getClass());
-
- final ServiceInfo serviceInfo = fetchServiceInfo(this, dreamService);
- final Intent overlayIntent = new Intent()
- .setComponent(overlayComponent)
- .putExtra(EXTRA_SHOW_COMPLICATIONS,
- fetchShouldShowComplications(this, serviceInfo))
- .putExtra(EXTRA_DREAM_COMPONENT, dreamService);
+ final Intent overlayIntent = new Intent().setComponent(overlayComponent);
mOverlayConnection = new OverlayConnection(
/* context= */ this,
@@ -1364,7 +1352,9 @@
// parameters once the window has been attached.
mDreamStartOverlayConsumer = overlay -> {
try {
- overlay.startDream(mWindow.getAttributes(), mOverlayCallback);
+ overlay.startDream(mWindow.getAttributes(), mOverlayCallback,
+ mDreamComponent.flattenToString(),
+ mShouldShowComplications);
} catch (RemoteException e) {
Log.e(mTag, "could not send window attributes:" + e);
}
diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl
index 2b6633d..05ebbfe 100644
--- a/core/java/android/service/dreams/IDreamOverlay.aidl
+++ b/core/java/android/service/dreams/IDreamOverlay.aidl
@@ -31,7 +31,11 @@
* @param params The {@link LayoutParams} for the associated DreamWindow, including the window
token of the Dream Activity.
* @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
- * dream.
+ * dream.
+ * @param dreamComponent The component name of the dream service requesting overlay.
+ * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
+ * and weather.
*/
- void startDream(in LayoutParams params, in IDreamOverlayCallback callback);
+ void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
+ in String dreamComponent, in boolean shouldShowComplications);
}
diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java
index a2ca5a3..5d3852b 100644
--- a/core/java/android/service/voice/AbstractHotwordDetector.java
+++ b/core/java/android/service/voice/AbstractHotwordDetector.java
@@ -126,21 +126,26 @@
Slog.d(TAG, "updateState()");
}
throwIfDetectorIsNoLongerActive();
- synchronized (mLock) {
- updateStateLocked(options, sharedMemory, null /* callback */, mDetectorType);
+ try {
+ mManagerService.updateState(options, sharedMemory);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
- protected void updateStateLocked(@Nullable PersistableBundle options,
- @Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback,
+ protected void initAndVerifyDetector(
+ @Nullable PersistableBundle options,
+ @Nullable SharedMemory sharedMemory,
+ @NonNull IHotwordRecognitionStatusCallback callback,
int detectorType) {
if (DEBUG) {
- Slog.d(TAG, "updateStateLocked()");
+ Slog.d(TAG, "initAndVerifyDetector()");
}
Identity identity = new Identity();
identity.packageName = ActivityThread.currentOpPackageName();
try {
- mManagerService.updateState(identity, options, sharedMemory, callback, detectorType);
+ mManagerService.initAndVerifyDetector(identity, options, sharedMemory, callback,
+ detectorType);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index d01e7fe..d58f6d3 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -817,10 +817,8 @@
@Override
void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
- // TODO: transition to use an API that is not updateState to provide
- // onHotwordDetectionServiceInitialized status to external callback
if (mSupportHotwordDetectionService) {
- updateStateLocked(options, sharedMemory, mInternalCallback,
+ initAndVerifyDetector(options, sharedMemory, mInternalCallback,
DETECTOR_TYPE_TRUSTED_HOTWORD_DSP);
}
try {
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index 02561c94..11688df 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -69,9 +69,7 @@
@Override
void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
- // TODO: transition to use an API that is not updateState to provide
- // onHotwordDetectionServiceInitialized status to external callback
- updateStateLocked(options, sharedMemory,
+ initAndVerifyDetector(options, sharedMemory,
new InitializationStateListener(mHandler, mCallback),
DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 5c899e4..7b6ebf7 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -15905,15 +15905,16 @@
}
/**
- * Returns whether the device is currently in touch mode. Touch mode is entered
- * once the user begins interacting with the device by touch, and affects various
- * things like whether focus is always visible to the user.
+ * Returns the touch mode state associated with this view.
*
- * If this view has no {@link ViewRootImpl} or {@link Display} attached, then it will return
- * the default touch mode value defined in
+ * Attached views return the touch mode state from the associated window's display.
+ * Detached views just return the default touch mode value defined in
* {@code com.android.internal.R.bool.config_defaultInTouchMode}.
*
- * @return Whether the device is in touch mode.
+ * Touch mode is entered once the user begins interacting with the device by touch, and
+ * affects various things like whether focus highlight is always visible to the user.
+ *
+ * @return the touch mode state associated with this view
*/
@ViewDebug.ExportedProperty
public boolean isInTouchMode() {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index aa3aefd..8a4d451 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -2439,7 +2439,11 @@
* Subclasses override this to specify a default movement method.
*/
protected MovementMethod getDefaultMovementMethod() {
- return null;
+ if (isFocusable() || isFocusableInTouchMode()) {
+ return ArrowKeyMovementMethod.getInstance();
+ } else {
+ return null;
+ }
}
/**
@@ -2654,9 +2658,12 @@
/**
* Gets the {@link android.text.method.MovementMethod} being used for this TextView,
* which provides positioning, scrolling, and text selection functionality.
- * This will frequently be null for non-EditText TextViews.
+ * By default, this returns an instance of {@link android.text.method.ArrowKeyMovementMethod}
+ * if this View is focusable or focusable in touch mode.
+ *
* @return the movement method being used for this TextView.
* @see android.text.method.MovementMethod
+ * @see android.text.method.ArrowKeyMovementMethod
*/
public final MovementMethod getMovementMethod() {
return mMovement;
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index fbdd325..1bc8e6d 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -407,6 +407,7 @@
public static final class Change implements Parcelable {
private final WindowContainerToken mContainer;
private WindowContainerToken mParent;
+ private WindowContainerToken mLastParent;
private final SurfaceControl mLeash;
private @TransitionMode int mMode = TRANSIT_NONE;
private @ChangeFlags int mFlags = FLAG_NONE;
@@ -435,6 +436,7 @@
private Change(Parcel in) {
mContainer = in.readTypedObject(WindowContainerToken.CREATOR);
mParent = in.readTypedObject(WindowContainerToken.CREATOR);
+ mLastParent = in.readTypedObject(WindowContainerToken.CREATOR);
mLeash = new SurfaceControl();
mLeash.readFromParcel(in);
mMode = in.readInt();
@@ -458,6 +460,14 @@
mParent = parent;
}
+ /**
+ * Sets the parent of this change's container before the transition if this change's
+ * container is reparented in the transition.
+ */
+ public void setLastParent(@Nullable WindowContainerToken lastParent) {
+ mLastParent = lastParent;
+ }
+
/** Sets the transition mode for this change */
public void setMode(@TransitionMode int mode) {
mMode = mode;
@@ -541,6 +551,17 @@
return mParent;
}
+ /**
+ * @return the parent of the changing container before the transition if it is reparented
+ * in the transition. The parent window may not be collected in the transition as a
+ * participant, and it may have been detached from the display. {@code null} if the changing
+ * container has not been reparented in the transition, or if the parent is not organizable.
+ */
+ @Nullable
+ public WindowContainerToken getLastParent() {
+ return mLastParent;
+ }
+
/** @return which action this change represents. */
public @TransitionMode int getMode() {
return mMode;
@@ -640,6 +661,7 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedObject(mContainer, flags);
dest.writeTypedObject(mParent, flags);
+ dest.writeTypedObject(mLastParent, flags);
mLeash.writeToParcel(dest, flags);
dest.writeInt(mMode);
dest.writeInt(mFlags);
@@ -685,6 +707,7 @@
+ mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation
+ " endFixedRotation=" + mEndFixedRotation;
if (mSnapshot != null) out += " snapshot=" + mSnapshot;
+ if (mLastParent != null) out += " lastParent=" + mLastParent;
return out + "}";
}
}
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index bbcf982..9f23f24 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -245,6 +245,23 @@
/**
* Set configuration and pass read-only data to hotword detection service.
+ *
+ * @param options Application configuration data to provide to the
+ * {@link HotwordDetectionService}. PersistableBundle does not allow any remotable objects or
+ * other contents that can be used to communicate with other processes.
+ * @param sharedMemory The unrestricted data blob to provide to the
+ * {@link HotwordDetectionService}. Use this to provide the hotword models data or other
+ * such data to the trusted process.
+ */
+ @EnforcePermission("MANAGE_HOTWORD_DETECTION")
+ void updateState(
+ in PersistableBundle options,
+ in SharedMemory sharedMemory);
+
+ /**
+ * Set configuration and pass read-only data to hotword detection service when creating
+ * the detector.
+ *
* Caller must provide an identity, used for permission tracking purposes.
* The uid/pid elements of the identity will be ignored by the server and replaced with the ones
* provided by binder.
@@ -259,7 +276,7 @@
* @param detectorType Indicate which detector is used.
*/
@EnforcePermission("MANAGE_HOTWORD_DETECTION")
- void updateState(
+ void initAndVerifyDetector(
in Identity originatorIdentity,
in PersistableBundle options,
in SharedMemory sharedMemory,
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 2fbf803..153e499 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1626,6 +1626,7 @@
<java-symbol type="xml" name="password_kbd_symbols_shift" />
<java-symbol type="xml" name="power_profile" />
<java-symbol type="xml" name="power_profile_test" />
+ <java-symbol type="xml" name="irq_device_map" />
<java-symbol type="xml" name="sms_short_codes" />
<java-symbol type="xml" name="audio_assets" />
<java-symbol type="xml" name="global_keys" />
@@ -4015,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/res/res/xml/irq_device_map.xml b/core/res/res/xml/irq_device_map.xml
new file mode 100644
index 0000000..86a44d6
--- /dev/null
+++ b/core/res/res/xml/irq_device_map.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<irq-device-map>
+ <!-- This file maps devices (chips) that can send IRQs to the CPU (and bring it out of sleep) to
+ logical subsystems in userspace code. Since each Android device has its own uniquely
+ designed chipset, this mapping is expected to be empty by default and should be overridden
+ by device specific configs.
+ This mapping helps the system to meaningfully attribute CPU wakeups to logical work that
+ happened on the device. The devices are referred to by their names as defined in the kernel.
+ Currently defined subsystems are:
+ - Alarm: Use this to denote wakeup alarms requested by apps via the AlarmManager API.
+
+ The overlay should use tags <device> and <subsystem> to describe this mapping in the
+ following way:
+
+ <irq-device-map>
+ <device name="device_name_1">
+ <subsystem>Subsystem1</subsystem>
+ <subsystem>Subsystem2</subsystem>
+ :
+ :
+ </device>
+ <device name="device_name_2">
+ :
+ </device>
+ :
+ </irq-device-map>
+
+ The tag <device> should have a "name" attribute specifying the kernel name of the device.
+ Each <device> tag can then enclose multiple <subsystem> tags. Each <subsystem> tag should
+ enclose the name of the logical subsystems (one of the ones defined above) as text.
+ Undefined subsystem names will be ignored by the framework.
+ -->
+</irq-device-map>
\ No newline at end of file
diff --git a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java
index 6bd498c..d633843 100644
--- a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java
+++ b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java
@@ -124,7 +124,7 @@
assertTransportBehavior(input, expected);
}
- public void testMutiplePingPing() {
+ public void testMultiplePingPing() {
final byte[] input = concat(
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, "red"),
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
@@ -248,7 +248,7 @@
}
@Override
- public int read(byte b[], int off, int len) throws IOException {
+ public int read(byte[] b, int off, int len) throws IOException {
// Instead of hanging indefinitely, wait a bit and claim that
// nothing was read, without hitting EOF
SystemClock.sleep(100);
@@ -259,13 +259,13 @@
private static class DelayingInputStream extends FilterInputStream {
private final long mDelay;
- public DelayingInputStream(InputStream in, long delay) {
+ DelayingInputStream(InputStream in, long delay) {
super(in);
mDelay = delay;
}
@Override
- public int read(byte b[], int off, int len) throws IOException {
+ public int read(byte[] b, int off, int len) throws IOException {
SystemClock.sleep(mDelay);
return super.read(b, off, len);
}
@@ -274,25 +274,25 @@
private static class DelayingOutputStream extends FilterOutputStream {
private final long mDelay;
- public DelayingOutputStream(OutputStream out, long delay) {
+ DelayingOutputStream(OutputStream out, long delay) {
super(out);
mDelay = delay;
}
@Override
- public void write(byte b[], int off, int len) throws IOException {
+ public void write(byte[] b, int off, int len) throws IOException {
SystemClock.sleep(mDelay);
super.write(b, off, len);
}
}
private static class TrickleInputStream extends FilterInputStream {
- public TrickleInputStream(InputStream in) {
+ TrickleInputStream(InputStream in) {
super(in);
}
@Override
- public int read(byte b[], int off, int len) throws IOException {
+ public int read(byte[] b, int off, int len) throws IOException {
return super.read(b, off, 1);
}
}
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index e154586..f6216fa 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1654,6 +1654,12 @@
android:exported="true">
</activity>
+ <activity android:name="android.app.activity.ActivityTransitionDrawableTest$TestActivity"
+ android:exported="true"
+ android:enabled="true"
+ android:theme="@style/Theme">
+ </activity>
+
<activity
android:name="android.os.TestVrActivity"
android:enableVrMode="com.android.frameworks.coretests/android.os.TestVrActivity$TestVrListenerService">
diff --git a/core/tests/coretests/src/android/app/activity/ActivityTransitionDrawableTest.java b/core/tests/coretests/src/android/app/activity/ActivityTransitionDrawableTest.java
new file mode 100644
index 0000000..2c4e443
--- /dev/null
+++ b/core/tests/coretests/src/android/app/activity/ActivityTransitionDrawableTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 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.app.activity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.content.Intent;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.transition.Fade;
+import android.view.View;
+import android.view.Window;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test for verifying Activity Transitions Drawable behavior
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+@Presubmit
+public class ActivityTransitionDrawableTest {
+ private static final String LAUNCH_ON_START = "launch on start";
+
+ @Rule
+ public final ActivityTestRule<TestActivity> mActivityTestRule =
+ new ActivityTestRule<>(TestActivity.class, true);
+
+ @Test
+ public void stopTransitionDrawableAlphaRestored() throws Throwable {
+ mActivityTestRule.runOnUiThread(() -> {
+ Activity activity = mActivityTestRule.getActivity();
+ Intent intent = new Intent(activity, TestActivity.class);
+ intent.putExtra(LAUNCH_ON_START, true);
+ Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle();
+ activity.startActivity(intent, bundle);
+ });
+
+ assertThat(TestActivity.activityAdded.await(5, TimeUnit.SECONDS)).isTrue();
+ TestActivity topActivity = TestActivity.sInstances.get(2);
+ TestActivity middleActivity = TestActivity.sInstances.get(1);
+ assertThat(topActivity.startedLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(middleActivity.stoppedLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ mActivityTestRule.runOnUiThread(() -> {
+ assertThat(middleActivity.getWindow().getDecorView().getBackground().getAlpha())
+ .isEqualTo(255);
+ });
+ }
+
+ public static class TestActivity extends Activity {
+ public static final ArrayList<TestActivity> sInstances = new ArrayList<TestActivity>();
+ public static CountDownLatch activityAdded = new CountDownLatch(3);
+
+ private boolean mLaunchOnStart = false;
+ public CountDownLatch startedLatch = new CountDownLatch(1);
+ public CountDownLatch stoppedLatch = new CountDownLatch(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
+ getWindow().setAllowEnterTransitionOverlap(false);
+ setContentView(new View(this));
+ Fade longFade = new Fade();
+ longFade.setDuration(2000);
+ getWindow().setEnterTransition(longFade);
+ getWindow().setExitTransition(longFade);
+ super.onCreate(savedInstanceState);
+ mLaunchOnStart = getIntent().getBooleanExtra(LAUNCH_ON_START, false);
+ sInstances.add(this);
+ activityAdded.countDown();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (mLaunchOnStart) {
+ mLaunchOnStart = false;
+ Intent intent = new Intent(this, TestActivity.class);
+ startActivity(intent);
+ }
+ startedLatch.countDown();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ stoppedLatch.countDown();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ sInstances.remove(this);
+ }
+ }
+}
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/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 31a9592..1174b68 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -94,7 +94,7 @@
ActivityEmbeddingComponent {
static final String TAG = "SplitController";
static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
@VisibleForTesting
@GuardedBy("mLock")
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 0fb6ff8..b516e140 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -117,9 +117,7 @@
if (mWindowLayoutChangeListeners.containsKey(context)
// In theory this method can be called on the same consumer with different context.
|| mWindowLayoutChangeListeners.containsValue(consumer)) {
- throw new IllegalArgumentException(
- "Context or Consumer has already been registered for WindowLayoutInfo"
- + " callback.");
+ return;
}
if (!context.isUiContext()) {
throw new IllegalArgumentException("Context must be a UI Context, which should be"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 33761d2..2b36b4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -452,14 +452,17 @@
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) {
TransitionInfo.Change pipChange = pipTaskChange;
- if (pipChange == null) {
+ if (mCurrentPipTaskToken == null) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
+ } else if (pipChange == null) {
// The pipTaskChange is null, this can happen if we are reparenting the PIP activity
// back to its original Task. In that case, we should animate the activity leash
- // instead, which should be the only non-task, independent, TRANSIT_CHANGE window.
+ // instead, which should be the change whose last parent is the recorded PiP Task.
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() == null && change.getMode() == TRANSIT_CHANGE
- && TransitionInfo.isIndependent(change, info)) {
+ if (mCurrentPipTaskToken.equals(change.getLastParent())) {
+ // Find the activity that is exiting PiP.
pipChange = change;
break;
}
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 1372f4d..aaaccd8 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
@@ -81,7 +81,7 @@
/** Set to {@code true} to enable shell transitions. */
public static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS
&& SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 1a1bebd..5ee8bf3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -61,7 +61,7 @@
@RunWith(AndroidJUnit4.class)
public final class StageTaskListenerTests extends ShellTestCase {
private static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
@Mock
private ShellTaskOrganizer mTaskOrganizer;
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 5e8a623..46fbe7c 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -1020,6 +1020,40 @@
return base::unexpected(std::nullopt);
}
+template <typename TChar, typename SP>
+base::expected<size_t, NullOrIOError> ResStringPool::stringIndex(
+ SP sp, std::unordered_map<SP, size_t>& map) const
+{
+ AutoMutex lock(mStringIndexLock);
+
+ if (map.empty()) {
+ // build string index on the first call
+ for (size_t i = 0; i < mHeader->stringCount; i++) {
+ base::expected<SP, NullOrIOError> s;
+ if constexpr(std::is_same_v<TChar, char16_t>) {
+ s = stringAt(i);
+ } else {
+ s = string8At(i);
+ }
+ if (s.has_value()) {
+ const auto r = map.insert({*s, i});
+ if (!r.second) {
+ ALOGE("failed to build string index, string id=%zu\n", i);
+ }
+ } else {
+ return base::unexpected(s.error());
+ }
+ }
+ }
+
+ if (!map.empty()) {
+ const auto result = map.find(sp);
+ if (result != map.end())
+ return result->second;
+ }
+ return base::unexpected(std::nullopt);
+}
+
base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_t* str,
size_t strLen) const
{
@@ -1027,134 +1061,28 @@
return base::unexpected(std::nullopt);
}
- if ((mHeader->flags&ResStringPool_header::UTF8_FLAG) != 0) {
- if (kDebugStringPoolNoisy) {
- ALOGI("indexOfString UTF-8: %s", String8(str, strLen).string());
- }
+ if (kDebugStringPoolNoisy) {
+ ALOGI("indexOfString (%s): %s", isUTF8() ? "UTF-8" : "UTF-16",
+ String8(str, strLen).string());
+ }
- // The string pool contains UTF 8 strings; we don't want to cause
- // temporary UTF-16 strings to be created as we search.
- if (mHeader->flags&ResStringPool_header::SORTED_FLAG) {
- // Do a binary search for the string... this is a little tricky,
- // because the strings are sorted with strzcmp16(). So to match
- // the ordering, we need to convert strings in the pool to UTF-16.
- // But we don't want to hit the cache, so instead we will have a
- // local temporary allocation for the conversions.
- size_t convBufferLen = strLen + 4;
- std::vector<char16_t> convBuffer(convBufferLen);
- ssize_t l = 0;
- ssize_t h = mHeader->stringCount-1;
-
- ssize_t mid;
- while (l <= h) {
- mid = l + (h - l)/2;
- int c = -1;
- const base::expected<StringPiece, NullOrIOError> s = string8At(mid);
- if (UNLIKELY(IsIOError(s))) {
- return base::unexpected(s.error());
- }
- if (s.has_value()) {
- char16_t* end = utf8_to_utf16(reinterpret_cast<const uint8_t*>(s->data()),
- s->size(), convBuffer.data(), convBufferLen);
- c = strzcmp16(convBuffer.data(), end-convBuffer.data(), str, strLen);
- }
- if (kDebugStringPoolNoisy) {
- ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n",
- s->data(), c, (int)l, (int)mid, (int)h);
- }
- if (c == 0) {
- if (kDebugStringPoolNoisy) {
- ALOGI("MATCH!");
- }
- return mid;
- } else if (c < 0) {
- l = mid + 1;
- } else {
- h = mid - 1;
- }
- }
- } else {
- // It is unusual to get the ID from an unsorted string block...
- // most often this happens because we want to get IDs for style
- // span tags; since those always appear at the end of the string
- // block, start searching at the back.
- String8 str8(str, strLen);
- const size_t str8Len = str8.size();
- for (int i=mHeader->stringCount-1; i>=0; i--) {
- const base::expected<StringPiece, NullOrIOError> s = string8At(i);
- if (UNLIKELY(IsIOError(s))) {
- return base::unexpected(s.error());
- }
- if (s.has_value()) {
- if (kDebugStringPoolNoisy) {
- ALOGI("Looking at %s, i=%d\n", s->data(), i);
- }
- if (str8Len == s->size()
- && memcmp(s->data(), str8.string(), str8Len) == 0) {
- if (kDebugStringPoolNoisy) {
- ALOGI("MATCH!");
- }
- return i;
- }
- }
- }
- }
-
+ base::expected<size_t, NullOrIOError> idx;
+ if (isUTF8()) {
+ auto str8 = String8(str, strLen);
+ idx = stringIndex<char>(StringPiece(str8.c_str(), str8.size()), mStringIndex8);
} else {
+ idx = stringIndex<char16_t>(StringPiece16(str, strLen), mStringIndex16);
+ }
+
+ if (UNLIKELY(!idx.has_value())) {
+ return base::unexpected(idx.error());
+ }
+
+ if (*idx < mHeader->stringCount) {
if (kDebugStringPoolNoisy) {
- ALOGI("indexOfString UTF-16: %s", String8(str, strLen).string());
+ ALOGI("MATCH! (idx=%zu)", *idx);
}
-
- if (mHeader->flags&ResStringPool_header::SORTED_FLAG) {
- // Do a binary search for the string...
- ssize_t l = 0;
- ssize_t h = mHeader->stringCount-1;
-
- ssize_t mid;
- while (l <= h) {
- mid = l + (h - l)/2;
- const base::expected<StringPiece16, NullOrIOError> s = stringAt(mid);
- if (UNLIKELY(IsIOError(s))) {
- return base::unexpected(s.error());
- }
- int c = s.has_value() ? strzcmp16(s->data(), s->size(), str, strLen) : -1;
- if (kDebugStringPoolNoisy) {
- ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n",
- String8(s->data(), s->size()).string(), c, (int)l, (int)mid, (int)h);
- }
- if (c == 0) {
- if (kDebugStringPoolNoisy) {
- ALOGI("MATCH!");
- }
- return mid;
- } else if (c < 0) {
- l = mid + 1;
- } else {
- h = mid - 1;
- }
- }
- } else {
- // It is unusual to get the ID from an unsorted string block...
- // most often this happens because we want to get IDs for style
- // span tags; since those always appear at the end of the string
- // block, start searching at the back.
- for (int i=mHeader->stringCount-1; i>=0; i--) {
- const base::expected<StringPiece16, NullOrIOError> s = stringAt(i);
- if (UNLIKELY(IsIOError(s))) {
- return base::unexpected(s.error());
- }
- if (kDebugStringPoolNoisy) {
- ALOGI("Looking at %s, i=%d\n", String8(s->data(), s->size()).string(), i);
- }
- if (s.has_value() && strLen == s->size() &&
- strzcmp16(s->data(), s->size(), str, strLen) == 0) {
- if (kDebugStringPoolNoisy) {
- ALOGI("MATCH!");
- }
- return i;
- }
- }
- }
+ return *idx;
}
return base::unexpected(std::nullopt);
}
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 9309091..24628cd 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -41,6 +41,7 @@
#include <array>
#include <map>
#include <memory>
+#include <unordered_map>
namespace android {
@@ -562,8 +563,17 @@
incfs::map_ptr<uint32_t> mStyles;
uint32_t mStylePoolSize; // number of uint32_t
+ // mStringIndex is used to quickly map a string to its ID
+ mutable Mutex mStringIndexLock;
+ mutable std::unordered_map<StringPiece, size_t> mStringIndex8;
+ mutable std::unordered_map<StringPiece16, size_t> mStringIndex16;
+
base::expected<StringPiece, NullOrIOError> stringDecodeAt(
size_t idx, incfs::map_ptr<uint8_t> str, size_t encLen) const;
+
+ template <typename TChar, typename SP=BasicStringPiece<TChar>>
+ base::expected<size_t, NullOrIOError> stringIndex(
+ SP str, std::unordered_map<SP, size_t>& map) const;
};
/**
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 50aa6fe..6ae7dfb 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -44,6 +44,7 @@
void onRequestStreamVolume(int seq);
void onRequestTrackInfoList(int seq);
void onRequestCurrentTvInputId(int seq);
+ void onRequestStartRecording(in Uri programUri, int seq);
void onRequestSigning(
in String id, in String algorithm, in String alias, in byte[] data, int seq);
void onAdRequest(in AdRequest request, int Seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index c8c695f..84b9c9e 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -63,6 +63,7 @@
void notifyContentAllowed(in IBinder sessionToken, int userId);
void notifyContentBlocked(in IBinder sessionToken, in String rating, int userId);
void notifySignalStrength(in IBinder sessionToken, int stength, int userId);
+ void notifyRecordingStarted(in IBinder sessionToken, in String recordingId, int userId);
void setSurface(in IBinder sessionToken, in Surface surface, int userId);
void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 818c287..95b4ffa 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -53,6 +53,7 @@
void notifyContentAllowed();
void notifyContentBlocked(in String rating);
void notifySignalStrength(int strength);
+ void notifyRecordingStarted(in String recordingId);
void setSurface(in Surface surface);
void dispatchSurfaceChanged(int format, int width, int height);
void notifyBroadcastInfoResponse(in BroadcastInfoResponse response);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 32b08b7..6478057 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -43,6 +43,7 @@
void onRequestStreamVolume();
void onRequestTrackInfoList();
void onRequestCurrentTvInputId();
+ void onRequestStartRecording(in Uri programUri);
void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
void onAdRequest(in AdRequest request);
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index a72f34c..042cb15 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -81,6 +81,7 @@
private static final int DO_CREATE_MEDIA_VIEW = 27;
private static final int DO_RELAYOUT_MEDIA_VIEW = 28;
private static final int DO_REMOVE_MEDIA_VIEW = 29;
+ private static final int DO_NOTIFY_RECORDING_STARTED = 30;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -164,6 +165,10 @@
mSessionImpl.sendCurrentTvInputId((String) msg.obj);
break;
}
+ case DO_NOTIFY_RECORDING_STARTED: {
+ mSessionImpl.notifyRecordingStarted((String) msg.obj);
+ break;
+ }
case DO_SEND_SIGNING_RESULT: {
SomeArgs args = (SomeArgs) msg.obj;
mSessionImpl.sendSigningResult((String) args.arg1, (byte[]) args.arg2);
@@ -381,6 +386,12 @@
}
@Override
+ public void notifyRecordingStarted(String recordingId) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageO(
+ DO_NOTIFY_RECORDING_STARTED, recordingId));
+ }
+
+ @Override
public void setSurface(Surface surface) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface));
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index c0261f2..a27fd10 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -487,6 +487,18 @@
}
@Override
+ public void onRequestStartRecording(Uri programUri, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestStartRecording(programUri);
+ }
+ }
+
+ @Override
public void onRequestSigning(
String id, String algorithm, String alias, byte[] data, int seq) {
synchronized (mSessionCallbackRecordMap) {
@@ -1035,6 +1047,18 @@
}
}
+ void notifyRecordingStarted(@Nullable String recordingId) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyRecordingStarted(mToken, recordingId, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -1696,6 +1720,15 @@
});
}
+ void postRequestStartRecording(Uri programUri) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestStartRecording(mSession, programUri);
+ }
+ });
+ }
+
void postRequestSigning(String id, String algorithm, String alias, byte[] data) {
mHandler.post(new Runnable() {
@Override
@@ -1847,6 +1880,15 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#RequestStartRecording} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+ */
+ public void onRequestStartRecording(Session session, Uri programUri) {
+ }
+
+ /**
* This is called when
* {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is
* called.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index f5b4322..3d65effa 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -455,6 +455,13 @@
}
/**
+ * Receives started recording's ID.
+ * @hide
+ */
+ public void onRecordingStarted(@Nullable String recordingId) {
+ }
+
+ /**
* Receives signing result.
* @param signingId the ID to identify the request. It's the same as the corresponding ID in
* {@link Session#requestSigning(String, String, String, byte[])}
@@ -905,6 +912,27 @@
}
/**
+ * Requests starting of recording
+ *
+ * @hide
+ */
+ @CallSuper
+ public void requestStartRecording(@Nullable Uri programUri) {
+ executeOrPostRunnableOnMainThread(() -> {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestStartRecording");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestStartRecording(programUri);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestStartRecording", e);
+ }
+ });
+ }
+
+ /**
* Requests signing of the given data.
*
* <p>This is used when the corresponding server of the broadcast-independent interactive
@@ -1114,6 +1142,10 @@
onAdResponse(response);
}
+ void notifyRecordingStarted(String recordingId) {
+ onRecordingStarted(recordingId);
+ }
+
/**
* Notifies when the session state is changed.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 53b0324..76ba69c 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -581,6 +581,22 @@
}
/**
+ * Alerts the TV interactive app that a recording has been started with recordingId
+ *
+ * @param recordingId The Id of the recording started
+ *
+ * @hide
+ */
+ public void notifyRecordingStarted(@Nullable String recordingId) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyRecordingStarted");
+ }
+ if (mSession != null) {
+ mSession.notifyRecordingStarted(recordingId);
+ }
+ }
+
+ /**
* Sends signing result to related TV interactive app.
*
* <p>This is used when the corresponding server of the broadcast-independent interactive
@@ -841,6 +857,19 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#requestStartRecording(Uri)}
+ * is called.
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @param programUri The program URI to record
+ *
+ * @hide
+ */
+ public void onRequestStartRecording(
+ @NonNull String iAppServiceId,
+ @Nullable Uri programUri) {
+ }
+
+ /**
* This is called when
* {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is
* called.
@@ -1164,6 +1193,20 @@
}
@Override
+ public void onRequestStartRecording(Session session, Uri programUri) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestStartRecording");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestStartRecording - session not created");
+ return;
+ }
+ if (mCallback != null) {
+ mCallback.onRequestStartRecording(mIAppServiceId, programUri);
+ }
+ }
+
+ @Override
public void onRequestSigning(
Session session, String id, String algorithm, String alias, byte[] data) {
if (DEBUG) {
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/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 2901705..2c24bf1 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -3,6 +3,7 @@
<string name="string_cancel">Cancel</string>
<string name="string_continue">Continue</string>
<string name="string_more_options">More options</string>
+ <string name="string_create_at_another_place">Create at another place</string>
<string name="string_no_thanks">No thanks</string>
<string name="passkey_creation_intro_title">A simple way to sign in safely</string>
<string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>
@@ -10,4 +11,5 @@
<string name="choose_provider_body">This provider will store passkeys and passwords for you and help you easily autofill and sign in. Learn more</string>
<string name="choose_create_option_title">Create a passkey at</string>
<string name="choose_sign_in_title">Use saved sign in</string>
+ <string name="create_passkey_at">Create passkey at</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 5918633..46bf19c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -1,6 +1,14 @@
package com.android.credentialmanager
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
import android.content.Context
+import android.content.Intent
+import android.credentials.ui.Entry
+import android.credentials.ui.ProviderData
+import android.credentials.ui.RequestInfo
+import android.graphics.drawable.Icon
+import android.os.Binder
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.createflow.CreatePasskeyUiState
import com.android.credentialmanager.createflow.CreateScreenState
@@ -11,8 +19,82 @@
// Consider repo per screen, similar to view model?
class CredentialManagerRepo(
- private val context: Context
+ private val context: Context,
+ intent: Intent,
) {
+ private val requestInfo: RequestInfo
+ private val providerList: List<ProviderData>
+
+ init {
+ requestInfo = intent.extras?.getParcelable(
+ RequestInfo.EXTRA_REQUEST_INFO,
+ RequestInfo::class.java
+ ) ?: RequestInfo(
+ Binder(),
+ RequestInfo.TYPE_CREATE,
+ /*isFirstUsage=*/false
+ )
+
+ providerList = intent.extras?.getParcelableArrayList(
+ ProviderData.EXTRA_PROVIDER_DATA_LIST,
+ ProviderData::class.java
+ ) ?: testProviderList()
+ }
+
+ private fun testProviderList(): List<ProviderData> {
+ return listOf(
+ ProviderData(
+ "com.google",
+ listOf<Entry>(
+ newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett"),
+ newEntry(2, "elisa.work@google.com", "Elisa Backett Work"),
+ ),
+ listOf<Entry>(
+ newEntry(3, "Go to Settings", ""),
+ newEntry(4, "Switch Account", ""),
+ ),
+ null
+ ),
+ ProviderData(
+ "com.dashlane",
+ listOf<Entry>(
+ newEntry(5, "elisa.beckett@dashlane.com", "Elisa Backett"),
+ newEntry(6, "elisa.work@dashlane.com", "Elisa Backett Work"),
+ ),
+ listOf<Entry>(
+ newEntry(7, "Manage Accounts", "Manage your accounts in the dashlane app"),
+ ),
+ null
+ ),
+ ProviderData(
+ "com.lastpass",
+ listOf<Entry>(
+ newEntry(8, "elisa.beckett@lastpass.com", "Elisa Backett"),
+ ),
+ listOf<Entry>(),
+ null
+ )
+
+ )
+ }
+
+ private fun newEntry(id: Int, title: String, subtitle: String): Entry {
+ val slice = Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1)
+ )
+ .addText(title, null, listOf(Entry.HINT_TITLE))
+ .addText(subtitle, null, listOf(Entry.HINT_SUBTITLE))
+ .addIcon(
+ Icon.createWithResource(context, R.drawable.ic_passkey),
+ null,
+ listOf(Entry.HINT_ICON))
+ .build()
+ return Entry(
+ id,
+ slice
+ )
+ }
+
private fun getCredentialProviderList():
List<com.android.credentialmanager.getflow.ProviderInfo> {
return listOf(
@@ -140,8 +222,11 @@
companion object {
lateinit var repo: CredentialManagerRepo
- fun setup(context: Context) {
- repo = CredentialManagerRepo(context)
+ fun setup(
+ context: Context,
+ intent: Intent,
+ ) {
+ repo = CredentialManagerRepo(context, intent)
}
fun getInstance(): CredentialManagerRepo {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 5cd6a13..98c824c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -15,7 +15,7 @@
class CredentialSelectorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- CredentialManagerRepo.setup(this)
+ CredentialManagerRepo.setup(this, intent)
val startDestination = intent.extras?.getString(
"start_destination",
"CREATE_PASSKEY"
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 5aa1e9b..044688b 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -22,4 +22,5 @@
PASSKEY_INTRO,
PROVIDER_SELECTION,
CREATION_OPTION_SELECTION,
+ MORE_OPTIONS_SELECTION,
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index fbec1bc..997519d 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -19,9 +19,13 @@
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -71,8 +75,13 @@
onOptionSelected = {viewModel.onCreateOptionSelected(it)},
onCancel = cancelActivity,
multiProvider = uiState.providers.size > 1,
- onMoreOptionSelected = {viewModel.onMoreOptionSelected()}
+ onMoreOptionSelected = {viewModel.onMoreOptionSelected(it)}
)
+ CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionSelectionCard(
+ providerInfo = uiState.selectedProvider!!,
+ onCancel = cancelActivity,
+ onBackButtonSelected = {viewModel.onBackButtonSelected(it)}
+ )
}
},
scrimColor = Color.Transparent,
@@ -202,6 +211,67 @@
}
}
+@Composable
+fun MoreOptionSelectionCard(
+ providerInfo: ProviderInfo,
+ onCancel: () -> Unit,
+ onBackButtonSelected: (String) -> Unit
+) {
+ Card(
+ backgroundColor = lightBackgroundColor,
+ ) {
+ Column() {
+ TopAppBar(
+ title = { Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1) },
+ backgroundColor = lightBackgroundColor,
+ elevation = 0.dp,
+ navigationIcon =
+ {
+ IconButton(onClick = { onBackButtonSelected(providerInfo.name) }) {
+ Icon(Icons.Filled.ArrowBack, "backIcon"
+ )
+ }
+ }
+ )
+ Divider(
+ thickness = 24.dp,
+ color = Color.Transparent
+ )
+ Text(
+ text = stringResource(R.string.create_passkey_at),
+ style = Typography.body1,
+ modifier = Modifier.padding(horizontal = 28.dp)
+ )
+ Card(
+ shape = Shapes.medium,
+ modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .align(alignment = Alignment.CenterHorizontally)
+ ) {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ }
+ }
+ Divider(
+ thickness = 24.dp,
+ color = Color.Transparent
+ )
+ Row(
+ horizontalArrangement = Arrangement.Start,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+ ) {
+ CancelButton(stringResource(R.string.string_cancel), onCancel)
+ }
+ Divider(
+ thickness = 18.dp,
+ color = Color.Transparent,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ }
+}
+
@ExperimentalMaterialApi
@Composable
fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) {
@@ -276,7 +346,7 @@
onOptionSelected: (String) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
- onMoreOptionSelected: () -> Unit,
+ onMoreOptionSelected: (String) -> Unit,
) {
Card(
backgroundColor = lightBackgroundColor,
@@ -318,7 +388,7 @@
}
if (multiProvider) {
item {
- MoreOptionRow(onSelect = onMoreOptionSelected)
+ MoreOptionRow(onSelect = { onMoreOptionSelected(providerInfo.name) })
}
}
}
@@ -389,7 +459,7 @@
shape = Shapes.large
) {
Text(
- text = stringResource(R.string.string_more_options),
+ text = stringResource(R.string.string_create_at_another_place),
style = Typography.h6,
)
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index e42016d..15300de 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -52,7 +52,17 @@
}
}
- fun onMoreOptionSelected() {
- Log.d("Account Selector", "On more option selected")
+ fun onMoreOptionSelected(providerName: String) {
+ uiState = uiState.copy(
+ currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
+ selectedProvider = getProviderInfoByName(providerName)
+ )
+ }
+
+ fun onBackButtonSelected(providerName: String) {
+ uiState = uiState.copy(
+ currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+ selectedProvider = getProviderInfoByName(providerName)
+ )
}
}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 6669d6b..696ea4a 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -59,6 +59,10 @@
<action android:name="android.content.pm.action.CONFIRM_INSTALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
+ <intent-filter android:priority="1">
+ <action android:name="android.content.pm.action.CONFIRM_PRE_APPROVAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
</activity>
<activity android:name=".InstallStaging"
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index ac1a574f..bfab9be 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -59,7 +59,8 @@
String callingAttributionTag = null;
final boolean isSessionInstall =
- PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
+ PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
+ || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
// If the activity was started via a PackageInstaller session, we retrieve the calling
// package from that session
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 20dc2cb..de76632 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -39,9 +39,11 @@
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
+import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
@@ -151,7 +153,7 @@
}
/**
- * Replace any dialog shown by the dialog with the one for the given {@link #createDialog id}.
+ * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}.
*
* @param id The dialog type to add
*/
@@ -296,6 +298,14 @@
? RESULT_OK : RESULT_FIRST_USER, result);
}
+ private static PackageInfo generateStubPackageInfo(String packageName) {
+ final PackageInfo info = new PackageInfo();
+ final ApplicationInfo aInfo = new ApplicationInfo();
+ info.applicationInfo = aInfo;
+ info.packageName = info.applicationInfo.packageName = packageName;
+ return info;
+ }
+
@Override
protected void onCreate(Bundle icicle) {
if (mLocalLOGV) Log.i(TAG, "creating for user " + getUserId());
@@ -315,6 +325,7 @@
mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
final Intent intent = getIntent();
+ final String action = intent.getAction();
mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
@@ -324,11 +335,11 @@
mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
? getPackageNameForUid(mOriginatingUid) : null;
- final Uri packageUri;
-
- if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction())) {
- final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
- final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);
+ final Object packageSource;
+ if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) {
+ final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
+ -1 /* defaultValue */);
+ final SessionInfo info = mInstaller.getSessionInfo(sessionId);
if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
finish();
@@ -336,18 +347,32 @@
}
mSessionId = sessionId;
- packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath));
+ packageSource = Uri.fromFile(new File(info.resolvedBaseCodePath));
+ mOriginatingURI = null;
+ mReferrerURI = null;
+ } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) {
+ final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
+ -1 /* defaultValue */);
+ final SessionInfo info = mInstaller.getSessionInfo(sessionId);
+ if (info == null || !info.isPreapprovalRequested) {
+ Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
+ finish();
+ return;
+ }
+
+ mSessionId = sessionId;
+ packageSource = info;
mOriginatingURI = null;
mReferrerURI = null;
} else {
mSessionId = -1;
- packageUri = intent.getData();
+ packageSource = intent.getData();
mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
}
// if there's nothing to do, quietly slip into the ether
- if (packageUri == null) {
+ if (packageSource == null) {
Log.w(TAG, "Unspecified source");
setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
finish();
@@ -359,7 +384,7 @@
return;
}
- boolean wasSetUp = processPackageUri(packageUri);
+ final boolean wasSetUp = processAppSnippet(packageSource);
if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp);
if (!wasSetUp) {
@@ -619,6 +644,39 @@
return true;
}
+ /**
+ * Use the SessionInfo and set up the installer for pre-commit install session.
+ *
+ * @param info The SessionInfo to compose
+ *
+ * @return {@code true} iff the installer could be set up
+ */
+ private boolean processSessionInfo(@NonNull SessionInfo info) {
+ mPkgInfo = generateStubPackageInfo(info.appPackageName);
+ mAppSnippet = new PackageUtil.AppSnippet(info.appLabel,
+ info.appIcon != null ? new BitmapDrawable(getResources(), info.appIcon)
+ : getPackageManager().getDefaultActivityIcon());
+ return true;
+ }
+
+ /**
+ * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install
+ * session) to set up the installer for this install.
+ *
+ * @param source The source of package URI or SessionInfo
+ *
+ * @return {@code true} iff the installer could be set up
+ */
+ private boolean processAppSnippet(@NonNull Object source) {
+ if (source instanceof Uri) {
+ return processPackageUri((Uri) source);
+ } else if (source instanceof SessionInfo) {
+ return processSessionInfo((SessionInfo) source);
+ }
+
+ return false;
+ }
+
@Override
public void onBackPressed() {
if (mSessionId != -1) {
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/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
index fe337d267..8e33ca3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
@@ -106,13 +106,20 @@
+ "license content #1\n"
+ "</pre><!-- license-text -->\n"
+ "</td></tr><!-- same-license -->\n"
- + "</table></body></html>\n";
+ + "</table>\n"
+ + "<div class=\"path-counts\"><table>\n"
+ + " <tr><th>Path prefix</th><th>Count</th></tr>\n\n"
+ + " <tr><td>file0</td><td>1</td></tr>\n"
+ + " <tr><td>file1</td><td>1</td></tr>\n"
+ + "</table></div>\n\n"
+ + "</body></html>\n";
private static final String HTML_NEW_BODY_STRING =
"<strong>Libraries</strong>\n"
+ "<ul class=\"libraries\">\n"
+ "<li><a href=\"#id0\">libA</a></li>\n"
+ "<li><a href=\"#id1\">libB</a></li>\n"
+ + "<li><a href=\"#id0\">libC</a></li>\n"
+ "</ul>\n"
+ "<strong>Files</strong>\n"
+ "<ul class=\"files\">\n"
@@ -146,7 +153,14 @@
+ "license content #1\n"
+ "</pre><!-- license-text -->\n"
+ "</td></tr><!-- same-license -->\n"
- + "</table></body></html>\n";
+ + "</table>\n"
+ + "<div class=\"path-counts\"><table>\n"
+ + " <tr><th>Path prefix</th><th>Count</th></tr>\n\n"
+ + " <tr><td>file0</td><td>1</td></tr>\n"
+ + " <tr><td>file1</td><td>1</td></tr>\n"
+ + " <tr><td>file2</td><td>1</td></tr>\n"
+ + "</table></div>\n\n"
+ + "</body></html>\n";
private static final String EXPECTED_OLD_HTML_STRING = HTML_HEAD_STRING + HTML_OLD_BODY_STRING;
@@ -263,7 +277,7 @@
Map<String, Set<String>> toOne = new HashMap<>();
toBoth.put("", new HashSet<String>(Arrays.asList("0", "1")));
- toOne.put("", new HashSet<String>(Arrays.asList("0", "1")));
+ toOne.put("", new HashSet<String>(Arrays.asList("0")));
fileNameToLibraryToContentIdMap.put("/file0", toBoth);
fileNameToLibraryToContentIdMap.put("/file1", toOne);
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index fa3ed21..01c9ac1 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1582,4 +1582,9 @@
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+
+ <!-- Default device corner radius, used for assist UI -->
+ <dimen name="config_rounded_mask_size">0px</dimen>
+ <dimen name="config_rounded_mask_size_top">0px</dimen>
+ <dimen name="config_rounded_mask_size_bottom">0px</dimen>
</resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 2111df5..647dd47 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -27,6 +27,8 @@
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
@@ -233,17 +235,14 @@
@ViewDebug.ExportedProperty(category="recents")
public boolean isLocked;
+ public Point positionInParent;
+
+ public Rect appBounds;
+
// Last snapshot data, only used for recent tasks
public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
- /**
- * Indicates that this task for the desktop tile in recents.
- *
- * Used when desktop mode feature is enabled.
- */
- public boolean desktopTile;
-
public Task() {
// Do nothing
}
@@ -274,7 +273,8 @@
this(other.key, other.colorPrimary, other.colorBackground, other.isDockable,
other.isLocked, other.taskDescription, other.topActivity);
lastSnapshotData.set(other.lastSnapshotData);
- desktopTile = other.desktopTile;
+ positionInParent = other.positionInParent;
+ appBounds = other.appBounds;
}
/**
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/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index cd9f851..80120a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3164,14 +3164,7 @@
* Whether the keyguard is showing and not occluded.
*/
public boolean isKeyguardVisible() {
- return isKeyguardShowing() && !mKeyguardOccluded;
- }
-
- /**
- * Whether the keyguard is showing. It may still be occluded and not visible.
- */
- public boolean isKeyguardShowing() {
- return mKeyguardShowing;
+ return mKeyguardShowing && !mKeyguardOccluded;
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 3ea8826..90f0446 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -94,11 +94,6 @@
void setOccluded(boolean occluded, boolean animate);
/**
- * @return Whether the keyguard is showing
- */
- boolean isShowing();
-
- /**
* Dismisses the keyguard by going to the next screen or making it gone.
*/
void dismissAndCollapse();
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
index 33e6ca4..9b441ad 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
@@ -21,6 +21,8 @@
import android.view.Display;
import android.view.Surface;
+import com.android.systemui.R;
+
/**
* Utility class for determining screen and corner dimensions.
*/
@@ -82,17 +84,13 @@
* where the curve ends), in pixels.
*/
public static int getCornerRadiusBottom(Context context) {
- int radius = 0;
-
- int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_bottom",
- "dimen", "com.android.systemui");
- if (resourceId > 0) {
- radius = context.getResources().getDimensionPixelSize(resourceId);
- }
+ int radius = context.getResources().getDimensionPixelSize(
+ R.dimen.config_rounded_mask_size_bottom);
if (radius == 0) {
radius = getCornerRadiusDefault(context);
}
+
return radius;
}
@@ -101,28 +99,17 @@
* the curve ends), in pixels.
*/
public static int getCornerRadiusTop(Context context) {
- int radius = 0;
-
- int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_top",
- "dimen", "com.android.systemui");
- if (resourceId > 0) {
- radius = context.getResources().getDimensionPixelSize(resourceId);
- }
+ int radius = context.getResources().getDimensionPixelSize(
+ R.dimen.config_rounded_mask_size_top);
if (radius == 0) {
radius = getCornerRadiusDefault(context);
}
+
return radius;
}
private static int getCornerRadiusDefault(Context context) {
- int radius = 0;
-
- int resourceId = context.getResources().getIdentifier("config_rounded_mask_size",
- "dimen", "com.android.systemui");
- if (resourceId > 0) {
- radius = context.getResources().getDimensionPixelSize(resourceId);
- }
- return radius;
+ return context.getResources().getDimensionPixelSize(R.dimen.config_rounded_mask_size);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 2578df3..0f5a99c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -245,7 +245,7 @@
mAcquiredReceived = true;
final UdfpsView view = mOverlay.getOverlayView();
if (view != null) {
- view.unconfigureDisplay();
+ unconfigureDisplay(view);
}
if (acquiredGood) {
mOverlay.onAcquiredGood();
@@ -737,6 +737,19 @@
mOverlay = null;
mOrientationListener.disable();
+
+ }
+
+ private void unconfigureDisplay(@NonNull UdfpsView view) {
+ if (view.isDisplayConfigured()) {
+ view.unconfigureDisplay();
+
+ if (mCancelAodTimeoutAction != null) {
+ mCancelAodTimeoutAction.run();
+ mCancelAodTimeoutAction = null;
+ }
+ mIsAodInterruptActive = false;
+ }
}
/**
@@ -812,12 +825,12 @@
* sensors, this can result in illumination persisting for longer than necessary.
*/
void onCancelUdfps() {
- if (mOverlay != null && mOverlay.getOverlayView() != null) {
- onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
- }
if (!mIsAodInterruptActive) {
return;
}
+ if (mOverlay != null && mOverlay.getOverlayView() != null) {
+ onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
+ }
if (mCancelAodTimeoutAction != null) {
mCancelAodTimeoutAction.run();
mCancelAodTimeoutAction = null;
@@ -911,15 +924,8 @@
}
}
mOnFingerDown = false;
- if (view.isDisplayConfigured()) {
- view.unconfigureDisplay();
- }
+ unconfigureDisplay(view);
- if (mCancelAodTimeoutAction != null) {
- mCancelAodTimeoutAction.run();
- mCancelAodTimeoutAction = null;
- }
- mIsAodInterruptActive = false;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index 934aedf..4d7f89d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -219,7 +219,7 @@
mView.animateInUdfpsBouncer(null);
}
- if (mKeyguardViewManager.isOccluded()) {
+ if (mKeyguardStateController.isOccluded()) {
mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 696fc72..d1b7368 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -64,29 +64,26 @@
private final Executor mExecutor;
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
- private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+ private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Nullable
private final ComponentName mLowLightDreamComponent;
private final UiEventLogger mUiEventLogger;
+ private final WindowManager mWindowManager;
// A reference to the {@link Window} used to hold the dream overlay.
private Window mWindow;
- // True if the service has been destroyed.
- private boolean mDestroyed;
+ // True if a dream has bound to the service and dream overlay service has started.
+ private boolean mStarted = false;
- private final Complication.Host mHost = new Complication.Host() {
- @Override
- public void requestExitDream() {
- mExecutor.execute(DreamOverlayService.this::requestExit);
- }
- };
+ // True if the service has been destroyed.
+ private boolean mDestroyed = false;
+
+ private final DreamOverlayComponent mDreamOverlayComponent;
private final LifecycleRegistry mLifecycleRegistry;
- private ViewModelStore mViewModelStore = new ViewModelStore();
-
private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
private final KeyguardUpdateMonitorCallback mKeyguardCallback =
@@ -103,7 +100,7 @@
}
};
- private DreamOverlayStateController mStateController;
+ private final DreamOverlayStateController mStateController;
@VisibleForTesting
public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum {
@@ -128,6 +125,7 @@
public DreamOverlayService(
Context context,
@Main Executor executor,
+ WindowManager windowManager,
DreamOverlayComponent.Factory dreamOverlayComponentFactory,
DreamOverlayStateController stateController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -136,19 +134,19 @@
ComponentName lowLightDreamComponent) {
mContext = context;
mExecutor = executor;
+ mWindowManager = windowManager;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLowLightDreamComponent = lowLightDreamComponent;
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
mStateController = stateController;
mUiEventLogger = uiEventLogger;
- final DreamOverlayComponent component =
- dreamOverlayComponentFactory.create(mViewModelStore, mHost);
- mDreamOverlayContainerViewController = component.getDreamOverlayContainerViewController();
+ final ViewModelStore viewModelStore = new ViewModelStore();
+ final Complication.Host host =
+ () -> mExecutor.execute(DreamOverlayService.this::requestExit);
+ mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host);
+ mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry();
setCurrentState(Lifecycle.State.CREATED);
- mLifecycleRegistry = component.getLifecycleRegistry();
- mDreamOverlayTouchMonitor = component.getDreamOverlayTouchMonitor();
- mDreamOverlayTouchMonitor.init();
}
private void setCurrentState(Lifecycle.State state) {
@@ -159,34 +157,48 @@
public void onDestroy() {
mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
setCurrentState(Lifecycle.State.DESTROYED);
- final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- if (mWindow != null) {
- windowManager.removeView(mWindow.getDecorView());
- }
- mStateController.setOverlayActive(false);
- mStateController.setLowLightActive(false);
+
+ resetCurrentDreamOverlay();
+
mDestroyed = true;
super.onDestroy();
}
@Override
public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
- mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
setCurrentState(Lifecycle.State.STARTED);
- final ComponentName dreamComponent = getDreamComponent();
- mStateController.setLowLightActive(
- dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
+
mExecutor.execute(() -> {
+ mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
+
if (mDestroyed) {
// The task could still be executed after the service has been destroyed. Bail if
// that is the case.
return;
}
+
+ if (mStarted) {
+ // Reset the current dream overlay before starting a new one. This can happen
+ // when two dreams overlap (briefly, for a smoother dream transition) and both
+ // dreams are bound to the dream overlay service.
+ resetCurrentDreamOverlay();
+ }
+
+ mDreamOverlayContainerViewController =
+ mDreamOverlayComponent.getDreamOverlayContainerViewController();
+ mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor();
+ mDreamOverlayTouchMonitor.init();
+
mStateController.setShouldShowComplications(shouldShowComplications());
addOverlayWindowLocked(layoutParams);
setCurrentState(Lifecycle.State.RESUMED);
mStateController.setOverlayActive(true);
+ final ComponentName dreamComponent = getDreamComponent();
+ mStateController.setLowLightActive(
+ dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+
+ mStarted = true;
});
}
@@ -222,8 +234,7 @@
removeContainerViewFromParent();
mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView());
- final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
+ mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
}
private void removeContainerViewFromParent() {
@@ -238,4 +249,18 @@
Log.w(TAG, "Removing dream overlay container view parent!");
parentView.removeView(containerView);
}
+
+ private void resetCurrentDreamOverlay() {
+ if (mStarted && mWindow != null) {
+ mWindowManager.removeView(mWindow.getDecorView());
+ }
+
+ mStateController.setOverlayActive(false);
+ mStateController.setLowLightActive(false);
+
+ mDreamOverlayContainerViewController = null;
+ mDreamOverlayTouchMonitor = null;
+
+ mStarted = false;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 8e2ac06..3dbadb0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -235,7 +235,7 @@
// 1100 - windowing
@Keep
public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS =
- new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", true);
+ new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false);
/**
* b/170163464: animate bubbles expanded view collapse with home gesture
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index ad8c688..c4eac1c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -662,7 +662,7 @@
}
override fun onKeyguardDismissAmountChanged() {
- if (keyguardViewController.isShowing && !playingCannedUnlockAnimation) {
+ if (keyguardStateController.isShowing && !playingCannedUnlockAnimation) {
showOrHideSurfaceIfDismissAmountThresholdsReached()
// If the surface is visible or it's about to be, start updating its appearance to
@@ -721,7 +721,7 @@
*/
private fun finishKeyguardExitRemoteAnimationIfReachThreshold() {
// no-op if keyguard is not showing or animation is not enabled.
- if (!keyguardViewController.isShowing) {
+ if (!keyguardStateController.isShowing) {
return
}
@@ -844,7 +844,7 @@
* animation.
*/
fun hideKeyguardViewAfterRemoteAnimation() {
- if (keyguardViewController.isShowing) {
+ if (keyguardStateController.isShowing) {
// Hide the keyguard, with no fade out since we animated it away during the unlock.
keyguardViewController.hide(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 7155acf..aee70ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -1875,7 +1875,7 @@
// if the keyguard is already showing, don't bother. check flags in both files
// to account for the hiding animation which results in a delay and discrepancy
// between flags
- if (mShowing && mKeyguardViewControllerLazy.get().isShowing()) {
+ if (mShowing && mKeyguardStateController.isShowing()) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
resetStateLocked();
return;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index da9fefa..33021e3 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -48,7 +48,6 @@
import androidx.annotation.NonNull;
-import com.android.keyguard.KeyguardViewController;
import com.android.systemui.Dumpable;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -61,6 +60,7 @@
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -90,7 +90,7 @@
private final AccessibilityManager mAccessibilityManager;
private final Lazy<AssistManager> mAssistManagerLazy;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
- private final KeyguardViewController mKeyguardViewController;
+ private final KeyguardStateController mKeyguardStateController;
private final UserTracker mUserTracker;
private final SystemActions mSystemActions;
private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
@@ -125,7 +125,7 @@
OverviewProxyService overviewProxyService,
Lazy<AssistManager> assistManagerLazy,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
- KeyguardViewController keyguardViewController,
+ KeyguardStateController keyguardStateController,
NavigationModeController navigationModeController,
UserTracker userTracker,
DumpManager dumpManager) {
@@ -134,7 +134,7 @@
mAccessibilityManager = accessibilityManager;
mAssistManagerLazy = assistManagerLazy;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
- mKeyguardViewController = keyguardViewController;
+ mKeyguardStateController = keyguardStateController;
mUserTracker = userTracker;
mSystemActions = systemActions;
accessibilityManager.addAccessibilityServicesStateChangeListener(this);
@@ -326,7 +326,7 @@
shadeWindowView =
mCentralSurfacesOptionalLazy.get().get().getNotificationShadeWindowView();
}
- boolean isKeyguardShowing = mKeyguardViewController.isShowing();
+ boolean isKeyguardShowing = mKeyguardStateController.isShowing();
boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow()
&& shadeWindowView.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
return imeVisibleOnShade
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c337dea..6d76c17 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -4287,7 +4287,7 @@
}
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
- && mStatusBarKeyguardViewManager.isShowing()) {
+ && mKeyguardStateController.isShowing()) {
mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
}
@@ -4405,7 +4405,7 @@
}
mSysUiState.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
isFullyExpanded() && !isInSettings())
- .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isInSettings())
+ .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isFullyExpanded() && isInSettings())
.commitUpdate(mDisplayId);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
index 1be4c04..b5c7ef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.notification;
-import android.annotation.Nullable;
import android.util.ArraySet;
import androidx.annotation.VisibleForTesting;
@@ -25,7 +24,6 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import javax.inject.Inject;
@@ -43,7 +41,6 @@
private boolean mLastDynamicUnlocked;
private boolean mCacheInvalid;
- @Nullable private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Inject
DynamicPrivacyController(NotificationLockscreenUserManager notificationLockscreenUserManager,
@@ -100,7 +97,7 @@
* contents aren't revealed yet?
*/
public boolean isInLockedDownShade() {
- if (!isStatusBarKeyguardShowing() || !mKeyguardStateController.isMethodSecure()) {
+ if (!mKeyguardStateController.isShowing() || !mKeyguardStateController.isMethodSecure()) {
return false;
}
int state = mStateController.getState();
@@ -113,16 +110,7 @@
return true;
}
- private boolean isStatusBarKeyguardShowing() {
- return mStatusBarKeyguardViewManager != null && mStatusBarKeyguardViewManager.isShowing();
- }
-
- public void setStatusBarKeyguardViewManager(
- StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
- }
-
public interface Listener {
void onDynamicPrivacyChanged();
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
index a7719d3..e71d80c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
@@ -18,6 +18,8 @@
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+
import android.os.SystemClock;
import android.service.notification.NotificationStats;
@@ -70,6 +72,8 @@
dismissalSurface = NotificationStats.DISMISSAL_PEEK;
} else if (mStatusBarStateController.isDozing()) {
dismissalSurface = NotificationStats.DISMISSAL_AOD;
+ } else if (mStatusBarStateController.getState() == KEYGUARD) {
+ dismissalSurface = NotificationStats.DISMISSAL_LOCKSCREEN;
}
return new DismissedByUserStats(
dismissalSurface,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ce465bc..2719dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -44,7 +44,8 @@
import javax.inject.Inject;
/**
- * A global state to track all input states for the algorithm.
+ * Global state to track all input states for
+ * {@link com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm}.
*/
@SysUISingleton
public class AmbientState implements Dumpable {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 4576a64..9900e41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -150,7 +150,6 @@
private final KeyguardBypassController mKeyguardBypassController;
private PowerManager.WakeLock mWakeLock;
private final KeyguardUpdateMonitor mUpdateMonitor;
- private final DozeParameters mDozeParameters;
private final KeyguardStateController mKeyguardStateController;
private final NotificationShadeWindowController mNotificationShadeWindowController;
private final SessionTracker mSessionTracker;
@@ -262,7 +261,7 @@
KeyguardStateController keyguardStateController, Handler handler,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@Main Resources resources,
- KeyguardBypassController keyguardBypassController, DozeParameters dozeParameters,
+ KeyguardBypassController keyguardBypassController,
MetricsLogger metricsLogger, DumpManager dumpManager,
PowerManager powerManager,
NotificationMediaManager notificationMediaManager,
@@ -276,7 +275,6 @@
VibratorHelper vibrator) {
mPowerManager = powerManager;
mUpdateMonitor = keyguardUpdateMonitor;
- mDozeParameters = dozeParameters;
mUpdateMonitor.registerCallback(this);
mMediaManager = notificationMediaManager;
mLatencyTracker = latencyTracker;
@@ -522,7 +520,7 @@
boolean deviceDreaming = mUpdateMonitor.isDreaming();
if (!mUpdateMonitor.isDeviceInteractive()) {
- if (!mKeyguardViewController.isShowing()
+ if (!mKeyguardStateController.isShowing()
&& !mScreenOffAnimationController.isKeyguardShowDelayed()) {
if (mKeyguardStateController.isUnlocked()) {
return MODE_WAKE_AND_UNLOCK;
@@ -539,7 +537,7 @@
if (unlockingAllowed && deviceDreaming) {
return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
}
- if (mKeyguardViewController.isShowing()) {
+ if (mKeyguardStateController.isShowing()) {
if (mKeyguardViewController.bouncerIsOrWillBeShowing() && unlockingAllowed) {
return MODE_DISMISS_BOUNCER;
} else if (unlockingAllowed) {
@@ -558,7 +556,7 @@
boolean bypass = mKeyguardBypassController.getBypassEnabled()
|| mAuthController.isUdfpsFingerDown();
if (!mUpdateMonitor.isDeviceInteractive()) {
- if (!mKeyguardViewController.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
return bypass ? MODE_WAKE_AND_UNLOCK : MODE_ONLY_WAKE;
} else if (!unlockingAllowed) {
return bypass ? MODE_SHOW_BOUNCER : MODE_NONE;
@@ -582,7 +580,7 @@
if (unlockingAllowed && mKeyguardStateController.isOccluded()) {
return MODE_UNLOCK_COLLAPSING;
}
- if (mKeyguardViewController.isShowing()) {
+ if (mKeyguardStateController.isShowing()) {
if ((mKeyguardViewController.bouncerIsOrWillBeShowing()
|| mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) {
return MODE_DISMISS_BOUNCER;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index e30bc68..d6fadca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -363,7 +363,7 @@
mKeyguardUpdateMonitor.onCameraLaunched();
}
- if (!mStatusBarKeyguardViewManager.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent,
false /* onlyProvisioned */, true /* dismissShade */,
@@ -420,7 +420,7 @@
// TODO(b/169087248) Possibly add haptics here for emergency action. Currently disabled for
// app-side haptic experimentation.
- if (!mStatusBarKeyguardViewManager.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
mCentralSurfaces.startActivityDismissingKeyguard(emergencyIntent,
false /* onlyProvisioned */, true /* dismissShade */,
true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
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 cc15c82..b6e658f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -459,7 +459,6 @@
private final KeyguardStateController mKeyguardStateController;
private final HeadsUpManagerPhone mHeadsUpManager;
private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
- private final DynamicPrivacyController mDynamicPrivacyController;
private final FalsingCollector mFalsingCollector;
private final FalsingManager mFalsingManager;
private final BroadcastDispatcher mBroadcastDispatcher;
@@ -762,7 +761,6 @@
mHeadsUpManager = headsUpManagerPhone;
mKeyguardIndicationController = keyguardIndicationController;
mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
- mDynamicPrivacyController = dynamicPrivacyController;
mFalsingCollector = falsingCollector;
mFalsingManager = falsingManager;
mBroadcastDispatcher = broadcastDispatcher;
@@ -1553,7 +1551,6 @@
.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mRemoteInputManager.addControllerCallback(mStatusBarKeyguardViewManager);
- mDynamicPrivacyController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
mLightBarController.setBiometricUnlockController(mBiometricUnlockController);
mMediaManager.setBiometricUnlockController(mBiometricUnlockController);
@@ -2062,7 +2059,7 @@
// Trimming will happen later if Keyguard is showing - doing it here might cause a jank in
// the bouncer appear animation.
- if (!mStatusBarKeyguardViewManager.isShowing()) {
+ if (!mKeyguardStateController.isShowing()) {
WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
}
}
@@ -2499,8 +2496,8 @@
};
// Do not deferKeyguard when occluded because, when keyguard is occluded,
// we do not launch the activity until keyguard is done.
- boolean occluded = mStatusBarKeyguardViewManager.isShowing()
- && mStatusBarKeyguardViewManager.isOccluded();
+ boolean occluded = mKeyguardStateController.isShowing()
+ && mKeyguardStateController.isOccluded();
boolean deferred = !occluded;
executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShadeDirectly,
willLaunchResolverActivity, deferred /* deferred */, animate);
@@ -2570,8 +2567,8 @@
@Override
public boolean onDismiss() {
if (runnable != null) {
- if (mStatusBarKeyguardViewManager.isShowing()
- && mStatusBarKeyguardViewManager.isOccluded()) {
+ if (mKeyguardStateController.isShowing()
+ && mKeyguardStateController.isOccluded()) {
mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
} else {
mMainExecutor.execute(runnable);
@@ -2665,7 +2662,7 @@
private void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
boolean afterKeyguardGone) {
- if (mStatusBarKeyguardViewManager.isShowing() && requiresShadeOpen) {
+ if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
}
dismissKeyguardThenExecute(action, null /* cancelAction */,
@@ -2689,7 +2686,7 @@
mBiometricUnlockController.startWakeAndUnlock(
BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING);
}
- if (mStatusBarKeyguardViewManager.isShowing()) {
+ if (mKeyguardStateController.isShowing()) {
mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction,
afterKeyguardGone);
} else {
@@ -2825,8 +2822,8 @@
}
private void logStateToEventlog() {
- boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
- boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
+ boolean isShowing = mKeyguardStateController.isShowing();
+ boolean isOccluded = mKeyguardStateController.isOccluded();
boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
boolean isSecure = mKeyguardStateController.isMethodSecure();
boolean unlocked = mKeyguardStateController.canDismissLockScreen();
@@ -3222,18 +3219,17 @@
Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
Trace.beginSection("CentralSurfaces#updateDozingState");
- boolean visibleNotOccluded = mStatusBarKeyguardViewManager.isShowing()
- && !mStatusBarKeyguardViewManager.isOccluded();
+ boolean keyguardVisible = mKeyguardStateController.isVisible();
// If we're dozing and we'll be animating the screen off, the keyguard isn't currently
// visible but will be shortly for the animation, so we should proceed as if it's visible.
- boolean visibleNotOccludedOrWillBe =
- visibleNotOccluded || (mDozing && mDozeParameters.shouldDelayKeyguardShow());
+ boolean keyguardVisibleOrWillBe =
+ keyguardVisible || (mDozing && mDozeParameters.shouldDelayKeyguardShow());
boolean wakeAndUnlock = mBiometricUnlockController.getMode()
== BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
boolean animate = (!mDozing && mDozeServiceHost.shouldAnimateWakeup() && !wakeAndUnlock)
|| (mDozing && mDozeParameters.shouldControlScreenOff()
- && visibleNotOccludedOrWillBe);
+ && keyguardVisibleOrWillBe);
mNotificationPanelViewController.setDozing(mDozing, animate);
updateQsExpansionEnabled();
@@ -3915,11 +3911,7 @@
@Override
public boolean isKeyguardShowing() {
- if (mStatusBarKeyguardViewManager == null) {
- Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true");
- return true;
- }
- return mStatusBarKeyguardViewManager.isShowing();
+ return mKeyguardStateController.isShowing();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index d4d510f..5f5ec68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -230,8 +230,6 @@
private View mNotificationContainer;
@Nullable protected KeyguardBouncer mBouncer;
- protected boolean mShowing;
- protected boolean mOccluded;
protected boolean mRemoteInputActive;
private boolean mGlobalActionsVisible = false;
private boolean mLastGlobalActionsVisible = false;
@@ -278,10 +276,9 @@
new KeyguardUpdateMonitorCallback() {
@Override
public void onEmergencyCallAction() {
-
// Since we won't get a setOccluded call we have to reset the view manually such that
// the bouncer goes away.
- if (mOccluded) {
+ if (mKeyguardStateController.isOccluded()) {
reset(true /* hideBouncerWhenShowing */);
}
}
@@ -481,7 +478,7 @@
} else {
mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
}
- } else if (mShowing && !hideBouncerOverDream) {
+ } else if (mKeyguardStateController.isShowing() && !hideBouncerOverDream) {
if (!isWakeAndUnlocking()
&& !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
&& !mCentralSurfaces.isInLaunchTransition()
@@ -502,7 +499,7 @@
mBouncerInteractor.show(/* isScrimmed= */false);
}
}
- } else if (!mShowing && isBouncerInTransit()) {
+ } else if (!mKeyguardStateController.isShowing() && isBouncerInTransit()) {
// Keyguard is not visible anymore, but expansion animation was still running.
// We need to hide the bouncer, otherwise it will be stuck in transit.
if (mBouncer != null) {
@@ -535,9 +532,8 @@
@Override
public void show(Bundle options) {
Trace.beginSection("StatusBarKeyguardViewManager#show");
- mShowing = true;
mNotificationShadeWindowController.setKeyguardShowing(true);
- mKeyguardStateController.notifyKeyguardState(mShowing,
+ mKeyguardStateController.notifyKeyguardState(true,
mKeyguardStateController.isOccluded());
reset(true /* hideBouncerWhenShowing */);
SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
@@ -601,7 +597,7 @@
} else {
mBouncerInteractor.hide();
}
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
// If we were showing the bouncer and then aborting, we need to also clear out any
// potential actions unless we actually unlocked.
cancelPostAuthActions();
@@ -618,7 +614,7 @@
public void showBouncer(boolean scrimmed) {
resetAlternateAuth(false);
- if (mShowing && !isBouncerShowing()) {
+ if (mKeyguardStateController.isShowing() && !isBouncerShowing()) {
if (mBouncer != null) {
mBouncer.show(false /* resetSecuritySelection */, scrimmed);
} else {
@@ -635,7 +631,7 @@
public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
boolean afterKeyguardGone, String message) {
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
try {
Trace.beginSection("StatusBarKeyguardViewManager#dismissWithAction");
cancelPendingWakeupAction();
@@ -721,11 +717,12 @@
@Override
public void reset(boolean hideBouncerWhenShowing) {
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
+ final boolean isOccluded = mKeyguardStateController.isOccluded();
// Hide quick settings.
- mNotificationPanelViewController.resetViews(/* animate= */ !mOccluded);
+ mNotificationPanelViewController.resetViews(/* animate= */ !isOccluded);
// Hide bouncer and quick-quick settings.
- if (mOccluded && !mDozing) {
+ if (isOccluded && !mDozing) {
mCentralSurfaces.hideKeyguard();
if (hideBouncerWhenShowing || needsFullscreenBouncer()) {
hideBouncer(false /* destroyView */);
@@ -807,7 +804,8 @@
private void setDozing(boolean dozing) {
if (mDozing != dozing) {
mDozing = dozing;
- if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) {
+ if (dozing || mBouncer.needsFullscreenBouncer()
+ || mKeyguardStateController.isOccluded()) {
reset(dozing /* hideBouncerWhenShowing */);
}
updateStates();
@@ -840,18 +838,23 @@
@Override
public void setOccluded(boolean occluded, boolean animate) {
- final boolean isOccluding = !mOccluded && occluded;
- final boolean isUnOccluding = mOccluded && !occluded;
- setOccludedAndUpdateStates(occluded);
+ final boolean wasOccluded = mKeyguardStateController.isOccluded();
+ final boolean isOccluding = !wasOccluded && occluded;
+ final boolean isUnOccluding = wasOccluded && !occluded;
+ mKeyguardStateController.notifyKeyguardState(
+ mKeyguardStateController.isShowing(), occluded);
+ updateStates();
+ final boolean isShowing = mKeyguardStateController.isShowing();
+ final boolean isOccluded = mKeyguardStateController.isOccluded();
- if (mShowing && isOccluding) {
+ if (isShowing && isOccluding) {
SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED);
if (mCentralSurfaces.isInLaunchTransition()) {
final Runnable endRunnable = new Runnable() {
@Override
public void run() {
- mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+ mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
reset(true /* hideBouncerWhenShowing */);
}
};
@@ -866,19 +869,19 @@
// When isLaunchingActivityOverLockscreen() is true, we know for sure that the post
// collapse runnables will be run.
mShadeController.get().addPostCollapseAction(() -> {
- mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+ mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
reset(true /* hideBouncerWhenShowing */);
});
return;
}
- } else if (mShowing && isUnOccluding) {
+ } else if (isShowing && isUnOccluding) {
SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN);
}
- if (mShowing) {
- mMediaManager.updateMediaMetaData(false, animate && !mOccluded);
+ if (isShowing) {
+ mMediaManager.updateMediaMetaData(false, animate && !isOccluded);
}
- mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+ mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
// setDozing(false) will call reset once we stop dozing. Also, if we're going away, there's
// no need to reset the keyguard views as we'll be gone shortly. Resetting now could cause
@@ -888,20 +891,11 @@
// by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
reset(isOccluding /* hideBouncerWhenShowing*/);
}
- if (animate && !mOccluded && mShowing && !bouncerIsShowing()) {
+ if (animate && !isOccluded && isShowing && !bouncerIsShowing()) {
mCentralSurfaces.animateKeyguardUnoccluding();
}
}
- private void setOccludedAndUpdateStates(boolean occluded) {
- mOccluded = occluded;
- updateStates();
- }
-
- public boolean isOccluded() {
- return mOccluded;
- }
-
@Override
public void startPreHideAnimation(Runnable finishRunnable) {
if (bouncerIsShowing()) {
@@ -932,8 +926,7 @@
@Override
public void hide(long startTime, long fadeoutDuration) {
Trace.beginSection("StatusBarKeyguardViewManager#hide");
- mShowing = false;
- mKeyguardStateController.notifyKeyguardState(mShowing,
+ mKeyguardStateController.notifyKeyguardState(false,
mKeyguardStateController.isOccluded());
launchPendingWakeupAction();
@@ -1081,11 +1074,6 @@
KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None;
}
- @Override
- public boolean isShowing() {
- return mShowing;
- }
-
/**
* Returns whether a back invocation can be handled, which depends on whether the keyguard
* is currently showing (which itself is derived from multiple states).
@@ -1184,8 +1172,8 @@
if (!mCentralSurfacesRegistered) {
return;
}
- boolean showing = mShowing;
- boolean occluded = mOccluded;
+ boolean showing = mKeyguardStateController.isShowing();
+ boolean occluded = mKeyguardStateController.isOccluded();
boolean bouncerShowing = bouncerIsShowing();
boolean bouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing();
boolean bouncerDismissible = !isFullscreenBouncer();
@@ -1219,13 +1207,6 @@
mNotificationShadeWindowController.setBouncerShowing(bouncerShowing);
mCentralSurfaces.setBouncerShowing(bouncerShowing);
}
-
- if (occluded != mLastOccluded || mFirstUpdate) {
- mKeyguardStateController.notifyKeyguardState(showing, occluded);
- }
- if (occluded != mLastOccluded || mShowing != showing || mFirstUpdate) {
- mKeyguardUpdateManager.setKeyguardShowing(showing, occluded);
- }
if (bouncerIsOrWillBeShowing != mLastBouncerIsOrWillBeShowing || mFirstUpdate
|| bouncerShowing != mLastBouncerShowing) {
mKeyguardUpdateManager.sendKeyguardBouncerChanged(bouncerIsOrWillBeShowing,
@@ -1276,12 +1257,12 @@
public boolean isNavBarVisible() {
boolean isWakeAndUnlockPulsing = mBiometricUnlockController != null
&& mBiometricUnlockController.getMode() == MODE_WAKE_AND_UNLOCK_PULSING;
- boolean keyguardShowing = mShowing && !mOccluded;
+ boolean keyguardVisible = mKeyguardStateController.isVisible();
boolean hideWhileDozing = mDozing && !isWakeAndUnlockPulsing;
- boolean keyguardWithGestureNav = (keyguardShowing && !mDozing && !mScreenOffAnimationPlaying
+ boolean keyguardWithGestureNav = (keyguardVisible && !mDozing && !mScreenOffAnimationPlaying
|| mPulsing && !mIsDocked)
&& mGesturalNav;
- return (!keyguardShowing && !hideWhileDozing && !mScreenOffAnimationPlaying
+ return (!keyguardVisible && !hideWhileDozing && !mScreenOffAnimationPlaying
|| bouncerIsShowing()
|| mRemoteInputActive
|| keyguardWithGestureNav
@@ -1413,7 +1394,7 @@
DismissWithActionRequest request = mPendingWakeupAction;
mPendingWakeupAction = null;
if (request != null) {
- if (mShowing) {
+ if (mKeyguardStateController.isShowing()) {
dismissWithAction(request.dismissAction, request.cancelAction,
request.afterKeyguardGone, request.message);
} else if (request.dismissAction != null) {
@@ -1432,10 +1413,10 @@
public boolean bouncerNeedsScrimming() {
// When a dream overlay is active, scrimming will cause any expansion to immediately expand.
- return (mOccluded && !mDreamOverlayStateController.isOverlayActive())
+ return (mKeyguardStateController.isOccluded()
+ && !mDreamOverlayStateController.isOverlayActive())
|| bouncerWillDismissWithAction()
- || (bouncerIsShowing()
- && bouncerIsScrimmed())
+ || (bouncerIsShowing() && bouncerIsScrimmed())
|| isFullscreenBouncer();
}
@@ -1454,8 +1435,6 @@
public void dump(PrintWriter pw) {
pw.println("StatusBarKeyguardViewManager:");
- pw.println(" mShowing: " + mShowing);
- pw.println(" mOccluded: " + mOccluded);
pw.println(" mRemoteInputActive: " + mRemoteInputActive);
pw.println(" mDozing: " + mDozing);
pw.println(" mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index c96faab..062c3d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -21,7 +21,9 @@
/** Provides information about the current wifi network. */
sealed class WifiNetworkModel {
/** A model representing that we have no active wifi network. */
- object Inactive : WifiNetworkModel()
+ object Inactive : WifiNetworkModel() {
+ override fun toString() = "WifiNetwork.Inactive"
+ }
/**
* A model representing that our wifi network is actually a carrier merged network, meaning it's
@@ -29,7 +31,9 @@
*
* See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
*/
- object CarrierMerged : WifiNetworkModel()
+ object CarrierMerged : WifiNetworkModel() {
+ override fun toString() = "WifiNetwork.CarrierMerged"
+ }
/** Provides information about an active wifi network. */
data class Active(
@@ -72,6 +76,24 @@
}
}
+ override fun toString(): String {
+ // Only include the passpoint-related values in the string if we have them. (Most
+ // networks won't have them so they'll be mostly clutter.)
+ val passpointString =
+ if (isPasspointAccessPoint ||
+ isOnlineSignUpForPasspointAccessPoint ||
+ passpointProviderFriendlyName != null) {
+ ", isPasspointAp=$isPasspointAccessPoint, " +
+ "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " +
+ "passpointName=$passpointProviderFriendlyName"
+ } else {
+ ""
+ }
+
+ return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " +
+ "level=$level, ssid=$ssid$passpointString)"
+ }
+
companion object {
@VisibleForTesting
internal const val MIN_VALID_LEVEL = 0
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index 250d9d4..1ae1eae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -35,9 +35,16 @@
}
/**
- * If the lock screen is visible.
- * The keyguard is also visible when the device is asleep or in always on mode, except when
- * the screen timed out and the user can unlock by quickly pressing power.
+ * If the keyguard is visible. This is unrelated to being locked or not.
+ */
+ default boolean isVisible() {
+ return isShowing() && !isOccluded();
+ }
+
+ /**
+ * If the keyguard is showing. This includes when it's occluded by an activity, and when
+ * the device is asleep or in always on mode, except when the screen timed out and the user
+ * can unlock by quickly pressing power.
*
* This is unrelated to being locked or not.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index 437d4d4..cc6fdcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -181,6 +181,7 @@
if (mShowing == showing && mOccluded == occluded) return;
mShowing = showing;
mOccluded = occluded;
+ mKeyguardUpdateMonitor.setKeyguardShowing(showing, occluded);
Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
"Keyguard showing: " + showing + " occluded: " + occluded);
notifyKeyguardChanged();
@@ -387,6 +388,8 @@
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardStateController:");
+ pw.println(" mShowing: " + mShowing);
+ pw.println(" mOccluded: " + mOccluded);
pw.println(" mSecure: " + mSecure);
pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
pw.println(" mTrustManaged: " + mTrustManaged);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java
new file mode 100644
index 0000000..dd9683f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.assist.ui;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DisplayUtilsTest extends SysuiTestCase {
+
+ @Mock
+ Resources mResources;
+ @Mock
+ Context mMockContext;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testGetCornerRadii_noOverlay() {
+ assertEquals(0, DisplayUtils.getCornerRadiusBottom(mContext));
+ assertEquals(0, DisplayUtils.getCornerRadiusTop(mContext));
+ }
+
+ @Test
+ public void testGetCornerRadii_onlyDefaultOverridden() {
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100);
+ when(mMockContext.getResources()).thenReturn(mResources);
+
+ assertEquals(100, DisplayUtils.getCornerRadiusBottom(mMockContext));
+ assertEquals(100, DisplayUtils.getCornerRadiusTop(mMockContext));
+ }
+
+ @Test
+ public void testGetCornerRadii_allOverridden() {
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100);
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_top)).thenReturn(
+ 150);
+ when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_bottom)).thenReturn(
+ 200);
+ when(mMockContext.getResources()).thenReturn(mResources);
+
+ assertEquals(200, DisplayUtils.getCornerRadiusBottom(mMockContext));
+ assertEquals(150, DisplayUtils.getCornerRadiusTop(mMockContext));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 11e5880..f210708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -38,6 +38,7 @@
import static org.mockito.Mockito.when;
import android.graphics.Rect;
+import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricOverlayConstants;
import android.hardware.biometrics.ComponentInfoInternal;
import android.hardware.biometrics.SensorProperties;
@@ -688,7 +689,7 @@
}
@Test
- public void aodInterruptCancelTimeoutActionWhenFingerUp() throws RemoteException {
+ public void aodInterruptCancelTimeoutActionOnFingerUp() throws RemoteException {
when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
@@ -740,6 +741,56 @@
}
@Test
+ public void aodInterruptCancelTimeoutActionOnAcquired() throws RemoteException {
+ when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
+ when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
+
+ // GIVEN AOD interrupt
+ mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+ BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+ mScreenObserver.onScreenTurnedOn();
+ mFgExecutor.runAllReady();
+ mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
+ mFgExecutor.runAllReady();
+
+ // Configure UdfpsView to accept the acquired event
+ when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+
+ // WHEN acquired is received
+ mOverlayController.onAcquired(TEST_UDFPS_SENSOR_ID,
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD);
+
+ // Configure UdfpsView to accept the ACTION_DOWN event
+ when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+
+ // WHEN ACTION_DOWN is received
+ verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
+ MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
+ mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+ mBiometricsExecutor.runAllReady();
+ downEvent.recycle();
+
+ // WHEN ACTION_MOVE is received
+ MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
+ mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+ mBiometricsExecutor.runAllReady();
+ moveEvent.recycle();
+ mFgExecutor.runAllReady();
+
+ // Configure UdfpsView to accept the finger up event
+ when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+
+ // WHEN it times out
+ mFgExecutor.advanceClockToNext();
+ mFgExecutor.runAllReady();
+
+ // THEN the display should be unconfigured once. If the timeout action is not
+ // cancelled, the display would be unconfigured twice which would cause two
+ // FP attempts.
+ verify(mUdfpsView, times(1)).unconfigureDisplay();
+ }
+
+ @Test
public void aodInterruptScreenOff() throws RemoteException {
// GIVEN screen off
mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index eec33ca..f370be1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -27,10 +28,10 @@
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
-import android.service.dreams.DreamService;
import android.service.dreams.IDreamOverlay;
import android.service.dreams.IDreamOverlayCallback;
import android.testing.AndroidTestingRunner;
+import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManagerImpl;
@@ -53,6 +54,8 @@
import org.junit.Rule;
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;
@@ -61,6 +64,7 @@
public class DreamOverlayServiceTest extends SysuiTestCase {
private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package",
"lowlight");
+ private static final String DREAM_COMPONENT = "package/dream";
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
@@ -108,12 +112,14 @@
@Mock
UiEventLogger mUiEventLogger;
+ @Captor
+ ArgumentCaptor<View> mViewCaptor;
+
DreamOverlayService mService;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mContext.addMockSystemService(WindowManager.class, mWindowManager);
when(mDreamOverlayComponent.getDreamOverlayContainerViewController())
.thenReturn(mDreamOverlayContainerViewController);
@@ -129,7 +135,7 @@
when(mDreamOverlayContainerViewController.getContainerView())
.thenReturn(mDreamOverlayContainerView);
- mService = new DreamOverlayService(mContext, mMainExecutor,
+ mService = new DreamOverlayService(mContext, mMainExecutor, mWindowManager,
mDreamOverlayComponentFactory,
mStateController,
mKeyguardUpdateMonitor,
@@ -143,7 +149,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
@@ -157,7 +164,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mWindowManager).addView(any(), any());
@@ -169,7 +177,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewController).init();
@@ -186,49 +195,76 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView);
}
@Test
- public void testShouldShowComplicationsFalseByDefault() {
- mService.onBind(new Intent());
+ public void testShouldShowComplicationsSetByStartDream() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
- assertThat(mService.shouldShowComplications()).isFalse();
- }
-
- @Test
- public void testShouldShowComplicationsSetByIntentExtra() {
- final Intent intent = new Intent();
- intent.putExtra(DreamService.EXTRA_SHOW_COMPLICATIONS, true);
- mService.onBind(intent);
+ // Inform the overlay service of dream starting.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ true /*shouldShowComplication*/);
assertThat(mService.shouldShowComplications()).isTrue();
}
@Test
- public void testLowLightSetByIntentExtra() throws RemoteException {
- final Intent intent = new Intent();
- intent.putExtra(DreamService.EXTRA_DREAM_COMPONENT, LOW_LIGHT_COMPONENT);
-
- final IBinder proxy = mService.onBind(intent);
+ public void testLowLightSetByStartDream() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
- assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback,
+ LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
+ assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
verify(mStateController).setLowLightActive(true);
}
@Test
- public void testDestroy() {
+ public void testDestroy() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+ // Inform the overlay service of dream starting.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback,
+ LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Verify view added.
+ verify(mWindowManager).addView(mViewCaptor.capture(), any());
+
+ // Service destroyed.
mService.onDestroy();
mMainExecutor.runAllReady();
+ // Verify view removed.
+ verify(mWindowManager).removeView(mViewCaptor.getValue());
+
+ // Verify state correctly set.
+ verify(mKeyguardUpdateMonitor).removeCallback(any());
+ verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
+ verify(mStateController).setOverlayActive(false);
+ verify(mStateController).setLowLightActive(false);
+ }
+
+ @Test
+ public void testDoNotRemoveViewOnDestroyIfOverlayNotStarted() {
+ // Service destroyed without ever starting dream.
+ mService.onDestroy();
+ mMainExecutor.runAllReady();
+
+ // Verify no view is removed.
+ verify(mWindowManager, never()).removeView(any());
+
+ // Verify state still correctly set.
verify(mKeyguardUpdateMonitor).removeCallback(any());
verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
verify(mStateController).setOverlayActive(false);
@@ -245,7 +281,8 @@
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback);
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
// Destroy the service.
mService.onDestroy();
@@ -255,4 +292,44 @@
verify(mWindowManager, never()).addView(any(), any());
}
+
+ @Test
+ public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException {
+ final IBinder proxy = mService.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ false /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Verify that a new window is added.
+ verify(mWindowManager).addView(mViewCaptor.capture(), any());
+ final View windowDecorView = mViewCaptor.getValue();
+
+ // Assert that the overlay is not showing complications.
+ assertThat(mService.shouldShowComplications()).isFalse();
+
+ clearInvocations(mDreamOverlayComponent);
+ clearInvocations(mWindowManager);
+
+ // New dream starting with dream complications showing. Note that when a new dream is
+ // binding to the dream overlay service, it receives the same instance of IBinder as the
+ // first one.
+ overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ true /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Assert that the overlay is showing complications.
+ assertThat(mService.shouldShowComplications()).isTrue();
+
+ // Verify that the old overlay window has been removed, and a new one created.
+ verify(mWindowManager).removeView(windowDecorView);
+ verify(mWindowManager).addView(any(), any());
+
+ // Verify that new instances of overlay container view controller and overlay touch monitor
+ // are created.
+ verify(mDreamOverlayComponent).getDreamOverlayContainerViewController();
+ verify(mDreamOverlayComponent).getDreamOverlayTouchMonitor();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 21c018a..39f3c96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -176,7 +176,7 @@
// and the keyguard goes away
mViewMediator.setShowingLocked(false);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false);
TestableLooper.get(this).processAllMessages();
@@ -201,7 +201,7 @@
// and the keyguard goes away
mViewMediator.setShowingLocked(false);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false);
TestableLooper.get(this).processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 8073103..6c03730 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -39,7 +39,6 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.keyguard.KeyguardViewController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -49,6 +48,7 @@
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Before;
import org.junit.Test;
@@ -113,7 +113,7 @@
mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager,
mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver,
mSystemActions, mOverviewProxyService, mAssistManagerLazy,
- () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardViewController.class),
+ () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class),
mNavigationModeController, mUserTracker, mDumpManager);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 51f0953..0e9d279 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -72,7 +72,6 @@
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
-import com.android.keyguard.KeyguardViewController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
@@ -194,7 +193,7 @@
@Mock
private CentralSurfaces mCentralSurfaces;
@Mock
- private KeyguardViewController mKeyguardViewController;
+ private KeyguardStateController mKeyguardStateController;
@Mock
private UserContextProvider mUserContextProvider;
@Mock
@@ -240,7 +239,7 @@
mock(AccessibilityButtonTargetsObserver.class),
mSystemActions, mOverviewProxyService,
() -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces),
- mKeyguardViewController, mock(NavigationModeController.class),
+ mKeyguardStateController, mock(NavigationModeController.class),
mock(UserTracker.class), mock(DumpManager.class)));
mNavigationBar = createNavBar(mContext);
mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
@@ -380,7 +379,7 @@
// Verify navbar didn't alter and showing back icon when the keyguard is showing without
// requesting IME insets visible.
- doReturn(true).when(mKeyguardViewController).isShowing();
+ doReturn(true).when(mKeyguardStateController).isShowing();
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
index b719c7f..a6381d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
@@ -32,7 +32,6 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Assert;
@@ -58,8 +57,6 @@
mDynamicPrivacyController = new DynamicPrivacyController(
mLockScreenUserManager, mKeyguardStateController,
mock(StatusBarStateController.class));
- mDynamicPrivacyController.setStatusBarKeyguardViewManager(
- mock(StatusBarKeyguardViewManager.class));
mDynamicPrivacyController.addListener(mListener);
// Disable dynamic privacy by default
allowNotificationsInPublic(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 365e608..4dea6be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -95,8 +95,6 @@
@Mock
private AuthController mAuthController;
@Mock
- private DozeParameters mDozeParameters;
- @Mock
private MetricsLogger mMetricsLogger;
@Mock
private NotificationMediaManager mNotificationMediaManager;
@@ -120,7 +118,7 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
TestableResources res = getContext().getOrCreateTestableResources();
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
when(mKeyguardStateController.isUnlocked()).thenReturn(false);
@@ -132,7 +130,7 @@
mBiometricUnlockController = new BiometricUnlockController(mDozeScrimController,
mKeyguardViewMediator, mScrimController,
mNotificationShadeWindowController, mKeyguardStateController, mHandler,
- mUpdateMonitor, res.getResources(), mKeyguardBypassController, mDozeParameters,
+ mUpdateMonitor, res.getResources(), mKeyguardBypassController,
mMetricsLogger, mDumpManager, mPowerManager,
mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
mAuthController, mStatusBarStateController,
@@ -170,7 +168,7 @@
public void onBiometricAuthenticated_whenFingerprintAndNotInteractive_wakeAndUnlock() {
reset(mUpdateMonitor);
reset(mStatusBarKeyguardViewManager);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
when(mDozeScrimController.isPulsing()).thenReturn(true);
// the value of isStrongBiometric doesn't matter here since we only care about the returned
@@ -187,7 +185,7 @@
public void onBiometricAuthenticated_whenDeviceIsAlreadyUnlocked_wakeAndUnlock() {
reset(mUpdateMonitor);
reset(mStatusBarKeyguardViewManager);
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
when(mKeyguardStateController.isUnlocked()).thenReturn(true);
when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
when(mDozeScrimController.isPulsing()).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 05f8760..ad497a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -516,32 +516,32 @@
@Test
public void executeRunnableDismissingKeyguard_nullRunnable_showingAndOccluded() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
}
@Test
public void executeRunnableDismissingKeyguard_nullRunnable_showing() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
}
@Test
public void executeRunnableDismissingKeyguard_nullRunnable_notShowing() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
}
@Test
public void executeRunnableDismissingKeyguard_dreaming_notShowing() throws RemoteException {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(true);
mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {},
@@ -555,8 +555,8 @@
@Test
public void executeRunnableDismissingKeyguard_notDreaming_notShowing() throws RemoteException {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(false);
mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {},
@@ -571,10 +571,10 @@
@Test
public void lockscreenStateMetrics_notShowing() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(false);
mCentralSurfaces.onKeyguardViewManagerStatesUpdated();
@@ -589,10 +589,10 @@
@Test
public void lockscreenStateMetrics_notShowing_secure() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
@@ -608,10 +608,10 @@
@Test
public void lockscreenStateMetrics_isShowing() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(false);
@@ -627,10 +627,10 @@
@Test
public void lockscreenStateMetrics_isShowing_secure() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
@@ -646,10 +646,10 @@
@Test
public void lockscreenStateMetrics_isShowingBouncer() {
// uninteresting state, except that fingerprint must be non-zero
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+ when(mKeyguardStateController.isOccluded()).thenReturn(false);
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
// interesting state
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
@@ -1053,9 +1053,9 @@
}
@Test
- public void startActivityDismissingKeyguard_isShowingandIsOccluded() {
- when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
- when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true);
+ public void startActivityDismissingKeyguard_isShowingAndIsOccluded() {
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
mCentralSurfaces.startActivityDismissingKeyguard(
new Intent(),
/* onlyProvisioned = */false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
new file mode 100644
index 0000000..a986777
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
@@ -0,0 +1,145 @@
+/*
+ * 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.statusbar.phone;
+
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+/**
+ * Mock implementation of KeyguardStateController which tracks showing and occluded states
+ * based on {@link #notifyKeyguardState(boolean showing, boolean occluded)}}.
+ */
+public class FakeKeyguardStateController implements KeyguardStateController {
+ private boolean mShowing;
+ private boolean mOccluded;
+ private boolean mCanDismissLockScreen;
+
+ @Override
+ public void notifyKeyguardState(boolean showing, boolean occluded) {
+ mShowing = showing;
+ mOccluded = occluded;
+ }
+
+ @Override
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ @Override
+ public boolean isOccluded() {
+ return mOccluded;
+ }
+
+ public void setCanDismissLockScreen(boolean canDismissLockScreen) {
+ mCanDismissLockScreen = canDismissLockScreen;
+ }
+
+ @Override
+ public boolean canDismissLockScreen() {
+ return mCanDismissLockScreen;
+ }
+
+ @Override
+ public boolean isBouncerShowing() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardScreenRotationAllowed() {
+ return false;
+ }
+
+ @Override
+ public boolean isMethodSecure() {
+ return true;
+ }
+
+ @Override
+ public boolean isTrusted() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardGoingAway() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardFadingAway() {
+ return false;
+ }
+
+ @Override
+ public boolean isLaunchTransitionFadingAway() {
+ return false;
+ }
+
+ @Override
+ public long getKeyguardFadingAwayDuration() {
+ return 0;
+ }
+
+ @Override
+ public long getKeyguardFadingAwayDelay() {
+ return 0;
+ }
+
+ @Override
+ public long calculateGoingToFullShadeDelay() {
+ return 0;
+ }
+
+ @Override
+ public float getDismissAmount() {
+ return 0f;
+ }
+
+ @Override
+ public boolean isDismissingFromSwipe() {
+ return false;
+ }
+
+ @Override
+ public boolean isFlingingToDismissKeyguard() {
+ return false;
+ }
+
+ @Override
+ public boolean isFlingingToDismissKeyguardDuringSwipeGesture() {
+ return false;
+ }
+
+ @Override
+ public boolean isSnappingKeyguardBackAfterSwipe() {
+ return false;
+ }
+
+ @Override
+ public void notifyPanelFlingStart(boolean dismiss) {
+ }
+
+ @Override
+ public void notifyPanelFlingEnd() {
+ }
+
+ @Override
+ public void addCallback(Callback listener) {
+ }
+
+ @Override
+ public void removeCallback(Callback listener) {
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index ecbb7e5b..8da8d04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -28,6 +28,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -68,7 +69,6 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.unfold.SysUIUnfoldComponent;
import com.google.common.truth.Truth;
@@ -94,7 +94,6 @@
@Mock private ViewMediatorCallback mViewMediatorCallback;
@Mock private LockPatternUtils mLockPatternUtils;
- @Mock private KeyguardStateController mKeyguardStateController;
@Mock private CentralSurfaces mCentralSurfaces;
@Mock private ViewGroup mContainer;
@Mock private NotificationPanelViewController mNotificationPanelView;
@@ -123,6 +122,8 @@
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback;
+ private FakeKeyguardStateController mKeyguardStateController =
+ spy(new FakeKeyguardStateController());
@Mock private ViewRootImpl mViewRootImpl;
@Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
@@ -180,7 +181,6 @@
mBiometricUnlockController,
mNotificationContainer,
mBypassController);
- when(mKeyguardStateController.isOccluded()).thenReturn(false);
mStatusBarKeyguardViewManager.show(null);
ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback> callbackArgumentCaptor =
ArgumentCaptor.forClass(KeyguardBouncer.BouncerExpansionCallback.class);
@@ -253,7 +253,7 @@
@Test
public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
- when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+ mKeyguardStateController.setCanDismissLockScreen(false);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
verify(mBouncer).show(eq(false), eq(false));
@@ -340,13 +340,12 @@
}
@Test
- public void setOccluded_onKeyguardOccludedChangedCalledCorrectly() {
+ public void setOccluded_onKeyguardOccludedChangedCalled() {
clearInvocations(mKeyguardStateController);
clearInvocations(mKeyguardUpdateMonitor);
- // Should be false to start, so no invocations
mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
- verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean());
+ verify(mKeyguardStateController).notifyKeyguardState(true, false);
clearInvocations(mKeyguardUpdateMonitor);
clearInvocations(mKeyguardStateController);
@@ -357,8 +356,8 @@
clearInvocations(mKeyguardUpdateMonitor);
clearInvocations(mKeyguardStateController);
- mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
- verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean());
+ mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
+ verify(mKeyguardStateController).notifyKeyguardState(true, false);
}
@Test
@@ -426,7 +425,7 @@
when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true);
assertTrue(
"Is showing not accurate when alternative auth showing",
- mStatusBarKeyguardViewManager.isShowing());
+ mStatusBarKeyguardViewManager.isBouncerShowing());
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 929e529..a3ad028 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -23,9 +23,9 @@
import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.statusbar.connectivity.WifiIcons
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -144,7 +144,12 @@
/** A function that, given a context, calculates the correct content description string. */
val contentDescription: (Context) -> String,
- )
+
+ /** A human-readable description used for the test names. */
+ val description: String,
+ ) {
+ override fun toString() = description
+ }
// Note: We use default values for the boolean parameters to reflect a "typical configuration"
// for wifi. This allows each TestCase to only define the parameter values that are critical
@@ -158,12 +163,21 @@
/** The expected output. Null if we expect the output to be null. */
val expected: Expected?
- )
+ ) {
+ override fun toString(): String {
+ return "when INPUT(enabled=$enabled, " +
+ "forceHidden=$forceHidden, " +
+ "showWhenEnabled=$alwaysShowIconWhenEnabled, " +
+ "hasDataCaps=$hasDataCapabilities, " +
+ "network=$network) then " +
+ "EXPECTED($expected)"
+ }
+ }
companion object {
- @Parameters(name = "{0}")
- @JvmStatic
- fun data(): Collection<TestCase> =
+ @Parameters(name = "{0}") @JvmStatic fun data(): Collection<TestCase> = testData
+
+ private val testData: List<TestCase> =
listOf(
// Enabled = false => no networks shown
TestCase(
@@ -215,11 +229,12 @@
network = WifiNetworkModel.Inactive,
expected =
Expected(
- iconResource = WifiIcons.WIFI_NO_NETWORK,
+ iconResource = WIFI_NO_NETWORK,
contentDescription = { context ->
"${context.getString(WIFI_NO_CONNECTION)}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No network icon",
),
),
TestCase(
@@ -231,7 +246,8 @@
contentDescription = { context ->
"${context.getString(WIFI_CONNECTION_STRENGTH[4])}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No internet level 4 icon",
),
),
TestCase(
@@ -242,7 +258,8 @@
iconResource = WIFI_FULL_ICONS[2],
contentDescription = { context ->
context.getString(WIFI_CONNECTION_STRENGTH[2])
- }
+ },
+ description = "Full internet level 2 icon",
),
),
@@ -252,11 +269,12 @@
network = WifiNetworkModel.Inactive,
expected =
Expected(
- iconResource = WifiIcons.WIFI_NO_NETWORK,
+ iconResource = WIFI_NO_NETWORK,
contentDescription = { context ->
"${context.getString(WIFI_NO_CONNECTION)}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No network icon",
),
),
TestCase(
@@ -268,7 +286,8 @@
contentDescription = { context ->
"${context.getString(WIFI_CONNECTION_STRENGTH[2])}," +
context.getString(NO_INTERNET)
- }
+ },
+ description = "No internet level 2 icon",
),
),
TestCase(
@@ -279,7 +298,8 @@
iconResource = WIFI_FULL_ICONS[0],
contentDescription = { context ->
context.getString(WIFI_CONNECTION_STRENGTH[0])
- }
+ },
+ description = "Full internet level 0 icon",
),
),
@@ -309,7 +329,8 @@
iconResource = WIFI_FULL_ICONS[4],
contentDescription = { context ->
context.getString(WIFI_CONNECTION_STRENGTH[4])
- }
+ },
+ description = "Full internet level 4 icon",
),
),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
index 15ba672..4ca1fd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
@@ -26,6 +26,7 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertTrue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +36,7 @@
private val serializer = IpcSerializer()
+ @Ignore("b/253046405")
@Test
fun serializeManyIncomingIpcs(): Unit = runBlocking(Dispatchers.Main.immediate) {
val processor = launch(start = CoroutineStart.LAZY) { serializer.process() }
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/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 4018be1..0f101b0 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -42,6 +42,7 @@
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
+import android.app.assist.ActivityId;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.ContentResolver;
@@ -916,13 +917,16 @@
return mGlobalContentCaptureOptions.getOptions(userId, packageName);
}
+ // ErrorProne says ContentCaptureManagerService.this.mLock needs to be guarded by
+ // 'service.mLock', which is the same as mLock.
+ @SuppressWarnings("GuardedBy")
@Override
public void notifyActivityEvent(int userId, @NonNull ComponentName activityComponent,
- @ActivityEventType int eventType) {
+ @ActivityEventType int eventType, @NonNull ActivityId activityId) {
synchronized (mLock) {
final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
- service.onActivityEventLocked(activityComponent, eventType);
+ service.onActivityEventLocked(activityId, activityComponent, eventType);
}
}
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index a08687f..cee78e0 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -548,12 +548,13 @@
}
@GuardedBy("mLock")
- void onActivityEventLocked(@NonNull ComponentName componentName, @ActivityEventType int type) {
+ void onActivityEventLocked(@NonNull ActivityId activityId,
+ @NonNull ComponentName componentName, @ActivityEventType int type) {
if (mRemoteService == null) {
if (mMaster.debug) Slog.d(mTag, "onActivityEvent(): no remote service");
return;
}
- final ActivityEvent event = new ActivityEvent(componentName, type);
+ final ActivityEvent event = new ActivityEvent(activityId, componentName, type);
if (mMaster.verbose) Slog.v(mTag, "onActivityEvent(): " + event);
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 285c406..41044bf 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -16,12 +16,18 @@
package android.os;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
import com.android.internal.os.BinderCallsStats;
import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
import java.util.List;
+
/**
* Battery stats local system service interface. This is used to pass internal data out of
* BatteryStatsImpl, as well as make unchecked calls into BatteryStatsImpl.
@@ -29,6 +35,19 @@
* @hide Only for use within Android OS.
*/
public abstract class BatteryStatsInternal {
+
+ public static final int CPU_WAKEUP_SUBSYSTEM_UNKNOWN = -1;
+ public static final int CPU_WAKEUP_SUBSYSTEM_ALARM = 1;
+
+ /** @hide */
+ @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = {
+ CPU_WAKEUP_SUBSYSTEM_UNKNOWN,
+ CPU_WAKEUP_SUBSYSTEM_ALARM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface CpuWakeupSubsystem {
+ }
+
/**
* Returns the wifi interfaces.
*/
@@ -56,9 +75,10 @@
/**
* Inform battery stats how many deferred jobs existed when the app got launched and how
* long ago was the last job execution for the app.
- * @param uid the uid of the app.
+ *
+ * @param uid the uid of the app.
* @param numDeferred number of deferred jobs.
- * @param sinceLast how long in millis has it been since a job was run
+ * @param sinceLast how long in millis has it been since a job was run
*/
public abstract void noteJobsDeferred(int uid, int numDeferred, long sinceLast);
@@ -72,4 +92,11 @@
* Informs battery stats of native thread IDs of threads taking incoming binder calls.
*/
public abstract void noteBinderThreadNativeIds(int[] binderThreadNativeTids);
+
+ /**
+ * Reports any activity that could potentially have caused the CPU to wake up.
+ * Accepts a timestamp to allow the reporter to report it before or after the event.
+ */
+ public abstract void noteCpuWakingActivity(@CpuWakeupSubsystem int subsystem,
+ long elapsedMillis, @NonNull int... uids);
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ee13118..c837051 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -204,6 +204,7 @@
import android.app.ProfilerInfo;
import android.app.SyncNotedAppOp;
import android.app.WaitResult;
+import android.app.assist.ActivityId;
import android.app.backup.BackupManager.OperationType;
import android.app.backup.IBackupManager;
import android.app.compat.CompatChanges;
@@ -2915,7 +2916,7 @@
* @param taskRoot Task's root
*/
public void updateActivityUsageStats(ComponentName activity, int userId, int event,
- IBinder appToken, ComponentName taskRoot) {
+ IBinder appToken, ComponentName taskRoot, ActivityId activityId) {
if (DEBUG_SWITCH) {
Slog.d(TAG_SWITCH, "updateActivityUsageStats: comp="
+ activity + " hash=" + appToken.hashCode() + " event=" + event);
@@ -2932,7 +2933,7 @@
if (contentCaptureService != null && (event == Event.ACTIVITY_PAUSED
|| event == Event.ACTIVITY_RESUMED || event == Event.ACTIVITY_STOPPED
|| event == Event.ACTIVITY_DESTROYED)) {
- contentCaptureService.notifyActivityEvent(userId, activity, event);
+ contentCaptureService.notifyActivityEvent(userId, activity, event, activityId);
}
// Currently we have move most of logic to the client side. When the activity lifecycle
// event changed, the client side will notify the VoiceInteractionManagerService. But
@@ -17106,9 +17107,9 @@
@Override
public void updateActivityUsageStats(ComponentName activity, int userId, int event,
- IBinder appToken, ComponentName taskRoot) {
+ IBinder appToken, ComponentName taskRoot, ActivityId activityId) {
ActivityManagerService.this.updateActivityUsageStats(activity, userId, event,
- appToken, taskRoot);
+ appToken, taskRoot, activityId);
}
@Override
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index d9d29d65..75d1f68 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -81,9 +81,11 @@
import android.telephony.ModemActivityInfo;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.StatsEvent;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.BackgroundThread;
@@ -104,6 +106,7 @@
import com.android.server.power.stats.BatteryStatsImpl;
import com.android.server.power.stats.BatteryUsageStatsProvider;
import com.android.server.power.stats.BatteryUsageStatsStore;
+import com.android.server.power.stats.CpuWakeupStats;
import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
import java.io.File;
@@ -120,6 +123,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -142,6 +146,8 @@
private final PowerProfile mPowerProfile;
final BatteryStatsImpl mStats;
+ @GuardedBy("mWakeupStats")
+ final CpuWakeupStats mCpuWakeupStats;
private final BatteryUsageStatsStore mBatteryUsageStatsStore;
private final BatteryStatsImpl.UserInfoProvider mUserManagerUserInfoProvider;
private final Context mContext;
@@ -373,6 +379,7 @@
}
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mStats,
mBatteryUsageStatsStore);
+ mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map);
}
public void publish() {
@@ -464,6 +471,12 @@
mStats.noteBinderThreadNativeIds(binderThreadNativeTids);
}
}
+
+ @Override
+ public void noteCpuWakingActivity(int subsystem, long elapsedMillis, int... uids) {
+ Objects.requireNonNull(uids);
+ mCpuWakeupStats.noteWakingActivity(subsystem, elapsedMillis, uids);
+ }
}
@Override
@@ -2251,12 +2264,13 @@
try {
String reason;
while ((reason = waitWakeup()) != null) {
+ final long nowElapsed = SystemClock.elapsedRealtime();
+ final long nowUptime = SystemClock.uptimeMillis();
// Wait for the completion of pending works if there is any
awaitCompletion();
-
+ mCpuWakeupStats.noteWakeupTimeAndReason(nowElapsed, nowUptime, reason);
synchronized (mStats) {
- mStats.noteWakeupReasonLocked(reason,
- SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());
+ mStats.noteWakeupReasonLocked(reason, nowElapsed, nowUptime);
}
}
} catch (RuntimeException e) {
@@ -2312,6 +2326,7 @@
pw.println(" --read-daily: read-load last written daily stats.");
pw.println(" --settings: dump the settings key/values related to batterystats");
pw.println(" --cpu: dump cpu stats for debugging purpose");
+ pw.println(" --wakeups: dump CPU wakeup history and attribution.");
pw.println(" --power-profile: dump the power profile constants");
pw.println(" --usage: write battery usage stats. Optional arguments:");
pw.println(" --proto: output as a binary protobuffer");
@@ -2567,6 +2582,10 @@
}
dumpUsageStatsToProto(fd, pw, model, proto);
return;
+ } else if ("--wakeups".equals(arg)) {
+ mCpuWakeupStats.dump(new IndentingPrintWriter(pw, " "),
+ SystemClock.elapsedRealtime());
+ return;
} else if ("-a".equals(arg)) {
flags |= BatteryStats.DUMP_VERBOSE;
} else if (arg.length() > 0 && arg.charAt(0) == '-'){
@@ -2697,12 +2716,16 @@
} else {
if (DBG) Slog.d(TAG, "begin dumpLocked from UID " + Binder.getCallingUid());
awaitCompletion();
+
synchronized (mStats) {
mStats.dumpLocked(mContext, pw, flags, reqUid, historyStart);
if (writeData) {
mStats.writeAsyncLocked();
}
}
+ pw.println();
+ mCpuWakeupStats.dump(new IndentingPrintWriter(pw, " "), SystemClock.elapsedRealtime());
+
if (DBG) Slog.d(TAG, "end dumpLocked");
}
}
diff --git a/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java b/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
index c33b5f1..0812fd9 100644
--- a/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
+++ b/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.assist.ActivityId;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.os.Bundle;
@@ -61,5 +62,6 @@
* Notifies the intelligence service of a high-level activity event for the given user.
*/
public abstract void notifyActivityEvent(@UserIdInt int userId,
- @NonNull ComponentName activityComponent, @ActivityEventType int eventType);
+ @NonNull ComponentName activityComponent, @ActivityEventType int eventType,
+ @NonNull ActivityId activityId);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2be84d0..8481560 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -105,7 +105,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 +189,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;
@@ -339,7 +338,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 +1649,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 +1930,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");
@@ -1946,7 +1943,8 @@
!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 +2963,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 +2978,8 @@
@GuardedBy("ImfLock.class")
private void hideStatusBarIconLocked() {
- if (mStatusBar != null) {
- mStatusBar.setIconVisibility(mSlotIme, false);
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setIconVisibility(mSlotIme, false);
}
}
@@ -3160,9 +3158,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/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 4b18add..77fea09 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4872,6 +4872,13 @@
}
@Override
+ public int getHintsFromListenerNoToken() {
+ synchronized (mNotificationLock) {
+ return mListenerHints;
+ }
+ }
+
+ @Override
public void requestInterruptionFilterFromListener(INotificationListener token,
int interruptionFilter) throws RemoteException {
final long identity = Binder.clearCallingIdentity();
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/pm/DumpHelper.java b/services/core/java/com/android/server/pm/DumpHelper.java
index d2a5e32..3385a09 100644
--- a/services/core/java/com/android/server/pm/DumpHelper.java
+++ b/services/core/java/com/android/server/pm/DumpHelper.java
@@ -26,6 +26,7 @@
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.incremental.PerUidReadTimeouts;
import android.service.pm.PackageServiceDumpProto;
@@ -60,6 +61,7 @@
private final ArrayMap<String, FeatureInfo> mAvailableFeatures;
private final ArraySet<String> mProtectedBroadcasts;
private final PerUidReadTimeouts[] mPerUidReadTimeouts;
+ private final SnapshotStatistics mSnapshotStatistics;
DumpHelper(
PermissionManagerServiceInternal permissionManager,
@@ -70,7 +72,8 @@
ChangedPackagesTracker changedPackagesTracker,
ArrayMap<String, FeatureInfo> availableFeatures,
ArraySet<String> protectedBroadcasts,
- PerUidReadTimeouts[] perUidReadTimeouts) {
+ PerUidReadTimeouts[] perUidReadTimeouts,
+ SnapshotStatistics snapshotStatistics) {
mPermissionManager = permissionManager;
mStorageEventHelper = storageEventHelper;
mDomainVerificationManager = domainVerificationManager;
@@ -81,6 +84,7 @@
mAvailableFeatures = availableFeatures;
mProtectedBroadcasts = protectedBroadcasts;
mPerUidReadTimeouts = perUidReadTimeouts;
+ mSnapshotStatistics = snapshotStatistics;
}
@NeverCompile // Avoid size overhead of debugging code.
@@ -585,6 +589,9 @@
if (dumpState.onTitlePrinted()) {
pw.println();
}
+ pw.println("Snapshot statistics:");
+ mSnapshotStatistics.dump(pw, " " /* indent */, SystemClock.currentTimeMicro(),
+ snapshot.getUsed(), dumpState.isBrief());
}
if (!checkin
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index e9f26e9..022bf3c 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -81,9 +81,11 @@
import android.content.pm.InstallationFileParcel;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.PreapprovalDetails;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
import android.content.pm.dex.DexMetadataHelper;
@@ -92,8 +94,13 @@
import android.content.pm.parsing.PackageLite;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
+import android.content.res.ApkAssets;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.icu.util.ULocale;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -193,6 +200,7 @@
private static final int MSG_INSTALL = 3;
private static final int MSG_ON_PACKAGE_INSTALLED = 4;
private static final int MSG_SESSION_VALIDATION_FAILURE = 5;
+ private static final int MSG_PRE_APPROVAL_REQUEST = 6;
/** XML constants used for persisting a session */
static final String TAG_SESSION = "session";
@@ -275,7 +283,7 @@
* #mValidatedTargetSdk} is compared with {@link Build.VERSION_CODES#R} before getting the
* target sdk version from a validated apk in {@link #validateApkInstallLocked()}, the compared
* result will not trigger any user action in
- * {@link #checkUserActionRequirement(PackageInstallerSession)}.
+ * {@link #checkUserActionRequirement(PackageInstallerSession, IntentSender)}.
*/
private static final int INVALID_TARGET_SDK_VERSION = Integer.MAX_VALUE;
@@ -360,6 +368,7 @@
@GuardedBy("mLock")
private boolean mShouldBeSealed = false;
+ private final AtomicBoolean mPreapprovalRequested = new AtomicBoolean(false);
private final AtomicBoolean mCommitted = new AtomicBoolean(false);
/**
@@ -387,6 +396,9 @@
@GuardedBy("mLock")
private IntentSender mRemoteStatusReceiver;
+ @GuardedBy("mLock")
+ private PreapprovalDetails mPreapprovalDetails;
+
/** Fields derived from commit parsing */
@GuardedBy("mLock")
private String mPackageName;
@@ -740,11 +752,12 @@
final Bundle extras = (Bundle) args.arg3;
final IntentSender statusReceiver = (IntentSender) args.arg4;
final int returnCode = args.argi1;
+ final boolean isPreapproval = args.argi2 == 1;
args.recycle();
sendOnPackageInstalled(mContext, statusReceiver, sessionId,
isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId,
- packageName, returnCode, message, extras);
+ packageName, returnCode, isPreapproval, message, extras);
break;
case MSG_SESSION_VALIDATION_FAILURE:
@@ -752,6 +765,9 @@
final String detailMessage = (String) msg.obj;
onSessionValidationFailure(error, detailMessage);
break;
+ case MSG_PRE_APPROVAL_REQUEST:
+ handlePreapprovalRequest();
+ break;
}
return true;
@@ -779,10 +795,7 @@
*/
private boolean isInstallerDeviceOwnerOrAffiliatedProfileOwner() {
assertNotLocked("isInstallerDeviceOwnerOrAffiliatedProfileOwner");
- // It is safe to access mInstallerUid and mInstallSource without lock
- // because they are immutable after sealing.
- assertSealed("isInstallerDeviceOwnerOrAffiliatedProfileOwner");
- if (userId != UserHandle.getUserId(mInstallerUid)) {
+ if (userId != UserHandle.getUserId(getInstallerUid())) {
return false;
}
DevicePolicyManagerInternal dpmi =
@@ -874,7 +887,7 @@
if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid,
userId)) {
- // show the installer to account for device poslicy or unknown sources use cases
+ // show the installer to account for device policy or unknown sources use cases
return USER_ACTION_REQUIRED;
}
@@ -1031,18 +1044,22 @@
mResolvedBaseFile.getAbsolutePath() : null;
info.progress = progress;
info.sealed = mSealed;
- info.isCommitted = mCommitted.get();
+ info.isCommitted = isCommitted();
+ info.isPreapprovalRequested = isPreapprovalRequested();
info.active = mActiveCount.get() > 0;
info.mode = params.mode;
info.installReason = params.installReason;
info.installScenario = params.installScenario;
info.sizeBytes = params.sizeBytes;
- info.appPackageName = mPackageName != null ? mPackageName : params.appPackageName;
+ info.appPackageName = mPreapprovalDetails != null ? mPreapprovalDetails.getPackageName()
+ : mPackageName != null ? mPackageName : params.appPackageName;
if (includeIcon) {
- info.appIcon = params.appIcon;
+ info.appIcon = mPreapprovalDetails != null && mPreapprovalDetails.getIcon() != null
+ ? mPreapprovalDetails.getIcon() : params.appIcon;
}
- info.appLabel = params.appLabel;
+ info.appLabel =
+ mPreapprovalDetails != null ? mPreapprovalDetails.getLabel() : params.appLabel;
info.installLocation = params.installLocation;
if (!scrubData) {
@@ -1086,6 +1103,11 @@
}
}
+ /** @hide */
+ boolean isPreapprovalRequested() {
+ return mPreapprovalRequested.get();
+ }
+
/** {@hide} */
boolean isCommitted() {
return mCommitted.get();
@@ -1122,6 +1144,14 @@
}
@GuardedBy("mLock")
+ private void assertPreparedAndNotPreapprovalRequestedLocked(String cookie) {
+ assertPreparedAndNotSealedLocked(cookie);
+ if (isPreapprovalRequested()) {
+ throw new IllegalStateException(cookie + " not allowed after requesting");
+ }
+ }
+
+ @GuardedBy("mLock")
private void assertPreparedAndNotSealedLocked(String cookie) {
assertPreparedAndNotCommittedOrDestroyedLocked(cookie);
if (mSealed) {
@@ -1132,7 +1162,7 @@
@GuardedBy("mLock")
private void assertPreparedAndNotCommittedOrDestroyedLocked(String cookie) {
assertPreparedAndNotDestroyedLocked(cookie);
- if (mCommitted.get()) {
+ if (isCommitted()) {
throw new SecurityException(cookie + " not allowed after commit");
}
}
@@ -1173,7 +1203,7 @@
@GuardedBy("mProgressLock")
private void computeProgressLocked(boolean forcePublish) {
- if (!isIncrementalInstallation() || !mCommitted.get()) {
+ if (!isIncrementalInstallation() || !isCommitted()) {
mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f)
+ MathUtils.constrain(mInternalProgress * 0.2f, 0f, 0.2f);
} else {
@@ -1200,7 +1230,7 @@
assertCallerIsOwnerRootOrVerifier();
synchronized (mLock) {
assertPreparedAndNotDestroyedLocked("getNames");
- if (!mCommitted.get()) {
+ if (!isCommitted()) {
return getNamesLocked();
} else {
return getStageDirContentsLocked();
@@ -1640,11 +1670,7 @@
@Override
public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) {
- if (hasParentSessionId()) {
- throw new IllegalStateException(
- "Session " + sessionId + " is a child of multi-package session "
- + getParentSessionId() + " and may not be committed directly.");
- }
+ assertNotChild("commit");
if (!markAsSealed(statusReceiver, forTransfer)) {
return;
@@ -1708,6 +1734,20 @@
}
}
+ @WorkerThread
+ private void handlePreapprovalRequest() {
+ /**
+ * Stops the process if the session needs user action. When the user answers the yes,
+ * {@link #setPermissionsResult(boolean)} is called and then
+ * {@link #MSG_PRE_APPROVAL_REQUEST} is handled to come back here to check again.
+ */
+ if (sendPendingUserActionIntentIfNeeded()) {
+ return;
+ }
+
+ dispatchSessionPreappoved();
+ }
+
private final class FileSystemConnector extends
IPackageInstallerSessionFileSystemConnector.Stub {
final Set<String> mAddedFiles = new ArraySet<>();
@@ -1835,7 +1875,7 @@
// single / child sessions.
try {
synchronized (mLock) {
- if (mCommitted.get()) {
+ if (isCommitted()) {
return true;
}
// Read transfers from the original owner stay open, but as the session's data
@@ -2115,7 +2155,11 @@
*/
@WorkerThread
private boolean sendPendingUserActionIntentIfNeeded() {
- assertNotChild("PackageInstallerSession#sendPendingUserActionIntentIfNeeded");
+ // To support pre-approval request of atomic install, we allow child session to handle
+ // the result by itself since it has the status receiver.
+ if (isCommitted()) {
+ assertNotChild("PackageInstallerSession#sendPendingUserActionIntentIfNeeded");
+ }
final IntentSender statusReceiver = getRemoteStatusReceiver();
return sessionContains(s -> checkUserActionRequirement(s, statusReceiver));
@@ -2420,7 +2464,10 @@
// User needs to confirm installation;
// give installer an intent they can use to involve
// user.
- final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_INSTALL);
+ final boolean isPreapproval = isPreapprovalRequested() && !isCommitted();
+ final Intent intent = new Intent(
+ isPreapproval ? PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL
+ : PackageInstaller.ACTION_CONFIRM_INSTALL);
intent.setPackage(mPm.getPackageInstallerPackageName());
intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
@@ -3009,6 +3056,9 @@
}
}
}
+
+ assertPreapprovalDetailsConsistentIfNeededLocked(packageLite, pkgInfo);
+
if (packageLite.isUseEmbeddedDex()) {
for (File file : mResolvedStagedFiles) {
if (file.getName().endsWith(".apk")
@@ -3253,6 +3303,78 @@
}
}
+ @GuardedBy("mLock")
+ private void assertPreapprovalDetailsConsistentIfNeededLocked(@NonNull PackageLite packageLite,
+ @Nullable PackageInfo info) throws PackageManagerException {
+ if (mPreapprovalDetails == null || !isPreapprovalRequested()) {
+ return;
+ }
+
+ if (!TextUtils.equals(mPackageName, mPreapprovalDetails.getPackageName())) {
+ throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
+ mPreapprovalDetails + " inconsistent with " + mPackageName);
+ }
+
+ // In case the app label in PreapprovalDetails from different locale in split APK,
+ // we check all APK files to find the app label.
+ final PackageInfo packageInfo =
+ info != null ? info : mContext.getPackageManager().getPackageArchiveInfo(
+ packageLite.getPath(), PackageInfoFlags.of(0));
+ if (packageInfo == null) {
+ throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+ "Failure to obtain package info.");
+ }
+ final List<String> filePaths = packageLite.getAllApkPaths();
+ final String appLabel = mPreapprovalDetails.getLabel();
+ final ULocale appLocale = mPreapprovalDetails.getLocale();
+ final ApplicationInfo appInfo = packageInfo.applicationInfo;
+ boolean appLabelMatched = false;
+ for (int i = filePaths.size() - 1; i >= 0 && !appLabelMatched; i--) {
+ appLabelMatched |= TextUtils.equals(getAppLabel(filePaths.get(i), appLocale, appInfo),
+ appLabel);
+ }
+ if (!appLabelMatched) {
+ throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
+ mPreapprovalDetails + " inconsistent with app label");
+ }
+ }
+
+ private CharSequence getAppLabel(String path, ULocale locale, ApplicationInfo appInfo)
+ throws PackageManagerException {
+ final Resources pRes = mContext.getResources();
+ final AssetManager assetManager = new AssetManager();
+ final Configuration config = new Configuration(pRes.getConfiguration());
+ final ApkAssets apkAssets;
+ try {
+ apkAssets = ApkAssets.loadFromPath(path);
+ } catch (IOException e) {
+ throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+ "Failure to get resources from package archive " + path);
+ }
+ assetManager.setApkAssets(new ApkAssets[]{apkAssets}, false /* invalidateCaches */);
+ config.setLocale(locale.toLocale());
+ final Resources res = new Resources(assetManager, pRes.getDisplayMetrics(), config);
+ return tryLoadingAppLabel(res, appInfo);
+ }
+
+ private CharSequence tryLoadingAppLabel(@NonNull Resources res, @NonNull ApplicationInfo info) {
+ CharSequence label = null;
+ // Try to load the label from the package's resources. If an app has not explicitly
+ // specified any label, just use the package name.
+ if (info.labelRes != 0) {
+ try {
+ label = res.getText(info.labelRes);
+ } catch (Resources.NotFoundException ignore) {
+ }
+ }
+ if (label == null) {
+ label = (info.nonLocalizedLabel != null)
+ ? info.nonLocalizedLabel : info.packageName;
+ }
+
+ return label;
+ }
+
private SigningDetails unsafeGetCertsWithoutVerification(String path)
throws PackageManagerException {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
@@ -3469,11 +3591,13 @@
}
void setPermissionsResult(boolean accepted) {
- if (!isSealed()) {
+ if (!isSealed() && !isPreapprovalRequested()) {
throw new SecurityException("Must be sealed to accept permissions");
}
- PackageInstallerSession root = hasParentSessionId()
+ // To support pre-approval request of atomic install, we allow child session to handle
+ // the result by itself since it has the status receiver.
+ final PackageInstallerSession root = hasParentSessionId() && isCommitted()
? mSessionProvider.getSession(getParentSessionId()) : this;
if (accepted) {
@@ -3481,7 +3605,8 @@
synchronized (mLock) {
mPermissionsManuallyAccepted = true;
}
- root.mHandler.obtainMessage(MSG_INSTALL).sendToTarget();
+ root.mHandler.obtainMessage(
+ isCommitted() ? MSG_INSTALL : MSG_PRE_APPROVAL_REQUEST).sendToTarget();
} else {
root.destroy();
root.dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null);
@@ -3593,7 +3718,7 @@
mDestroyed = true;
r = () -> {
assertNotLocked("abandonStaged");
- if (isStaged() && mCommitted.get()) {
+ if (isStaged() && isCommitted()) {
mStagingManager.abortCommittedSession(mStagedSession);
}
destroy();
@@ -3937,7 +4062,7 @@
private boolean canBeAddedAsChild(int parentCandidate) {
synchronized (mLock) {
return (!hasParentSessionId() || mParentSessionId == parentCandidate)
- && !mCommitted.get()
+ && !isCommitted()
&& !mDestroyed;
}
}
@@ -4096,10 +4221,68 @@
args.arg3 = extras;
args.arg4 = statusReceiver;
args.argi1 = returnCode;
+ args.argi2 = isPreapprovalRequested() && !isCommitted() ? 1 : 0;
mHandler.obtainMessage(MSG_ON_PACKAGE_INSTALLED, args).sendToTarget();
}
}
+ private void dispatchSessionPreappoved() {
+ final IntentSender target = getRemoteStatusReceiver();
+ final Intent intent = new Intent();
+ intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
+ intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS);
+ intent.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, true);
+ try {
+ target.sendIntent(mContext, 0 /* code */, intent, null /* onFinished */,
+ null /* handler */);
+ } catch (IntentSender.SendIntentException ignored) {
+ }
+ }
+
+ @Override
+ public void requestUserPreapproval(@NonNull PreapprovalDetails details,
+ @NonNull IntentSender statusReceiver) {
+ validatePreapprovalRequest(details, statusReceiver);
+ dispatchPreapprovalRequest();
+ }
+
+ /**
+ * Validates whether the necessary information (e.g., PreapprovalDetails) are provided.
+ */
+ private void validatePreapprovalRequest(@NonNull PreapprovalDetails details,
+ @NonNull IntentSender statusReceiver) {
+ assertCallerIsOwnerOrRoot();
+ if (isMultiPackage()) {
+ throw new IllegalStateException(
+ "Session " + sessionId + " is a parent of multi-package session and "
+ + "requestUserPreapproval on the parent session isn't supported.");
+ }
+
+ synchronized (mLock) {
+ assertPreparedAndNotSealedLocked("request of session " + sessionId);
+ mPreapprovalDetails = details;
+ setRemoteStatusReceiver(statusReceiver);
+ }
+ }
+
+ private void dispatchPreapprovalRequest() {
+ synchronized (mLock) {
+ assertPreparedAndNotPreapprovalRequestedLocked("dispatchPreapprovalRequest");
+ }
+
+ // Mark this session are pre-approval requested, and ready to progress to the next phase.
+ markAsPreapprovalRequested();
+
+ mHandler.obtainMessage(MSG_PRE_APPROVAL_REQUEST).sendToTarget();
+ }
+
+ /**
+ * Marks this session as pre-approval requested, and prevents further related modification.
+ */
+ private void markAsPreapprovalRequested() {
+ mPreapprovalRequested.set(true);
+ }
+
void setSessionReady() {
synchronized (mLock) {
// Do not allow destroyed/failed session to change state
@@ -4265,6 +4448,7 @@
pw.printPair("mClientProgress", clientProgress);
pw.printPair("mProgress", progress);
pw.printPair("mCommitted", mCommitted);
+ pw.printPair("mPreapprovalRequested", mPreapprovalRequested);
pw.printPair("mSealed", mSealed);
pw.printPair("mPermissionsManuallyAccepted", mPermissionsManuallyAccepted);
pw.printPair("mStageDirInUse", mStageDirInUse);
@@ -4282,6 +4466,7 @@
pw.printPair("mSessionReady", mSessionReady);
pw.printPair("mSessionErrorCode", mSessionErrorCode);
pw.printPair("mSessionErrorMessage", mSessionErrorMessage);
+ pw.printPair("mPreapprovalDetails", mPreapprovalDetails);
pw.println();
pw.decreaseIndent();
@@ -4295,6 +4480,8 @@
final Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_USER_ACTION);
+ fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL,
+ PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()));
fillIn.putExtra(Intent.EXTRA_INTENT, intent);
try {
target.sendIntent(context, 0, fillIn, null, null);
@@ -4307,7 +4494,7 @@
*/
private static void sendOnPackageInstalled(Context context, IntentSender target, int sessionId,
boolean showNotification, int userId, String basePackageName, int returnCode,
- String msg, Bundle extras) {
+ boolean isPreapproval, String msg, Bundle extras) {
if (INSTALL_SUCCEEDED == returnCode && showNotification) {
boolean update = (extras != null) && extras.getBoolean(Intent.EXTRA_REPLACING);
Notification notification = PackageInstallerService.buildSuccessNotification(context,
@@ -4330,6 +4517,7 @@
fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE,
PackageManager.installStatusToString(returnCode, msg));
fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
+ fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, isPreapproval);
if (extras != null) {
final String existing = extras.getString(
PackageManager.EXTRA_FAILURE_EXISTING_PACKAGE);
@@ -4448,7 +4636,7 @@
writeStringAttribute(out, ATTR_SESSION_STAGE_CID, stageCid);
}
writeBooleanAttribute(out, ATTR_PREPARED, mPrepared);
- writeBooleanAttribute(out, ATTR_COMMITTED, mCommitted.get());
+ writeBooleanAttribute(out, ATTR_COMMITTED, isCommitted());
writeBooleanAttribute(out, ATTR_DESTROYED, mDestroyed);
writeBooleanAttribute(out, ATTR_SEALED, mSealed);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index d939ca6..8fed153 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6031,7 +6031,7 @@
new DumpHelper(mPermissionManager, mStorageEventHelper,
mDomainVerificationManager, mInstallerService, mRequiredVerifierPackages,
knownPackages, mChangedPackagesTracker, availableFeatures, protectedBroadcasts,
- getPerUidReadTimeouts(snapshot)
+ getPerUidReadTimeouts(snapshot), mSnapshotStatistics
).doDump(snapshot, fd, pw, args);
}
}
diff --git a/services/core/java/com/android/server/pm/SnapshotStatistics.java b/services/core/java/com/android/server/pm/SnapshotStatistics.java
index 95f80de..2cfc894 100644
--- a/services/core/java/com/android/server/pm/SnapshotStatistics.java
+++ b/services/core/java/com/android/server/pm/SnapshotStatistics.java
@@ -240,11 +240,6 @@
public int mTotalUsed = 0;
/**
- * The total number of times a snapshot was bypassed because corking was in effect.
- */
- public int mTotalCorked = 0;
-
- /**
* The total number of builds that count as big, which means they took longer than
* SNAPSHOT_BIG_BUILD_TIME_NS.
*/
@@ -297,13 +292,6 @@
}
}
- /**
- * Record a cork.
- */
- private void corked() {
- mTotalCorked++;
- }
-
private Stats(long now) {
mStartTimeUs = now;
mTimes = new int[mTimeBins.count()];
@@ -321,7 +309,6 @@
mUsed = Arrays.copyOf(orig.mUsed, orig.mUsed.length);
mTotalBuilds = orig.mTotalBuilds;
mTotalUsed = orig.mTotalUsed;
- mTotalCorked = orig.mTotalCorked;
mBigBuilds = orig.mBigBuilds;
mShortLived = orig.mShortLived;
mTotalTimeUs = orig.mTotalTimeUs;
@@ -379,7 +366,6 @@
* Dump the summary statistics record. Choose the header or the data.
* number of builds
* number of uses
- * number of corks
* number of big builds
* number of short lifetimes
* cumulative build time, in seconds
@@ -388,13 +374,13 @@
private void dumpStats(PrintWriter pw, String indent, long now, boolean header) {
dumpPrefix(pw, indent, now, header, "Summary stats");
if (header) {
- pw.format(Locale.US, " %10s %10s %10s %10s %10s %10s %10s",
- "TotBlds", "TotUsed", "TotCork", "BigBlds", "ShortLvd",
+ pw.format(Locale.US, " %10s %10s %10s %10s %10s %10s",
+ "TotBlds", "TotUsed", "BigBlds", "ShortLvd",
"TotTime", "MaxTime");
} else {
pw.format(Locale.US,
- " %10d %10d %10d %10d %10d %10d %10d",
- mTotalBuilds, mTotalUsed, mTotalCorked, mBigBuilds, mShortLived,
+ " %10d %10d %10d %10d %10d %10d",
+ mTotalBuilds, mTotalUsed, mBigBuilds, mShortLived,
mTotalTimeUs / 1000, mMaxBuildTimeUs / 1000);
}
pw.println();
@@ -559,16 +545,6 @@
}
/**
- * Record a corked snapshot request.
- */
- public final void corked() {
- synchronized (mLock) {
- mShort[0].corked();
- mLong[0].corked();
- }
- }
-
- /**
* Roll a stats array. Shift the elements up an index and create a new element at
* index zero. The old element zero is completed with the specified time.
*/
@@ -624,8 +600,7 @@
* Dump the statistics. The format is compatible with the PackageManager dumpsys
* output.
*/
- public void dump(PrintWriter pw, String indent, long now, int unrecorded,
- int corkLevel, boolean brief) {
+ public void dump(PrintWriter pw, String indent, long now, int unrecorded, boolean brief) {
// Grab the raw statistics under lock, but print them outside of the lock.
Stats[] l;
Stats[] s;
@@ -635,8 +610,7 @@
s = Arrays.copyOf(mShort, mShort.length);
s[0] = new Stats(s[0]);
}
- pw.format(Locale.US, "%s Unrecorded-hits: %d Cork-level: %d", indent,
- unrecorded, corkLevel);
+ pw.format(Locale.US, "%s Unrecorded-hits: %d", indent, unrecorded);
pw.println();
dump(pw, indent, now, l, s, "stats");
if (brief) {
diff --git a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java
new file mode 100644
index 0000000..5f76fbc
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java
@@ -0,0 +1,455 @@
+/*
+ * 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.power.stats;
+
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
+import android.util.LongSparseArray;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseLongArray;
+import android.util.TimeSparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.IntPair;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Stores stats about CPU wakeups and tries to attribute them to subsystems and uids.
+ */
+public class CpuWakeupStats {
+ private static final String SUBSYSTEM_ALARM_STRING = "Alarm";
+ @VisibleForTesting
+ static final long WAKEUP_RETENTION_MS = 3 * 24 * 60 * 60_000; // 3 days.
+ @VisibleForTesting
+ static final long WAKEUP_REASON_HALF_WINDOW_MS = 500;
+
+ private final IrqDeviceMap mIrqDeviceMap;
+ private final WakingActivityHistory mRecentWakingActivity = new WakingActivityHistory();
+
+ @VisibleForTesting
+ final TimeSparseArray<Wakeup> mWakeupEvents = new TimeSparseArray<>();
+ @VisibleForTesting
+ final TimeSparseArray<SparseArray<SparseBooleanArray>> mWakeupAttribution =
+ new TimeSparseArray<>();
+
+ public CpuWakeupStats(Context context, int mapRes) {
+ mIrqDeviceMap = IrqDeviceMap.getInstance(context, mapRes);
+ }
+
+ /** Notes a wakeup reason as reported by SuspendControlService to battery stats. */
+ public synchronized void noteWakeupTimeAndReason(long elapsedRealtime, long uptime,
+ String rawReason) {
+ final Wakeup parsedWakeup = new Wakeup(rawReason, elapsedRealtime, uptime);
+ mWakeupEvents.put(elapsedRealtime, parsedWakeup);
+ attemptAttributionFor(parsedWakeup);
+ // Assuming that wakeups always arrive in monotonically increasing elapsedRealtime order,
+ // we can delete all history that will not be useful in attributing future wakeups.
+ mRecentWakingActivity.clearAllBefore(elapsedRealtime - WAKEUP_REASON_HALF_WINDOW_MS);
+
+ // Limit history of wakeups and their attribution to the last WAKEUP_RETENTION_MS. Note that
+ // the last wakeup and its attribution (if computed) is always stored, even if that wakeup
+ // had occurred before WAKEUP_RETENTION_MS.
+ int lastIdx = mWakeupEvents.closestIndexOnOrBefore(elapsedRealtime - WAKEUP_RETENTION_MS);
+ for (int i = lastIdx; i >= 0; i--) {
+ mWakeupEvents.removeAt(i);
+ }
+ lastIdx = mWakeupAttribution.closestIndexOnOrBefore(elapsedRealtime - WAKEUP_RETENTION_MS);
+ for (int i = lastIdx; i >= 0; i--) {
+ mWakeupAttribution.removeAt(i);
+ }
+ }
+
+ /** Notes a waking activity that could have potentially woken up the CPU. */
+ public synchronized void noteWakingActivity(int subsystem, long elapsedRealtime, int... uids) {
+ if (!attemptAttributionWith(subsystem, elapsedRealtime, uids)) {
+ mRecentWakingActivity.recordActivity(subsystem, elapsedRealtime, uids);
+ }
+ }
+
+ private synchronized void attemptAttributionFor(Wakeup wakeup) {
+ final SparseBooleanArray subsystems = getResponsibleSubsystemsForWakeup(wakeup);
+ if (subsystems == null) {
+ // We don't support attribution for this kind of wakeup yet.
+ return;
+ }
+
+ SparseArray<SparseBooleanArray> attribution = mWakeupAttribution.get(wakeup.mElapsedMillis);
+ if (attribution == null) {
+ attribution = new SparseArray<>();
+ mWakeupAttribution.put(wakeup.mElapsedMillis, attribution);
+ }
+
+ for (int subsystemIdx = 0; subsystemIdx < subsystems.size(); subsystemIdx++) {
+ final int subsystem = subsystems.keyAt(subsystemIdx);
+
+ // Blame all activity that happened WAKEUP_REASON_HALF_WINDOW_MS before or after
+ // the wakeup from each responsible subsystem.
+ final long startTime = wakeup.mElapsedMillis - WAKEUP_REASON_HALF_WINDOW_MS;
+ final long endTime = wakeup.mElapsedMillis + WAKEUP_REASON_HALF_WINDOW_MS;
+
+ final SparseBooleanArray uidsToBlame = mRecentWakingActivity.removeBetween(subsystem,
+ startTime, endTime);
+ attribution.put(subsystem, uidsToBlame);
+ }
+ }
+
+ private synchronized boolean attemptAttributionWith(int subsystem, long activityElapsed,
+ int... uids) {
+ final int startIdx = mWakeupEvents.closestIndexOnOrAfter(
+ activityElapsed - WAKEUP_REASON_HALF_WINDOW_MS);
+ final int endIdx = mWakeupEvents.closestIndexOnOrBefore(
+ activityElapsed + WAKEUP_REASON_HALF_WINDOW_MS);
+
+ for (int wakeupIdx = startIdx; wakeupIdx <= endIdx; wakeupIdx++) {
+ final Wakeup wakeup = mWakeupEvents.valueAt(wakeupIdx);
+ final SparseBooleanArray subsystems = getResponsibleSubsystemsForWakeup(wakeup);
+ if (subsystems == null) {
+ // Unsupported for attribution
+ continue;
+ }
+ if (subsystems.get(subsystem)) {
+ // We don't expect more than one wakeup to be found within such a short window, so
+ // just attribute this one and exit
+ SparseArray<SparseBooleanArray> attribution = mWakeupAttribution.get(
+ wakeup.mElapsedMillis);
+ if (attribution == null) {
+ attribution = new SparseArray<>();
+ mWakeupAttribution.put(wakeup.mElapsedMillis, attribution);
+ }
+ SparseBooleanArray uidsToBlame = attribution.get(subsystem);
+ if (uidsToBlame == null) {
+ uidsToBlame = new SparseBooleanArray(uids.length);
+ attribution.put(subsystem, uidsToBlame);
+ }
+ for (final int uid : uids) {
+ uidsToBlame.put(uid, true);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Dumps the relevant stats for cpu wakeups and their attribution to subsystem and uids */
+ public synchronized void dump(IndentingPrintWriter pw, long nowElapsed) {
+ pw.println("CPU wakeup stats:");
+ pw.increaseIndent();
+
+ mIrqDeviceMap.dump(pw);
+ pw.println();
+
+ mRecentWakingActivity.dump(pw, nowElapsed);
+ pw.println();
+
+ final SparseLongArray attributionStats = new SparseLongArray();
+ pw.println("Wakeup events:");
+ pw.increaseIndent();
+ for (int i = mWakeupEvents.size() - 1; i >= 0; i--) {
+ TimeUtils.formatDuration(mWakeupEvents.keyAt(i), nowElapsed, pw);
+ pw.println(":");
+
+ pw.increaseIndent();
+ final Wakeup wakeup = mWakeupEvents.valueAt(i);
+ pw.println(wakeup);
+ pw.print("Attribution: ");
+ final SparseArray<SparseBooleanArray> attribution = mWakeupAttribution.get(
+ wakeup.mElapsedMillis);
+ if (attribution == null) {
+ pw.println("N/A");
+ } else {
+ for (int subsystemIdx = 0; subsystemIdx < attribution.size(); subsystemIdx++) {
+ if (subsystemIdx > 0) {
+ pw.print(", ");
+ }
+ final long counters = attributionStats.get(attribution.keyAt(subsystemIdx),
+ IntPair.of(0, 0));
+ int attributed = IntPair.first(counters);
+ final int total = IntPair.second(counters) + 1;
+
+ pw.print("subsystem: " + subsystemToString(attribution.keyAt(subsystemIdx)));
+ pw.print(", uids: [");
+ final SparseBooleanArray uids = attribution.valueAt(subsystemIdx);
+ if (uids != null) {
+ for (int uidIdx = 0; uidIdx < uids.size(); uidIdx++) {
+ if (uidIdx > 0) {
+ pw.print(", ");
+ }
+ UserHandle.formatUid(pw, uids.keyAt(uidIdx));
+ }
+ attributed++;
+ }
+ pw.print("]");
+
+ attributionStats.put(attribution.keyAt(subsystemIdx),
+ IntPair.of(attributed, total));
+ }
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+ pw.decreaseIndent();
+
+ pw.println("Attribution stats:");
+ pw.increaseIndent();
+ for (int i = 0; i < attributionStats.size(); i++) {
+ pw.print("Subsystem " + subsystemToString(attributionStats.keyAt(i)));
+ pw.print(": ");
+ final long ratio = attributionStats.valueAt(i);
+ pw.println(IntPair.first(ratio) + "/" + IntPair.second(ratio));
+ }
+ pw.println("Total: " + mWakeupEvents.size());
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ private static final class WakingActivityHistory {
+ private static final long WAKING_ACTIVITY_RETENTION_MS = 3 * 60 * 60_000; // 3 hours.
+ private SparseArray<TimeSparseArray<SparseBooleanArray>> mWakingActivity =
+ new SparseArray<>();
+
+ void recordActivity(int subsystem, long elapsedRealtime, int... uids) {
+ if (uids == null) {
+ return;
+ }
+ TimeSparseArray<SparseBooleanArray> wakingActivity = mWakingActivity.get(subsystem);
+ if (wakingActivity == null) {
+ wakingActivity = new TimeSparseArray<>();
+ mWakingActivity.put(subsystem, wakingActivity);
+ }
+ SparseBooleanArray uidsToBlame = wakingActivity.get(elapsedRealtime);
+ if (uidsToBlame == null) {
+ uidsToBlame = new SparseBooleanArray(uids.length);
+ wakingActivity.put(elapsedRealtime, uidsToBlame);
+ }
+ for (int i = 0; i < uids.length; i++) {
+ uidsToBlame.put(uids[i], true);
+ }
+ // Limit activity history per subsystem to the last WAKING_ACTIVITY_RETENTION_MS.
+ // Note that the last activity is always present, even if it occurred before
+ // WAKING_ACTIVITY_RETENTION_MS.
+ final int endIdx = wakingActivity.closestIndexOnOrBefore(
+ elapsedRealtime - WAKING_ACTIVITY_RETENTION_MS);
+ for (int i = endIdx; i >= 0; i--) {
+ wakingActivity.removeAt(endIdx);
+ }
+ }
+
+ void clearAllBefore(long elapsedRealtime) {
+ for (int subsystemIdx = mWakingActivity.size() - 1; subsystemIdx >= 0; subsystemIdx--) {
+ final TimeSparseArray<SparseBooleanArray> activityPerSubsystem =
+ mWakingActivity.valueAt(subsystemIdx);
+ final int endIdx = activityPerSubsystem.closestIndexOnOrBefore(elapsedRealtime);
+ for (int removeIdx = endIdx; removeIdx >= 0; removeIdx--) {
+ activityPerSubsystem.removeAt(removeIdx);
+ }
+ // Generally waking activity is a high frequency occurrence for any subsystem, so we
+ // don't delete the TimeSparseArray even if it is now empty, to avoid object churn.
+ // This will leave one TimeSparseArray per subsystem, which are few right now.
+ }
+ }
+
+ SparseBooleanArray removeBetween(int subsystem, long startElapsed, long endElapsed) {
+ final SparseBooleanArray uidsToReturn = new SparseBooleanArray();
+
+ final TimeSparseArray<SparseBooleanArray> activityForSubsystem =
+ mWakingActivity.get(subsystem);
+ if (activityForSubsystem != null) {
+ final int startIdx = activityForSubsystem.closestIndexOnOrAfter(startElapsed);
+ final int endIdx = activityForSubsystem.closestIndexOnOrBefore(endElapsed);
+ for (int i = endIdx; i >= startIdx; i--) {
+ final SparseBooleanArray uidsForTime = activityForSubsystem.valueAt(i);
+ for (int j = 0; j < uidsForTime.size(); j++) {
+ if (uidsForTime.valueAt(j)) {
+ uidsToReturn.put(uidsForTime.keyAt(j), true);
+ }
+ }
+ }
+ // More efficient to remove in a separate loop as it avoids repeatedly calling gc().
+ for (int i = endIdx; i >= startIdx; i--) {
+ activityForSubsystem.removeAt(i);
+ }
+ // Generally waking activity is a high frequency occurrence for any subsystem, so we
+ // don't delete the TimeSparseArray even if it is now empty, to avoid object churn.
+ // This will leave one TimeSparseArray per subsystem, which are few right now.
+ }
+ return uidsToReturn.size() > 0 ? uidsToReturn : null;
+ }
+
+ void dump(IndentingPrintWriter pw, long nowElapsed) {
+ pw.println("Recent waking activity:");
+ pw.increaseIndent();
+ for (int i = 0; i < mWakingActivity.size(); i++) {
+ pw.println("Subsystem " + subsystemToString(mWakingActivity.keyAt(i)) + ":");
+ final LongSparseArray<SparseBooleanArray> wakingActivity =
+ mWakingActivity.valueAt(i);
+ if (wakingActivity == null) {
+ continue;
+ }
+ pw.increaseIndent();
+ for (int j = wakingActivity.size() - 1; j >= 0; j--) {
+ TimeUtils.formatDuration(wakingActivity.keyAt(j), nowElapsed, pw);
+ final SparseBooleanArray uidsToBlame = wakingActivity.valueAt(j);
+ if (uidsToBlame == null) {
+ pw.println();
+ continue;
+ }
+ pw.print(": ");
+ for (int k = 0; k < uidsToBlame.size(); k++) {
+ if (uidsToBlame.valueAt(k)) {
+ UserHandle.formatUid(pw, uidsToBlame.keyAt(k));
+ pw.print(", ");
+ }
+ }
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ private SparseBooleanArray getResponsibleSubsystemsForWakeup(Wakeup wakeup) {
+ if (ArrayUtils.isEmpty(wakeup.mDevices)) {
+ return null;
+ }
+ final SparseBooleanArray result = new SparseBooleanArray();
+ for (final Wakeup.IrqDevice device : wakeup.mDevices) {
+ final List<String> rawSubsystems = mIrqDeviceMap.getSubsystemsForDevice(device.mDevice);
+
+ boolean anyKnownSubsystem = false;
+ if (rawSubsystems != null) {
+ for (int i = 0; i < rawSubsystems.size(); i++) {
+ final int subsystem = stringToKnownSubsystem(rawSubsystems.get(i));
+ if (subsystem != CPU_WAKEUP_SUBSYSTEM_UNKNOWN) {
+ // Just in case the xml had arbitrary subsystem names, we want to make sure
+ // that we only put the known ones into our attribution map.
+ result.put(subsystem, true);
+ anyKnownSubsystem = true;
+ }
+ }
+ }
+ if (!anyKnownSubsystem) {
+ result.put(CPU_WAKEUP_SUBSYSTEM_UNKNOWN, true);
+ }
+ }
+ return result;
+ }
+
+ static int stringToKnownSubsystem(String rawSubsystem) {
+ switch (rawSubsystem) {
+ case SUBSYSTEM_ALARM_STRING:
+ return CPU_WAKEUP_SUBSYSTEM_ALARM;
+ }
+ return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
+ }
+
+ static String subsystemToString(int subsystem) {
+ switch (subsystem) {
+ case CPU_WAKEUP_SUBSYSTEM_ALARM:
+ return SUBSYSTEM_ALARM_STRING;
+ case CPU_WAKEUP_SUBSYSTEM_UNKNOWN:
+ return "Unknown";
+ }
+ return "N/A";
+ }
+
+ private static final class Wakeup {
+ private static final String PARSER_TAG = "CpuWakeupStats.Wakeup";
+ private static final String ABORT_REASON_PREFIX = "Abort";
+ private static final Pattern sIrqPattern = Pattern.compile("(\\d+)\\s+(\\S+)");
+
+ String mRawReason;
+ long mElapsedMillis;
+ long mUptimeMillis;
+ IrqDevice[] mDevices;
+
+ Wakeup(String rawReason, long elapsedMillis, long uptimeMillis) {
+ mRawReason = rawReason;
+ mElapsedMillis = elapsedMillis;
+ mUptimeMillis = uptimeMillis;
+ mDevices = parseIrqDevices(rawReason);
+ }
+
+ private static IrqDevice[] parseIrqDevices(String rawReason) {
+ final String[] components = rawReason.split(":");
+ if (ArrayUtils.isEmpty(components) || components[0].startsWith(ABORT_REASON_PREFIX)) {
+ // We don't support parsing aborts yet.
+ return null;
+ }
+
+ int parsedDeviceCount = 0;
+ IrqDevice[] parsedDevices = new IrqDevice[components.length];
+
+ for (String component : components) {
+ final Matcher matcher = sIrqPattern.matcher(component);
+ if (matcher.find()) {
+ final int line;
+ final String device;
+ try {
+ line = Integer.parseInt(matcher.group(1));
+ device = matcher.group(2);
+ } catch (NumberFormatException e) {
+ Slog.e(PARSER_TAG,
+ "Exception while parsing device names from part: " + component, e);
+ continue;
+ }
+ parsedDevices[parsedDeviceCount++] = new IrqDevice(line, device);
+ }
+ }
+ return (parsedDeviceCount > 0) ? Arrays.copyOf(parsedDevices, parsedDeviceCount) : null;
+ }
+
+ @Override
+ public String toString() {
+ return "Wakeup{"
+ + "mRawReason='" + mRawReason + '\''
+ + ", mElapsedMillis=" + mElapsedMillis
+ + ", mUptimeMillis=" + TimeUtils.formatDuration(mUptimeMillis)
+ + ", mDevices=" + Arrays.toString(mDevices)
+ + '}';
+ }
+
+ static final class IrqDevice {
+ int mLine;
+ String mDevice;
+
+ IrqDevice(int line, String device) {
+ mLine = line;
+ mDevice = device;
+ }
+
+ @Override
+ public String toString() {
+ return "IrqDevice{" + "mLine=" + mLine + ", mDevice='" + mDevice + '\'' + '}';
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/IrqDeviceMap.java b/services/core/java/com/android/server/power/stats/IrqDeviceMap.java
new file mode 100644
index 0000000..091d18e
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/IrqDeviceMap.java
@@ -0,0 +1,137 @@
+/*
+ * 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.power.stats;
+
+import android.annotation.XmlRes;
+import android.content.Context;
+import android.content.res.XmlResourceParser;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.LongSparseArray;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses irq_device_map.xml to store a mapping of devices that can send IRQs to the CPU to
+ * subsystems that represent some logical work happening on the device that could need an IRQ.
+ */
+public class IrqDeviceMap {
+ private static final String TAG_IRQ_DEVICE_MAP = "irq-device-map";
+ private static final String TAG_DEVICE = "device";
+ private static final String TAG_SUBSYSTEM = "subsystem";
+ private static final String ATTR_NAME = "name";
+
+ private static LongSparseArray<IrqDeviceMap> sInstanceMap = new LongSparseArray<>(1);
+
+ private final ArrayMap<String, List<String>> mSubsystemsForDevice = new ArrayMap();
+
+ private IrqDeviceMap(XmlResourceParser parser) {
+ try {
+ XmlUtils.beginDocument(parser, TAG_IRQ_DEVICE_MAP);
+
+ int type;
+ String currentDevice = null;
+ final ArraySet<String> subsystems = new ArraySet<>();
+
+ while ((type = parser.getEventType()) != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_DEVICE)) {
+ currentDevice = parser.getAttributeValue(null, ATTR_NAME);
+ }
+ if (currentDevice != null && type == XmlPullParser.END_TAG
+ && parser.getName().equals(TAG_DEVICE)) {
+ final int n = subsystems.size();
+ if (n > 0) {
+ mSubsystemsForDevice.put(currentDevice,
+ Collections.unmodifiableList(new ArrayList<>(subsystems)));
+ }
+ subsystems.clear();
+ currentDevice = null;
+ }
+ if (currentDevice != null && type == XmlPullParser.START_TAG
+ && parser.getName().equals(TAG_SUBSYSTEM)) {
+ parser.next();
+ if (parser.getEventType() == XmlPullParser.TEXT) {
+ subsystems.add(parser.getText());
+ }
+ }
+ parser.next();
+ }
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ parser.close();
+ }
+ }
+
+ /**
+ * Returns an instance of IrqDeviceMap initialzed with the given context and xml resource.
+ * The xml resource should describe the mapping in a way similar to
+ * core/res/res/xml/irq_device_map.xml.
+ */
+ public static IrqDeviceMap getInstance(Context context, @XmlRes int resId) {
+ synchronized (IrqDeviceMap.class) {
+ final int idx = sInstanceMap.indexOfKey(resId);
+ if (idx >= 0) {
+ return sInstanceMap.valueAt(idx);
+ }
+ }
+ final XmlResourceParser parser = context.getResources().getXml(resId);
+ final IrqDeviceMap irqDeviceMap = new IrqDeviceMap(parser);
+ synchronized (IrqDeviceMap.class) {
+ sInstanceMap.put(resId, irqDeviceMap);
+ }
+ return irqDeviceMap;
+ }
+
+ List<String> getSubsystemsForDevice(String device) {
+ return mSubsystemsForDevice.get(device);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Irq device map:");
+ pw.increaseIndent();
+
+ final LongSparseArray<IrqDeviceMap> instanceMap;
+ synchronized (IrqDeviceMap.class) {
+ instanceMap = sInstanceMap;
+ }
+ final int idx = instanceMap.indexOfValue(this);
+ final String res = (idx >= 0) ? ("0x" + Long.toHexString(instanceMap.keyAt(idx))) : null;
+ pw.println("Loaded from xml resource: " + res);
+
+ pw.println("Map:");
+ pw.increaseIndent();
+ for (int i = 0; i < mSubsystemsForDevice.size(); i++) {
+ pw.print(mSubsystemsForDevice.keyAt(i) + ": ");
+ pw.println(mSubsystemsForDevice.valueAt(i));
+ }
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+}
diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
index fd6ec06..f744d00 100644
--- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
+++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
@@ -46,6 +46,8 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
SensorEventListener {
@@ -275,7 +277,7 @@
public void onSensorChanged(SensorEvent event) {
// Using log space to represent human sensation (Fechner's Law) instead of lux
// because lux values causes bright flashes to skew the average very high.
- addElement(event.timestamp, Math.max(0,
+ addElement(TimeUnit.NANOSECONDS.toMillis(event.timestamp), Math.max(0,
(int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER)));
updateLightSession();
mHandler.removeCallbacksAndMessages(mDelayedUpdateToken);
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/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index c524bc4..f52f0b7 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -1047,6 +1047,29 @@
}
@Override
+ public void notifyRecordingStarted(IBinder sessionToken, String recordingId, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+ "notifyRecordingStarted");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).notifyRecordingStarted(recordingId);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyRecordingStarted", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+
+ }
+
+ @Override
public void startInteractiveApp(IBinder sessionToken, int userId) {
if (DEBUG) {
Slogf.d(TAG, "BinderService#start(userId=%d)", userId);
@@ -2213,6 +2236,23 @@
}
@Override
+ public void onRequestStartRecording(Uri programUri) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestStartRecording: " + programUri);
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestStartRecording(programUri, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestStartRecording", e);
+ }
+ }
+ }
+
+ @Override
public void onRequestSigning(String id, String algorithm, String alias, byte[] data) {
synchronized (mLock) {
if (DEBUG) {
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 ce7794d..78b657b 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -256,6 +256,7 @@
import android.app.WaitResult;
import android.app.WindowConfiguration;
import android.app.admin.DevicePolicyManager;
+import android.app.assist.ActivityId;
import android.app.servertransaction.ActivityConfigurationChangeItem;
import android.app.servertransaction.ActivityLifecycleItem;
import android.app.servertransaction.ActivityRelaunchItem;
@@ -5585,7 +5586,9 @@
LocalServices.getService(ContentCaptureManagerInternal.class);
if (contentCaptureService != null) {
contentCaptureService.notifyActivityEvent(mUserId, mActivityComponent,
- ActivityEvent.TYPE_ACTIVITY_STARTED);
+ ActivityEvent.TYPE_ACTIVITY_STARTED,
+ new ActivityId(getTask() != null ? getTask().mTaskId : INVALID_TASK_ID,
+ shareableActivityToken));
}
break;
case PAUSED:
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 7d23cca..416d546 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -152,6 +152,7 @@
import android.app.ProfilerInfo;
import android.app.WaitResult;
import android.app.admin.DevicePolicyCache;
+import android.app.assist.ActivityId;
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.app.compat.CompatChanges;
@@ -4859,17 +4860,19 @@
void updateActivityUsageStats(ActivityRecord activity, int event) {
ComponentName taskRoot = null;
+ int taskId = INVALID_TASK_ID;
final Task task = activity.getTask();
if (task != null) {
final ActivityRecord rootActivity = task.getRootActivity();
if (rootActivity != null) {
taskRoot = rootActivity.mActivityComponent;
}
+ taskId = task.mTaskId;
}
-
final Message m = PooledLambda.obtainMessage(
ActivityManagerInternal::updateActivityUsageStats, mAmInternal,
- activity.mActivityComponent, activity.mUserId, event, activity.token, taskRoot);
+ activity.mActivityComponent, activity.mUserId, event, activity.token, taskRoot,
+ new ActivityId(taskId, activity.shareableActivityToken));
mH.sendMessage(m);
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
new file mode 100644
index 0000000..5e44d6c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -0,0 +1,125 @@
+/*
+ * 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 android.util.DisplayMetrics.DENSITY_DEFAULT;
+
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.Slog;
+
+import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
+
+/**
+ * The class that defines default launch params for tasks in desktop mode
+ */
+public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
+
+ private static final String TAG =
+ TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
+ private static final boolean DEBUG = false;
+
+ // Desktop mode feature flag.
+ static final boolean DESKTOP_MODE_SUPPORTED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_mode", false);
+ // Override default freeform task width when desktop mode is enabled. In dips.
+ private static final int DESKTOP_MODE_DEFAULT_WIDTH_DP = SystemProperties.getInt(
+ "persist.wm.debug.desktop_mode.default_width", 840);
+ // Override default freeform task height when desktop mode is enabled. In dips.
+ private static final int DESKTOP_MODE_DEFAULT_HEIGHT_DP = SystemProperties.getInt(
+ "persist.wm.debug.desktop_mode.default_height", 630);
+
+ private StringBuilder mLogBuilder;
+
+ @Override
+ public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
+ @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
+ @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
+ LaunchParamsController.LaunchParams currentParams,
+ LaunchParamsController.LaunchParams outParams) {
+
+ initLogBuilder(task, activity);
+ int result = calculate(task, layout, activity, source, options, request, phase,
+ currentParams, outParams);
+ outputLog();
+ return result;
+ }
+
+ private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
+ @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
+ @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
+ LaunchParamsController.LaunchParams currentParams,
+ LaunchParamsController.LaunchParams outParams) {
+
+ if (task == null) {
+ appendLog("task null, skipping");
+ return RESULT_SKIP;
+ }
+ if (phase != PHASE_BOUNDS) {
+ appendLog("not in bounds phase, skipping");
+ return RESULT_SKIP;
+ }
+ if (!task.inFreeformWindowingMode()) {
+ appendLog("not a freeform task, skipping");
+ return RESULT_SKIP;
+ }
+ if (!currentParams.mBounds.isEmpty()) {
+ appendLog("currentParams has bounds set, not overriding");
+ return RESULT_SKIP;
+ }
+
+ // Copy over any values
+ outParams.set(currentParams);
+
+ // Update width and height with default desktop mode values
+ float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT;
+ final int width = (int) (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f);
+ final int height = (int) (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f);
+ outParams.mBounds.right = width;
+ outParams.mBounds.bottom = height;
+
+ // Center the task in window bounds
+ Rect windowBounds = task.getWindowConfiguration().getBounds();
+ outParams.mBounds.offset(windowBounds.centerX() - outParams.mBounds.centerX(),
+ windowBounds.centerY() - outParams.mBounds.centerY());
+
+ appendLog("setting desktop mode task bounds to %s", outParams.mBounds);
+
+ return RESULT_DONE;
+ }
+
+ private void initLogBuilder(Task task, ActivityRecord activity) {
+ if (DEBUG) {
+ mLogBuilder = new StringBuilder(
+ "DesktopModeLaunchParamsModifier: task=" + task + " activity=" + activity);
+ }
+ }
+
+ private void appendLog(String format, Object... args) {
+ if (DEBUG) mLogBuilder.append(" ").append(String.format(format, args));
+ }
+
+ private void outputLog() {
+ if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
+ }
+}
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/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java
index 7bd2a4a..e74e5787 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsController.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsController.java
@@ -64,6 +64,10 @@
void registerDefaultModifiers(ActivityTaskSupervisor supervisor) {
// {@link TaskLaunchParamsModifier} handles window layout preferences.
registerModifier(new TaskLaunchParamsModifier(supervisor));
+ if (DesktopModeLaunchParamsModifier.DESKTOP_MODE_SUPPORTED) {
+ // {@link DesktopModeLaunchParamsModifier} handles default task size changes
+ registerModifier(new DesktopModeLaunchParamsModifier());
+ }
}
/**
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/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 38c7595..3c55396 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2020,7 +2020,12 @@
// non-fullscreen bounds. Then when this new PIP task exits PIP, it can restore
// to its previous freeform bounds.
rootTask.setLastNonFullscreenBounds(task.mLastNonFullscreenBounds);
- rootTask.setBounds(task.getBounds());
+ // When creating a new Task for PiP, set its initial bounds as the TaskFragment in
+ // case the activity is embedded, so that it can be animated to PiP window from the
+ // current bounds.
+ // Use Task#setBoundsUnchecked to skip checking windowing mode as the windowing mode
+ // will be updated later after this is collected in transition.
+ rootTask.setBoundsUnchecked(r.getTaskFragment().getBounds());
// Move the last recents animation transaction from original task to the new one.
if (task.mLastRecentsAnimationTransaction != null) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index ebc8ae0..885968f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2605,6 +2605,13 @@
return boundsChange;
}
+ /** Sets the requested bounds regardless of the windowing mode. */
+ int setBoundsUnchecked(@NonNull Rect bounds) {
+ final int boundsChange = super.setBounds(bounds);
+ updateSurfaceBounds();
+ return boundsChange;
+ }
+
@Override
public boolean isCompatible(int windowingMode, int activityType) {
// TODO: Should we just move this to ConfigurationContainer?
@@ -5852,10 +5859,7 @@
return BOUNDS_CHANGE_NONE;
}
- final int result = super.setBounds(!inMultiWindowMode() ? null : bounds);
-
- updateSurfaceBounds();
- return result;
+ return setBoundsUnchecked(!inMultiWindowMode() ? null : bounds);
}
@Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index cb29e3f..ab38ed23 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1561,6 +1561,10 @@
if (info.mEndParent != null) {
change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken());
}
+ if (info.mStartParent != null && info.mStartParent.mRemoteToken != null
+ && target.getParent() != info.mStartParent) {
+ change.setLastParent(info.mStartParent.mRemoteToken.toWindowContainerToken());
+ }
change.setMode(info.getTransitMode(target));
change.setStartAbsBounds(info.mAbsoluteBounds);
change.setFlags(info.getChangeFlags(target));
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ad3f045..961c320 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -423,7 +423,7 @@
* @see #ENABLE_SHELL_TRANSITIONS
*/
public static final boolean sEnableShellTransitions =
- SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, true);
+ SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, false);
/**
* Allows a fullscreen windowing mode activity to launch in its desired orientation directly
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/res/xml/irq_device_map_1.xml b/services/tests/servicestests/res/xml/irq_device_map_1.xml
new file mode 100644
index 0000000..1f1a77b
--- /dev/null
+++ b/services/tests/servicestests/res/xml/irq_device_map_1.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<irq-device-map>
+ <device name="test.device.1">
+ <subsystem>test.subsystem.1</subsystem>
+ <subsystem>test.subsystem.2</subsystem>
+ </device>
+ <device name="test.device.2">
+ <subsystem>test.subsystem.3</subsystem>
+ <subsystem>test.subsystem.2</subsystem>
+ </device>
+</irq-device-map>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/irq_device_map_2.xml b/services/tests/servicestests/res/xml/irq_device_map_2.xml
new file mode 100644
index 0000000..508c98d
--- /dev/null
+++ b/services/tests/servicestests/res/xml/irq_device_map_2.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+
+<!-- Long comment describing anything that may be needed
+for this file and its maintenance -->
+<irq-device-map>
+ <!-- Small comment specific to this device -->
+ <invalid name="test.device.1">
+ <!-- These valid subsystem definitions should be ignored because of invalid parent tag -->
+ <subsystem>test.subsystem.1</subsystem>
+ <subsystem>test.subsystem.2</subsystem>
+ </invalid>
+ <device name="test.device.2">
+ <!-- Multiline comment to describe nuances
+ about why these subsystems
+ rely on this hardware device
+ to wake the CPU up from sleep
+ -->
+ <subsystem>test.subsystem.3</subsystem>
+ <!-- Small comment specific to test.subsystem.4 -->
+ <subsystem>test.subsystem.4</subsystem>
+ <subsystem>test.subsystem.5</subsystem>
+ <!-- Duplicates should be ignored -->
+ <subsystem>test.subsystem.4</subsystem>
+ <subsystem>test.subsystem.3</subsystem>
+ <subsystem>test.subsystem.5</subsystem>
+ </device>
+
+ <device name="test.device.3">
+ <!-- All child tags are invalid, mapping should be empty / non-existent for this device -->
+ <subsys>ignored</subsys>
+ <system>redundant</system>
+ </device>
+
+ <device name="test.device.4">
+ <!-- Invalid child tags should be skipped but others should be mapped -->
+ <invalid>unused</invalid>
+ <random>skipped</random>
+ <subsystem>test.subsystem.1</subsystem>
+ <subsystem>test.subsystem.4</subsystem>
+ </device>
+
+</irq-device-map>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/irq_device_map_3.xml b/services/tests/servicestests/res/xml/irq_device_map_3.xml
new file mode 100644
index 0000000..498b676
--- /dev/null
+++ b/services/tests/servicestests/res/xml/irq_device_map_3.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<irq-device-map>
+ <device name="test.alarm.device">
+ <subsystem>Alarm</subsystem>
+ </device>
+ <device name="test.wifi.device">
+ <subsystem>undefined</subsystem>
+ </device>
+</irq-device-map>
\ No newline at end of file
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/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
index 27c3ca4..a545b1f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
@@ -325,6 +325,7 @@
actual.getSessionErrorMessage());
assertEquals(expected.isPrepared(), actual.isPrepared());
assertEquals(expected.isCommitted(), actual.isCommitted());
+ assertEquals(expected.isPreapprovalRequested(), actual.isPreapprovalRequested());
assertEquals(expected.createdMillis, actual.createdMillis);
assertEquals(expected.isSealed(), actual.isSealed());
assertEquals(expected.isMultiPackage(), actual.isMultiPackage());
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java
new file mode 100644
index 0000000..7731a32
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.power.stats;
+
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
+
+import static com.android.server.power.stats.CpuWakeupStats.WAKEUP_REASON_HALF_WINDOW_MS;
+import static com.android.server.power.stats.CpuWakeupStats.WAKEUP_RETENTION_MS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.frameworks.servicestests.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+
+@RunWith(AndroidJUnit4.class)
+public class CpuWakeupStatsTest {
+ private static final String KERNEL_REASON_ALARM_IRQ = "120 test.alarm.device";
+ private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
+ private static final String KERNEL_REASON_UNKNOWN = "unsupported-free-form-reason";
+
+ private static final int TEST_UID_1 = 13239823;
+ private static final int TEST_UID_2 = 25268423;
+ private static final int TEST_UID_3 = 92261423;
+ private static final int TEST_UID_4 = 56926423;
+ private static final int TEST_UID_5 = 76421423;
+
+ private static final Context sContext = InstrumentationRegistry.getTargetContext();
+ private final ThreadLocalRandom mRandom = ThreadLocalRandom.current();
+
+ @Test
+ public void removesOldWakeups() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_1);
+
+ final Set<Long> timestamps = new HashSet<>();
+ final long firstWakeup = 453192;
+
+ obj.noteWakeupTimeAndReason(firstWakeup, 32, "unused");
+ timestamps.add(firstWakeup);
+ for (int i = 1; i < 1000; i++) {
+ final long delta = mRandom.nextLong(WAKEUP_RETENTION_MS);
+ if (timestamps.add(firstWakeup + delta)) {
+ obj.noteWakeupTimeAndReason(firstWakeup + delta, i, "unused");
+ }
+ }
+ assertThat(obj.mWakeupEvents.size()).isEqualTo(timestamps.size());
+
+ obj.noteWakeupTimeAndReason(firstWakeup + WAKEUP_RETENTION_MS + 1242, 231, "unused");
+ assertThat(obj.mWakeupEvents.size()).isEqualTo(timestamps.size());
+
+ for (int i = 0; i < 100; i++) {
+ final long now = mRandom.nextLong(WAKEUP_RETENTION_MS + 1, 100 * WAKEUP_RETENTION_MS);
+ obj.noteWakeupTimeAndReason(now, i, "unused");
+ assertThat(obj.mWakeupEvents.closestIndexOnOrBefore(now - WAKEUP_RETENTION_MS))
+ .isLessThan(0);
+ }
+ }
+
+ @Test
+ public void alarmIrqAttributionSolo() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3);
+ final long wakeupTime = 12423121;
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_ALARM_IRQ);
+
+ // Outside the window, so should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM,
+ wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM,
+ wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2);
+ // Should be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3, TEST_UID_5);
+
+ final SparseArray<SparseBooleanArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(1);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_1)).isEqualTo(false);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_2)).isEqualTo(false);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_3)).isEqualTo(true);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_5)).isEqualTo(true);
+ }
+
+ @Test
+ public void alarmIrqAttributionCombined() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3);
+ final long wakeupTime = 92123210;
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 4,
+ KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_ALARM_IRQ);
+
+ // Outside the window, so should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM,
+ wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM,
+ wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2);
+ // Should be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4,
+ TEST_UID_5);
+
+ final SparseArray<SparseBooleanArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(2);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_1)).isEqualTo(false);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_2)).isEqualTo(false);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_3)).isEqualTo(true);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_4)).isEqualTo(true);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_5)).isEqualTo(true);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue();
+ }
+
+ @Test
+ public void unknownIrqAttribution() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3);
+ final long wakeupTime = 92123410;
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 24, KERNEL_REASON_UNKNOWN_IRQ);
+
+ // Unrelated subsystems, should not be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4,
+ TEST_UID_5);
+
+ final SparseArray<SparseBooleanArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(1);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue();
+ final SparseBooleanArray uids = attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN);
+ assertThat(uids == null || uids.size() == 0).isTrue();
+ }
+
+ @Test
+ public void unknownAttribution() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3);
+ final long wakeupTime = 72123210;
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNKNOWN);
+
+ // Unrelated subsystems, should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4);
+
+ // There should be nothing in the attribution map.
+ assertThat(obj.mWakeupAttribution.size()).isEqualTo(0);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/IrqDeviceMapTest.java b/services/tests/servicestests/src/com/android/server/power/stats/IrqDeviceMapTest.java
new file mode 100644
index 0000000..43d9e60
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/stats/IrqDeviceMapTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.power.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.frameworks.servicestests.R;
+import com.android.internal.util.CollectionUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class IrqDeviceMapTest {
+ private static final String TEST_DEVICE_1 = "test.device.1";
+ private static final String TEST_DEVICE_2 = "test.device.2";
+ private static final String TEST_DEVICE_3 = "test.device.3";
+ private static final String TEST_DEVICE_4 = "test.device.4";
+
+ private static final String TEST_SUBSYSTEM_1 = "test.subsystem.1";
+ private static final String TEST_SUBSYSTEM_2 = "test.subsystem.2";
+ private static final String TEST_SUBSYSTEM_3 = "test.subsystem.3";
+ private static final String TEST_SUBSYSTEM_4 = "test.subsystem.4";
+ private static final String TEST_SUBSYSTEM_5 = "test.subsystem.5";
+
+ private static final Context sContext = InstrumentationRegistry.getTargetContext();
+
+ @Test
+ public void cachesInstancesPerXml() {
+ IrqDeviceMap irqDeviceMap1 = IrqDeviceMap.getInstance(sContext, R.xml.irq_device_map_1);
+ IrqDeviceMap irqDeviceMap2 = IrqDeviceMap.getInstance(sContext, R.xml.irq_device_map_1);
+ assertThat(irqDeviceMap1).isSameInstanceAs(irqDeviceMap2);
+
+ irqDeviceMap2 = IrqDeviceMap.getInstance(sContext, R.xml.irq_device_map_2);
+ assertThat(irqDeviceMap1).isNotSameInstanceAs(irqDeviceMap2);
+
+ irqDeviceMap1 = IrqDeviceMap.getInstance(sContext, R.xml.irq_device_map_2);
+ assertThat(irqDeviceMap1).isSameInstanceAs(irqDeviceMap2);
+ }
+
+ @Test
+ public void simpleXml() {
+ IrqDeviceMap deviceMap = IrqDeviceMap.getInstance(sContext, R.xml.irq_device_map_1);
+
+ List<String> subsystems = deviceMap.getSubsystemsForDevice(TEST_DEVICE_1);
+ assertThat(subsystems).hasSize(2);
+ // No specific order is required.
+ assertThat(subsystems).containsExactly(TEST_SUBSYSTEM_2, TEST_SUBSYSTEM_1);
+
+ subsystems = deviceMap.getSubsystemsForDevice(TEST_DEVICE_2);
+ assertThat(subsystems).hasSize(2);
+ // No specific order is required.
+ assertThat(subsystems).containsExactly(TEST_SUBSYSTEM_2, TEST_SUBSYSTEM_3);
+ }
+
+ @Test
+ public void complexXml() {
+ IrqDeviceMap deviceMap = IrqDeviceMap.getInstance(sContext, R.xml.irq_device_map_2);
+
+ List<String> subsystems = deviceMap.getSubsystemsForDevice(TEST_DEVICE_1);
+ assertThat(CollectionUtils.isEmpty(subsystems)).isTrue();
+
+ subsystems = deviceMap.getSubsystemsForDevice(TEST_DEVICE_2);
+ assertThat(subsystems).hasSize(3);
+ // No specific order is required.
+ assertThat(subsystems).containsExactly(TEST_SUBSYSTEM_3, TEST_SUBSYSTEM_4,
+ TEST_SUBSYSTEM_5);
+
+ subsystems = deviceMap.getSubsystemsForDevice(TEST_DEVICE_3);
+ assertThat(CollectionUtils.isEmpty(subsystems)).isTrue();
+
+ subsystems = deviceMap.getSubsystemsForDevice(TEST_DEVICE_4);
+ assertThat(subsystems).hasSize(2);
+ // No specific order is required.
+ assertThat(subsystems).containsExactly(TEST_SUBSYSTEM_4, TEST_SUBSYSTEM_1);
+ }
+}
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/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
new file mode 100644
index 0000000..7830e90
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.LaunchParamsController.LaunchParamsModifier.Result;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for desktop mode task bounds.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DesktopModeLaunchParamsModifierTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase {
+
+ private ActivityRecord mActivity;
+
+ private DesktopModeLaunchParamsModifier mTarget;
+
+ private LaunchParamsController.LaunchParams mCurrent;
+ private LaunchParamsController.LaunchParams mResult;
+
+ @Before
+ public void setUp() throws Exception {
+ mActivity = new ActivityBuilder(mAtm).build();
+ mTarget = new DesktopModeLaunchParamsModifier();
+ mCurrent = new LaunchParamsController.LaunchParams();
+ mCurrent.reset();
+ mResult = new LaunchParamsController.LaunchParams();
+ mResult.reset();
+ }
+
+ @Test
+ public void testReturnsSkipIfTaskIsNull() {
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate());
+ }
+
+ @Test
+ public void testReturnsSkipIfNotBoundsPhase() {
+ final Task task = new TaskBuilder(mSupervisor).build();
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).setPhase(
+ PHASE_DISPLAY).calculate());
+ }
+
+ @Test
+ public void testReturnsSkipIfTaskNotInFreeform() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FULLSCREEN).build();
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
+ }
+
+ @Test
+ public void testReturnsSkipIfCurrentParamsHasBounds() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FREEFORM).build();
+ mCurrent.mBounds.set(/* left */ 0, /* top */ 0, /* right */ 100, /* bottom */ 100);
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
+ }
+
+ @Test
+ public void testUsesDefaultBounds() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FREEFORM).build();
+ assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(dpiToPx(task, 840), mResult.mBounds.width());
+ assertEquals(dpiToPx(task, 630), mResult.mBounds.height());
+ }
+
+ @Test
+ public void testUsesDisplayAreaAndWindowingModeFromSource() {
+ final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+ WINDOWING_MODE_FREEFORM).build();
+ TaskDisplayArea mockTaskDisplayArea = mock(TaskDisplayArea.class);
+ mCurrent.mPreferredTaskDisplayArea = mockTaskDisplayArea;
+ mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+
+ assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+ assertEquals(mockTaskDisplayArea, mResult.mPreferredTaskDisplayArea);
+ assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
+ }
+
+ private int dpiToPx(Task task, int dpi) {
+ float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT;
+ return (int) (dpi * density + 0.5f);
+ }
+
+ private class CalculateRequestBuilder {
+ private Task mTask;
+ private int mPhase = PHASE_BOUNDS;
+ private final ActivityRecord mActivity =
+ DesktopModeLaunchParamsModifierTests.this.mActivity;
+ private final LaunchParamsController.LaunchParams mCurrentParams = mCurrent;
+ private final LaunchParamsController.LaunchParams mOutParams = mResult;
+
+ private CalculateRequestBuilder setTask(Task task) {
+ mTask = task;
+ return this;
+ }
+
+ private CalculateRequestBuilder setPhase(int phase) {
+ mPhase = phase;
+ return this;
+ }
+
+ @Result
+ private int calculate() {
+ return mTarget.onCalculate(mTask, /* layout*/ null, mActivity, /* source */
+ null, /* options */ null, /* request */ null, mPhase, mCurrentParams,
+ mOutParams);
+ }
+ }
+}
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/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 4f03f54..e0e1d73 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -72,6 +72,7 @@
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Rect;
import android.os.PowerManager;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
@@ -391,6 +392,33 @@
assertEquals(WINDOWING_MODE_FULLSCREEN, fullscreenTask.getWindowingMode());
}
+ @Test
+ public void testMovingEmbeddedActivityToPip() {
+ final Rect taskBounds = new Rect(0, 0, 800, 1000);
+ final Rect taskFragmentBounds = new Rect(0, 0, 400, 1000);
+ final Task task = mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask(
+ WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ task.setBounds(taskBounds);
+ assertEquals(taskBounds, task.getBounds());
+ final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .createActivityCount(2)
+ .setBounds(taskFragmentBounds)
+ .build();
+ assertEquals(taskFragmentBounds, taskFragment.getBounds());
+ final ActivityRecord topActivity = taskFragment.getTopMostActivity();
+
+ // Move the top activity to pinned root task.
+ mRootWindowContainer.moveActivityToPinnedRootTask(topActivity,
+ null /* launchIntoPipHostActivity */, "test");
+
+ final Task pinnedRootTask = task.getDisplayArea().getRootPinnedTask();
+
+ // Ensure the initial bounds of the PiP Task is the same as the TaskFragment.
+ ensureTaskPlacement(pinnedRootTask, topActivity);
+ assertEquals(taskFragmentBounds, pinnedRootTask.getBounds());
+ }
+
private static void ensureTaskPlacement(Task task, ActivityRecord... activities) {
final ArrayList<ActivityRecord> taskActivities = new ArrayList<>();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index ec6a74c..29a514c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1325,6 +1325,35 @@
}
@Test
+ public void testReparentChangeLastParent() {
+ final Transition transition = createTestTransition(TRANSIT_CHANGE);
+ final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+ final ArraySet<WindowContainer> participants = transition.mParticipants;
+
+ // Reparent activity in transition.
+ final Task lastParent = createTask(mDisplayContent);
+ final Task newParent = createTask(mDisplayContent);
+ final ActivityRecord activity = createActivityRecord(lastParent);
+ activity.mVisibleRequested = true;
+ // Skip manipulate the SurfaceControl.
+ doNothing().when(activity).setDropInputMode(anyInt());
+ changes.put(activity, new Transition.ChangeInfo(activity));
+ activity.reparent(newParent, POSITION_TOP);
+ activity.mVisibleRequested = false;
+
+ participants.add(activity);
+ final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+ participants, changes);
+ final TransitionInfo info = Transition.calculateTransitionInfo(
+ transition.mType, 0 /* flags */, targets, changes, mMockT);
+
+ // Change contains last parent info.
+ assertEquals(1, info.getChanges().size());
+ assertEquals(lastParent.mRemoteToken.toWindowContainerToken(),
+ info.getChanges().get(0).getLastParent());
+ }
+
+ @Test
public void testIncludeEmbeddedActivityReparent() {
final Transition transition = createTestTransition(TRANSIT_OPEN);
final Task task = createTask(mDisplayContent);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 79546b8..151ff80 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -1242,6 +1242,19 @@
@android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION)
@Override
public void updateState(
+ @Nullable PersistableBundle options,
+ @Nullable SharedMemory sharedMemory) {
+ synchronized (this) {
+ enforceIsCurrentVoiceInteractionService();
+
+ Binder.withCleanCallingIdentity(
+ () -> mImpl.updateStateLocked(options, sharedMemory));
+ }
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION)
+ @Override
+ public void initAndVerifyDetector(
@NonNull Identity voiceInteractorIdentity,
@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory,
@@ -1250,21 +1263,12 @@
synchronized (this) {
enforceIsCurrentVoiceInteractionService();
- if (mImpl == null) {
- Slog.w(TAG, "updateState without running voice interaction service");
- return;
- }
-
voiceInteractorIdentity.uid = Binder.getCallingUid();
voiceInteractorIdentity.pid = Binder.getCallingPid();
- final long caller = Binder.clearCallingIdentity();
- try {
- mImpl.updateStateLocked(
- voiceInteractorIdentity, options, sharedMemory, callback, detectorType);
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
+ Binder.withCleanCallingIdentity(
+ () -> mImpl.initAndVerifyDetectorLocked(voiceInteractorIdentity, options,
+ sharedMemory, callback, detectorType));
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index dcf7b78..5d1901d 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -551,12 +551,31 @@
}
public void updateStateLocked(
+ @Nullable PersistableBundle options,
+ @Nullable SharedMemory sharedMemory) {
+ Slog.v(TAG, "updateStateLocked");
+
+ if (sharedMemory != null && !sharedMemory.setProtect(OsConstants.PROT_READ)) {
+ Slog.w(TAG, "Can't set sharedMemory to be read-only");
+ throw new IllegalStateException("Can't set sharedMemory to be read-only");
+ }
+
+ if (mHotwordDetectionConnection == null) {
+ Slog.w(TAG, "update State, but no hotword detection connection");
+ throw new IllegalStateException("Hotword detection connection not found");
+ }
+ synchronized (mHotwordDetectionConnection.mLock) {
+ mHotwordDetectionConnection.updateStateLocked(options, sharedMemory);
+ }
+ }
+
+ public void initAndVerifyDetectorLocked(
@NonNull Identity voiceInteractorIdentity,
@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory,
IHotwordRecognitionStatusCallback callback,
int detectorType) {
- Slog.v(TAG, "updateStateLocked");
+ Slog.v(TAG, "initAndVerifyDetectorLocked");
int voiceInteractionServiceUid = mInfo.getServiceInfo().applicationInfo.uid;
if (mHotwordDetectionComponentName == null) {
Slog.w(TAG, "Hotword detection service name not found");
@@ -614,8 +633,6 @@
mInfo.getServiceInfo().applicationInfo.uid, voiceInteractorIdentity,
mHotwordDetectionComponentName, mUser, /* bindInstantServiceAllowed= */ false,
options, sharedMemory, callback, detectorType);
- } else {
- mHotwordDetectionConnection.updateStateLocked(options, sharedMemory);
}
}