Merge "Add new InputBindResult.ResultCode for no editor"
diff --git a/api/current.txt b/api/current.txt
index 1ffba87..3c3cfcc 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -16296,7 +16296,7 @@
public final class TotalCaptureResult extends android.hardware.camera2.CaptureResult {
method public java.util.List<android.hardware.camera2.CaptureResult> getPartialResults();
- method public <T> T getPhysicalCameraKey(android.hardware.camera2.CaptureResult.Key<T>, java.lang.String);
+ method public java.util.Map<java.lang.String, android.hardware.camera2.CaptureResult> getPhysicalCameraResults();
}
}
diff --git a/api/test-current.txt b/api/test-current.txt
index ac4ce3b..c30c056 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -211,6 +211,7 @@
method public abstract boolean isPermissionReviewModeEnabled();
field public static final java.lang.String FEATURE_ADOPTABLE_STORAGE = "android.software.adoptable_storage";
field public static final java.lang.String FEATURE_FILE_BASED_ENCRYPTION = "android.software.file_based_encryption";
+ field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000
}
public class PermissionInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index ed570e7..e58c535 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -97,6 +97,7 @@
BootSequenceReported boot_sequence_reported = 57;
DaveyOccurred davey_occurred = 58;
OverlayStateChanged overlay_state_changed = 59;
+ ForegroundServiceStateChanged foreground_service_state_changed = 60;
// TODO: Reorder the numbering so that the most frequent occur events occur in the first 15.
}
@@ -998,6 +999,25 @@
optional State state = 4;
}
+/*
+ * Logs foreground service starts and stops.
+ * Note that this is not when a service starts or stops, but when it is
+ * considered foreground.
+ * Logged from
+ * //frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
+ */
+message ForegroundServiceStateChanged {
+ optional int32 uid = 1;
+ // package_name + "/" + class_name
+ optional string short_name = 2;
+
+ enum State {
+ ENTER = 1;
+ EXIT = 2;
+ }
+ optional State state = 3;
+}
+
/**
* Pulls bytes transferred via wifi (Sum of foreground and background usage).
*
@@ -1463,3 +1483,4 @@
message FullBatteryCapacity {
optional int32 capacity_uAh = 1;
}
+
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 417145c..5f0619a 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -28,6 +28,7 @@
#include "stats_util.h"
#include <log/logprint.h>
+#include <private/android_filesystem_config.h>
using android::util::FIELD_COUNT_REPEATED;
using android::util::FIELD_TYPE_MESSAGE;
@@ -47,7 +48,7 @@
MetricsManager::MetricsManager(const ConfigKey& key, const StatsdConfig& config,
const long timeBaseSec, sp<UidMap> uidMap)
- : mConfigKey(key), mUidMap(uidMap), mStatsdUid(getStatsdUid()) {
+ : mConfigKey(key), mUidMap(uidMap) {
mConfigValid =
initStatsdConfig(key, config, *uidMap, timeBaseSec, mTagIds, mAllAtomMatchers, mAllConditionTrackers,
mAllMetricProducers, mAllAnomalyTrackers, mConditionToMetricMap,
@@ -59,9 +60,9 @@
// mConfigValid = false;
// ALOGE("Log source white list is empty! This config won't get any data.");
- mAllowedUid.push_back(1000);
- mAllowedUid.push_back(0);
- mAllowedUid.push_back(mStatsdUid);
+ mAllowedUid.push_back(AID_ROOT);
+ mAllowedUid.push_back(AID_STATSD);
+ mAllowedUid.push_back(AID_SYSTEM);
mAllowedLogSources.insert(mAllowedUid.begin(), mAllowedUid.end());
} else {
for (const auto& source : config.allowed_log_source()) {
@@ -198,7 +199,7 @@
// unless that caller is statsd itself (statsd is allowed to spoof uids).
long appHookUid = event.GetLong(event.size()-2, &err);
int32_t loggerUid = event.GetUid();
- if (err != NO_ERROR || (loggerUid != appHookUid && loggerUid != mStatsdUid)) {
+ if (err != NO_ERROR || (loggerUid != appHookUid && loggerUid != AID_STATSD)) {
VLOG("AppHook has invalid uid: claimed %ld but caller is %d", appHookUid, loggerUid);
return;
}
@@ -333,16 +334,6 @@
return totalSize;
}
-int32_t MetricsManager::getStatsdUid() {
- auto suit = UidMap::sAidToUidMapping.find("AID_STATSD");
- if (suit != UidMap::sAidToUidMapping.end()) {
- return suit->second;
- } else {
- ALOGE("Statsd failed to find its own uid!");
- return -1;
- }
-}
-
} // namespace statsd
} // namespace os
} // namespace android
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index a1220f9..d4b9102 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -75,9 +75,6 @@
sp<UidMap> mUidMap;
- // The uid of statsd.
- const int32_t mStatsdUid;
-
bool mConfigValid = false;
// The uid log sources from StatsdConfig.
@@ -139,9 +136,6 @@
void initLogSourceWhiteList();
- // Fetches the uid of statsd from UidMap.
- static int32_t getStatsdUid();
-
// The metrics that don't need to be uploaded or even reported.
std::set<int64_t> mNoReportMetricIds;
diff --git a/core/java/android/app/InstantAppResolverService.java b/core/java/android/app/InstantAppResolverService.java
index 76a3682..2ba4c00 100644
--- a/core/java/android/app/InstantAppResolverService.java
+++ b/core/java/android/app/InstantAppResolverService.java
@@ -88,7 +88,7 @@
public void onGetInstantAppResolveInfo(Intent sanitizedIntent, int[] hostDigestPrefix,
String token, InstantAppResolutionCallback callback) {
// if not overridden, forward to old methods and filter out non-web intents
- if (sanitizedIntent.isBrowsableWebIntent()) {
+ if (sanitizedIntent.isWebIntent()) {
onGetInstantAppResolveInfo(hostDigestPrefix, token, callback);
} else {
callback.onInstantAppResolveInfo(Collections.emptyList());
@@ -107,7 +107,7 @@
String token, InstantAppResolutionCallback callback) {
Log.e(TAG, "New onGetInstantAppIntentFilter is not overridden");
// if not overridden, forward to old methods and filter out non-web intents
- if (sanitizedIntent.isBrowsableWebIntent()) {
+ if (sanitizedIntent.isWebIntent()) {
onGetInstantAppIntentFilter(hostDigestPrefix, token, callback);
} else {
callback.onInstantAppResolveInfo(Collections.emptyList());
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 9b62f19..fa73e3c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -10089,9 +10089,8 @@
}
/** @hide */
- public boolean isBrowsableWebIntent() {
+ public boolean isWebIntent() {
return ACTION_VIEW.equals(mAction)
- && hasCategory(CATEGORY_BROWSABLE)
&& hasWebURI();
}
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index b61a6d9..db1630b 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -1601,7 +1601,7 @@
* @hide
*/
public boolean isAllowedToUseHiddenApi() {
- return false;
+ return isSystemApp();
}
/**
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 4e5f835..486c86c 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -441,6 +441,7 @@
* @hide
*/
@SystemApi
+ @TestApi
public static final int MATCH_FACTORY_ONLY = 0x00200000;
/**
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 2da8937..3afc346 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -6738,31 +6738,6 @@
+ " " + packageName + "}";
}
- public String dumpState_temp() {
- String flags = "";
- flags += ((applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
- flags += ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
- if ("".equals(flags)) {
- flags = "-";
- }
- String privFlags = "";
- privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
- privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
- privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
- if ("".equals(privFlags)) {
- privFlags = "-";
- }
- return "Package{"
- + Integer.toHexString(System.identityHashCode(this))
- + " " + packageName
- + ", ver:" + getLongVersionCode()
- + ", path: " + codePath
- + ", flags: " + flags
- + ", privFlags: " + privFlags
- + ", extra: " + (mExtras == null ? "<<NULL>>" : Integer.toHexString(System.identityHashCode(mExtras)) + "}")
- + "}";
- }
-
@Override
public int describeContents() {
return 0;
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 40ee834..df64401 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -923,6 +923,14 @@
* {@link CameraCaptureSession#setRepeatingRequest} or
* {@link CameraCaptureSession#setRepeatingBurst}</p>
*
+ * <p>Individual physical camera settings will only be honored for camera session
+ * that was initialiazed with corresponding physical camera id output configuration
+ * {@link OutputConfiguration#setPhysicalCameraId} and the same output targets are
+ * also attached in the request by {@link CaptureRequest.Builder#addTarget}.</p>
+ *
+ * <p>The output is undefined for any logical camera streams in case valid physical camera
+ * settings are attached.</p>
+ *
* @param templateType An enumeration selecting the use case for this request. Not all template
* types are supported on every device. See the documentation for each template type for
* details.
diff --git a/core/java/android/hardware/camera2/TotalCaptureResult.java b/core/java/android/hardware/camera2/TotalCaptureResult.java
index bae2d04..0be45a0 100644
--- a/core/java/android/hardware/camera2/TotalCaptureResult.java
+++ b/core/java/android/hardware/camera2/TotalCaptureResult.java
@@ -17,7 +17,6 @@
package android.hardware.camera2;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.impl.CaptureResultExtras;
import android.hardware.camera2.impl.PhysicalCaptureResultInfo;
@@ -26,6 +25,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* <p>The total assembled results of a single image capture from the image sensor.</p>
@@ -49,9 +49,9 @@
*
* <p>For a logical multi-camera device, if the CaptureRequest contains a surface for an underlying
* physical camera, the corresponding {@link TotalCaptureResult} object will include the metadata
- * for that physical camera. And its keys and values can be accessed by
- * {@link #getPhysicalCameraKey}. If all requested surfaces are for the logical camera, no
- * metadata for physical camera will be included.</p>
+ * for that physical camera. And the mapping between the physical camera id and result metadata
+ * can be accessed via {@link #getPhysicalCameraResults}. If all requested surfaces are for the
+ * logical camera, no metadata for physical camera will be included.</p>
*
* <p>{@link TotalCaptureResult} objects are immutable.</p>
*
@@ -62,7 +62,7 @@
private final List<CaptureResult> mPartialResults;
private final int mSessionId;
// The map between physical camera id and capture result
- private final HashMap<String, CameraMetadataNative> mPhysicalCaptureResults;
+ private final HashMap<String, CaptureResult> mPhysicalCaptureResults;
/**
* Takes ownership of the passed-in camera metadata and the partial results
@@ -83,10 +83,12 @@
mSessionId = sessionId;
- mPhysicalCaptureResults = new HashMap<String, CameraMetadataNative>();
+ mPhysicalCaptureResults = new HashMap<String, CaptureResult>();
for (PhysicalCaptureResultInfo onePhysicalResult : physicalResults) {
+ CaptureResult physicalResult = new CaptureResult(
+ onePhysicalResult.getCameraMetadata(), parent, extras);
mPhysicalCaptureResults.put(onePhysicalResult.getCameraId(),
- onePhysicalResult.getCameraMetadata());
+ physicalResult);
}
}
@@ -101,7 +103,7 @@
mPartialResults = new ArrayList<>();
mSessionId = CameraCaptureSession.SESSION_ID_NONE;
- mPhysicalCaptureResults = new HashMap<String, CameraMetadataNative>();
+ mPhysicalCaptureResults = new HashMap<String, CaptureResult>();
}
/**
@@ -132,36 +134,20 @@
}
/**
- * Get a capture result field value for a particular physical camera id.
+ * Get the map between physical camera ids and their capture result metadata
*
- * <p>The field definitions can be found in {@link CaptureResult}.</p>
- *
- * <p>This function can be called for logical camera devices, which are devices that have
+ * <p>This function can be called for logical multi-camera devices, which are devices that have
* REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability and calls to {@link
* CameraCharacteristics#getPhysicalCameraIds} return a non-empty list of physical devices that
- * are backing the logical camera. The camera id included in physicalCameraId argument
- * selects an individual physical device, and returns its specific capture result field.</p>
+ * are backing the logical camera.</p>
*
- * <p>This function should only be called if one or more streams from the underlying
- * 'physicalCameraId' was requested by the corresponding capture request.</p>
- *
- * @throws IllegalArgumentException if the key was not valid, or the physicalCameraId is not
- * applicable to the current camera, or a stream from 'physicalCameraId' is not requested by the
- * corresponding capture request.
- *
- * @param key The result field to read.
- * @param physicalCameraId The physical camera the result originates from.
- *
- * @return The value of that key, or {@code null} if the field is not set.
- */
- @Nullable
- public <T> T getPhysicalCameraKey(Key<T> key, @NonNull String physicalCameraId) {
- if (!mPhysicalCaptureResults.containsKey(physicalCameraId)) {
- throw new IllegalArgumentException(
- "No TotalCaptureResult exists for physical camera " + physicalCameraId);
- }
+ * <p>If one or more streams from the underlying physical cameras were requested by the
+ * corresponding capture request, this function returns the result metadata for those physical
+ * cameras. Otherwise, an empty map is returned.</p>
- CameraMetadataNative physicalMetadata = mPhysicalCaptureResults.get(physicalCameraId);
- return physicalMetadata.get(key);
+ * @return unmodifiable map between physical camera ids and their capture result metadata
+ */
+ public Map<String, CaptureResult> getPhysicalCameraResults() {
+ return Collections.unmodifiableMap(mPhysicalCaptureResults);
}
}
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index eb4bced..b205e2c 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -368,7 +368,7 @@
* desirable for the camera application to request streams from individual physical cameras.
* This call achieves it by mapping the OutputConfiguration to the physical camera id.</p>
*
- * <p>The valid physical camera id can be queried by {@link
+ * <p>The valid physical camera ids can be queried by {@link
* android.hardware.camera2.CameraCharacteristics#getPhysicalCameraIds}.
* </p>
*
diff --git a/core/java/android/os/IServiceManager.java b/core/java/android/os/IServiceManager.java
index 2176a78..89bf7b9 100644
--- a/core/java/android/os/IServiceManager.java
+++ b/core/java/android/os/IServiceManager.java
@@ -76,9 +76,15 @@
int DUMP_FLAG_PRIORITY_CRITICAL = 1 << 0;
int DUMP_FLAG_PRIORITY_HIGH = 1 << 1;
int DUMP_FLAG_PRIORITY_NORMAL = 1 << 2;
+ /**
+ * Services are by default registered with a DEFAULT dump priority. DEFAULT priority has the
+ * same priority as NORMAL priority but the services are not called with dump priority
+ * arguments.
+ */
+ int DUMP_FLAG_PRIORITY_DEFAULT = 1 << 3;
int DUMP_FLAG_PRIORITY_ALL = DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH
- | DUMP_FLAG_PRIORITY_NORMAL;
+ | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PRIORITY_DEFAULT;
/* Allows services to dump sections in protobuf format. */
- int DUMP_FLAG_PROTO = 1 << 3;
+ int DUMP_FLAG_PROTO = 1 << 4;
}
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 42ec315..3be76d6 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -83,7 +83,7 @@
* @param service the service object
*/
public static void addService(String name, IBinder service) {
- addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
+ addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}
/**
@@ -96,7 +96,7 @@
* to access this service
*/
public static void addService(String name, IBinder service, boolean allowIsolated) {
- addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
+ addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}
/**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8c65041..f179371 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3692,6 +3692,20 @@
new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
+ * User-selected RTT mode
+ * 0 = OFF
+ * 1 = FULL
+ * 2 = VCO
+ * 3 = HCO
+ * Uses the same constants as TTY (e.g. {@link android.telecom.TelecomManager#TTY_MODE_OFF})
+ * @hide
+ */
+ public static final String RTT_CALLING_MODE = "rtt_calling_mode";
+
+ /** @hide */
+ public static final Validator RTT_CALLING_MODE_VALIDATOR = TTY_MODE_VALIDATOR;
+
+ /**
* Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
* boolean (1 or 0).
*/
@@ -4016,6 +4030,7 @@
DTMF_TONE_WHEN_DIALING,
DTMF_TONE_TYPE_WHEN_DIALING,
HEARING_AID,
+ RTT_CALLING_MODE,
TTY_MODE,
MASTER_MONO,
SOUND_EFFECTS_ENABLED,
@@ -4214,6 +4229,7 @@
VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR);
VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR);
VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR);
+ VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR);
VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR);
VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR);
@@ -10255,6 +10271,20 @@
public static final String BATTERY_TIP_CONSTANTS = "battery_tip_constants";
/**
+ * An integer to show the version of the anomaly config. Ex: 1, which means
+ * current version is 1.
+ * @hide
+ */
+ public static final String ANOMALY_CONFIG_VERSION = "anomaly_config_version";
+
+ /**
+ * A base64-encoded string represents anomaly stats config, used for
+ * {@link android.app.StatsManager}.
+ * @hide
+ */
+ public static final String ANOMALY_CONFIG = "anomaly_config";
+
+ /**
* Always on display(AOD) specific settings
* This is encoded as a key=value list, separated by commas. Ex:
*
diff --git a/core/java/android/transition/TransitionUtils.java b/core/java/android/transition/TransitionUtils.java
index 084b79d..60b77bc 100644
--- a/core/java/android/transition/TransitionUtils.java
+++ b/core/java/android/transition/TransitionUtils.java
@@ -163,10 +163,14 @@
public static Bitmap createViewBitmap(View view, Matrix matrix, RectF bounds,
ViewGroup sceneRoot) {
final boolean addToOverlay = !view.isAttachedToWindow();
+ ViewGroup parent = null;
+ int indexInParent = 0;
if (addToOverlay) {
if (sceneRoot == null || !sceneRoot.isAttachedToWindow()) {
return null;
}
+ parent = (ViewGroup) view.getParent();
+ indexInParent = parent.indexOfChild(view);
sceneRoot.getOverlay().add(view);
}
Bitmap bitmap = null;
@@ -190,6 +194,7 @@
}
if (addToOverlay) {
sceneRoot.getOverlay().remove(view);
+ parent.addView(view, indexInParent);
}
return bitmap;
}
diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java
index f0838a1..77c652e 100644
--- a/core/java/android/transition/Visibility.java
+++ b/core/java/android/transition/Visibility.java
@@ -402,8 +402,11 @@
// Becoming GONE
if (startView == endView) {
viewToKeep = endView;
- } else {
+ } else if (mCanRemoveViews) {
overlayView = startView;
+ } else {
+ overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
+ (View) startView.getParent());
}
}
}
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
index 1dfff5e..6bd6930 100644
--- a/core/java/com/android/internal/app/ResolverListController.java
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -106,7 +106,7 @@
int flags = PackageManager.MATCH_DEFAULT_ONLY
| (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
| (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0);
- if (intent.isBrowsableWebIntent()
+ if (intent.isWebIntent()
|| (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
flags |= PackageManager.MATCH_INSTANT;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f627aaa..deefddb 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3861,6 +3861,7 @@
android:excludeFromRecents="true"
android:label="@string/user_owner_label"
android:exported="true"
+ android:visibleToInstantApps="true"
>
</activity>
<activity-alias android:name="com.android.internal.app.ForwardIntentToParent"
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 3e62183..e1f95a3 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -109,6 +109,8 @@
Settings.Global.ALWAYS_ON_DISPLAY_CONSTANTS,
Settings.Global.ANIMATOR_DURATION_SCALE,
Settings.Global.ANOMALY_DETECTION_CONSTANTS,
+ Settings.Global.ANOMALY_CONFIG,
+ Settings.Global.ANOMALY_CONFIG_VERSION,
Settings.Global.APN_DB_UPDATE_CONTENT_URL,
Settings.Global.APN_DB_UPDATE_METADATA_URL,
Settings.Global.APP_IDLE_CONSTANTS,
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index 04c5295..f41267e 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -60,6 +60,7 @@
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
@@ -801,36 +802,18 @@
* @return The new typeface.
*/
public static Typeface createFromAsset(AssetManager mgr, String path) {
- if (path == null) {
- throw new NullPointerException(); // for backward compatibility
- }
- synchronized (sDynamicCacheLock) {
- Typeface typeface = new Builder(mgr, path).build();
- if (typeface != null) return typeface;
+ Preconditions.checkNotNull(path); // for backward compatibility
+ Preconditions.checkNotNull(mgr);
- final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */,
- null /* axes */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
- DEFAULT_FAMILY);
- typeface = sDynamicTypefaceCache.get(key);
- if (typeface != null) return typeface;
-
- final FontFamily fontFamily = new FontFamily();
- if (fontFamily.addFontFromAssetManager(mgr, path, 0, true /* isAsset */,
- 0 /* ttc index */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
- null /* axes */)) {
- if (!fontFamily.freeze()) {
- return Typeface.DEFAULT;
- }
- final FontFamily[] families = { fontFamily };
- typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
- RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
- sDynamicTypefaceCache.put(key, typeface);
- return typeface;
- } else {
- fontFamily.abortCreation();
- }
+ Typeface typeface = new Builder(mgr, path).build();
+ if (typeface != null) return typeface;
+ // check if the file exists, and throw an exception for backward compatibility
+ try (InputStream inputStream = mgr.open(path)) {
+ } catch (IOException e) {
+ throw new RuntimeException("Font asset not found " + path);
}
- throw new RuntimeException("Font asset not found " + path);
+
+ return Typeface.DEFAULT;
}
/**
@@ -848,13 +831,22 @@
/**
* Create a new typeface from the specified font file.
*
- * @param path The path to the font data.
+ * @param file The path to the font data.
* @return The new typeface.
*/
- public static Typeface createFromFile(@Nullable File path) {
+ public static Typeface createFromFile(@Nullable File file) {
// For the compatibility reasons, leaving possible NPE here.
// See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
- return createFromFile(path.getAbsolutePath());
+
+ Typeface typeface = new Builder(file).build();
+ if (typeface != null) return typeface;
+
+ // check if the file exists, and throw an exception for backward compatibility
+ if (!file.exists()) {
+ throw new RuntimeException("Font asset not found " + file.getAbsolutePath());
+ }
+
+ return Typeface.DEFAULT;
}
/**
@@ -864,19 +856,8 @@
* @return The new typeface.
*/
public static Typeface createFromFile(@Nullable String path) {
- final FontFamily fontFamily = new FontFamily();
- if (fontFamily.addFont(path, 0 /* ttcIndex */, null /* axes */,
- RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)) {
- if (!fontFamily.freeze()) {
- return Typeface.DEFAULT;
- }
- FontFamily[] families = { fontFamily };
- return createFromFamiliesWithDefault(families, DEFAULT_FAMILY,
- RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
- } else {
- fontFamily.abortCreation();
- }
- throw new RuntimeException("Font not found " + path);
+ Preconditions.checkNotNull(path); // for backward compatibility
+ return createFromFile(new File(path));
}
/**
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index c33dce1..d3c6edd 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -236,6 +236,62 @@
*/
public static final String GNSS_HARDWARE_MODEL_NAME_UNKNOWN = "Model Name Unknown";
+ /**
+ * Broadcast intent action for Settings app to inject a footer at the bottom of location
+ * settings.
+ *
+ * <p>This broadcast is used for two things:
+ * <ol>
+ * <li>For receivers to inject a footer with provided text. This is for use only by apps
+ * that are included in the system image. </li>
+ * <li>For receivers to know their footer is injected under location settings.</li>
+ * </ol>
+ *
+ * <p>To inject a footer to location settings, you must declare a broadcast receiver of
+ * {@link LocationManager#SETTINGS_FOOTER_DISPLAYED_ACTION} in the manifest as so:
+ * <pre>
+ * <receiver android:name="com.example.android.footer.MyFooterInjector">
+ * <intent-filter>
+ * <action android:name="com.android.settings.location.INJECT_FOOTER" />
+ * </intent-filter>
+ * <meta-data
+ * android:name="com.android.settings.location.FOOTER_STRING"
+ * android:resource="@string/my_injected_footer_string" />
+ * </receiver>
+ * </pre>
+ *
+ * <p>On entering location settings, Settings app will send a
+ * {@link #SETTINGS_FOOTER_DISPLAYED_ACTION} broadcast to receivers whose footer is successfully
+ * injected. On leaving location settings, the footer becomes not visible to users. Settings app
+ * will send a {@link #SETTINGS_FOOTER_REMOVED_ACTION} broadcast to those receivers.
+ *
+ * @hide
+ */
+ public static final String SETTINGS_FOOTER_DISPLAYED_ACTION =
+ "com.android.settings.location.DISPLAYED_FOOTER";
+
+ /**
+ * Broadcast intent action when location settings footer is not visible to users.
+ *
+ * <p>See {@link #SETTINGS_FOOTER_DISPLAYED_ACTION} for more detail on how to use.
+ *
+ * @hide
+ */
+ public static final String SETTINGS_FOOTER_REMOVED_ACTION =
+ "com.android.settings.location.REMOVED_FOOTER";
+
+ /**
+ * Metadata name for {@link LocationManager#SETTINGS_FOOTER_DISPLAYED_ACTION} broadcast
+ * receivers to specify a string resource id as location settings footer text. This is for use
+ * only by apps that are included in the system image.
+ *
+ * <p>See {@link #SETTINGS_FOOTER_DISPLAYED_ACTION} for more detail on how to use.
+ *
+ * @hide
+ */
+ public static final String METADATA_SETTINGS_FOOTER_STRING =
+ "com.android.settings.location.FOOTER_STRING";
+
// Map from LocationListeners to their associated ListenerTransport objects
private HashMap<LocationListener,ListenerTransport> mListeners =
new HashMap<LocationListener,ListenerTransport>();
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index adb4dbf..85a579d 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3017,7 +3017,7 @@
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 153;
+ private static final int SETTINGS_VERSION = 154;
private final int mUserId;
@@ -3627,6 +3627,23 @@
currentVersion = 153;
}
+ if (currentVersion == 153) {
+ // Version 154: Read notification badge configuration from config.
+ // If user has already set the value, don't do anything.
+ final SettingsState systemSecureSettings = getSecureSettingsLocked(userId);
+ final Setting showNotificationBadges = systemSecureSettings.getSettingLocked(
+ Settings.Secure.NOTIFICATION_BADGING);
+ if (showNotificationBadges.isNull()) {
+ final boolean defaultValue = getContext().getResources().getBoolean(
+ com.android.internal.R.bool.config_notificationBadging);
+ systemSecureSettings.insertSettingLocked(
+ Secure.NOTIFICATION_BADGING,
+ defaultValue ? "1" : "0",
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+ currentVersion = 154;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
index 1897171..1da50ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
@@ -88,7 +88,8 @@
* Layout is happening from end -> start
*/
private void calculateIconTranslations() {
- float translationX = getWidth();
+ float width = getWidth();
+ float translationX = width;
float contentStart = getPaddingStart();
int childCount = getChildCount();
// Underflow === don't show content until that index
@@ -133,6 +134,15 @@
}
}
}
+
+ // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
+ if (isLayoutRtl()) {
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ ViewState state = getViewStateFromChild(child);
+ state.xTranslation = width - state.xTranslation - child.getWidth();
+ }
+ }
}
private void applyIconStates() {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2f7d4c1..14404f5 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -84,6 +84,7 @@
import android.util.EventLog;
import android.util.PrintWriterPrinter;
import android.util.Slog;
+import android.util.StatsLog;
import android.util.SparseArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -1094,6 +1095,8 @@
active.mNumActive++;
}
r.isForeground = true;
+ StatsLog.write(StatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.userId, r.shortName,
+ StatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER);
}
r.postNotification();
if (r.app != null) {
@@ -1109,6 +1112,8 @@
decActiveForegroundAppLocked(smap, r);
}
r.isForeground = false;
+ StatsLog.write(StatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.userId, r.shortName,
+ StatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT);
if (r.app != null) {
mAm.updateLruProcessLocked(r.app, false, null);
updateServiceForegroundLocked(r.app, true);
@@ -2533,7 +2538,10 @@
cancelForegroundNotificationLocked(r);
if (r.isForeground) {
decActiveForegroundAppLocked(smap, r);
+ StatsLog.write(StatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.userId, r.shortName,
+ StatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT);
}
+
r.isForeground = false;
r.foregroundId = 0;
r.foregroundNoti = null;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a3a854d..99f36d0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -778,8 +778,11 @@
long mWaitForNetworkTimeoutMs;
/**
- * Helper class which parses out priority arguments and dumps sections according to their
- * priority. If priority arguments are omitted, function calls the legacy dump command.
+ * Helper class which strips out priority and proto arguments then calls the dump function with
+ * the appropriate arguments. If priority arguments are omitted, function calls the legacy
+ * dump command.
+ * If priority arguments are omitted all sections are dumped, otherwise sections are dumped
+ * according to their priority.
*/
private final PriorityDump.PriorityDumper mPriorityDumper = new PriorityDump.PriorityDumper() {
@Override
@@ -791,24 +794,7 @@
@Override
public void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
- if (asProto) {
- doDump(fd, pw, new String[0], asProto);
- } else {
- doDump(fd, pw, new String[]{"settings"}, asProto);
- doDump(fd, pw, new String[]{"intents"}, asProto);
- doDump(fd, pw, new String[]{"broadcasts"}, asProto);
- doDump(fd, pw, new String[]{"providers"}, asProto);
- doDump(fd, pw, new String[]{"permissions"}, asProto);
- doDump(fd, pw, new String[]{"services"}, asProto);
- doDump(fd, pw, new String[]{"recents"}, asProto);
- doDump(fd, pw, new String[]{"lastanr"}, asProto);
- doDump(fd, pw, new String[]{"starter"}, asProto);
- doDump(fd, pw, new String[]{"containers"}, asProto);
- if (mAssociations.size() > 0) {
- doDump(fd, pw, new String[]{"associations"}, asProto);
- }
- doDump(fd, pw, new String[]{"processes"}, asProto);
- }
+ doDump(fd, pw, new String[]{"-a", "--normal-priority"}, asProto);
}
@Override
@@ -15504,6 +15490,7 @@
boolean dumpClient = false;
boolean dumpCheckin = false;
boolean dumpCheckinFormat = false;
+ boolean dumpNormalPriority = false;
boolean dumpVisibleStacksOnly = false;
boolean dumpFocusedStackOnly = false;
String dumpPackage = null;
@@ -15536,6 +15523,8 @@
dumpCheckin = dumpCheckinFormat = true;
} else if ("-C".equals(opt)) {
dumpCheckinFormat = true;
+ } else if ("--normal-priority".equals(opt)) {
+ dumpNormalPriority = true;
} else if ("-h".equals(opt)) {
ActivityManagerShellCommand.dumpHelp(pw, true);
return;
@@ -15937,11 +15926,15 @@
pw.println("-------------------------------------------------------------------------------");
}
dumpActivityContainersLocked(pw);
- pw.println();
- if (dumpAll) {
- pw.println("-------------------------------------------------------------------------------");
+ // Activities section is dumped as part of the Critical priority dump. Exclude the
+ // section if priority is Normal.
+ if (!dumpNormalPriority){
+ pw.println();
+ if (dumpAll) {
+ pw.println("-------------------------------------------------------------------------------");
+ }
+ dumpActivitiesLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
}
- dumpActivitiesLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
if (mAssociations.size() > 0) {
pw.println();
if (dumpAll) {
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 77f6d44..a0f31cd 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -1266,7 +1266,7 @@
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "resolveIntent");
int modifiedFlags = flags
| PackageManager.MATCH_DEFAULT_ONLY | ActivityManagerService.STOCK_PM_FLAGS;
- if (intent.isBrowsableWebIntent()
+ if (intent.isWebIntent()
|| (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
modifiedFlags |= PackageManager.MATCH_INSTANT;
}
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
index 373d87d..13873e4 100644
--- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -328,10 +328,12 @@
@Override
public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
pw.print("Connectivity: connected=");
- pw.print(mConnected);
+ pw.println(mConnected);
+
pw.print("Tracking ");
pw.print(mTrackedJobs.size());
- pw.println(":");
+ pw.println(" jobs");
+
for (int i = 0; i < mTrackedJobs.size(); i++) {
final JobStatus js = mTrackedJobs.valueAt(i);
if (js.shouldDump(filterUid)) {
@@ -339,7 +341,9 @@
js.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, js.getSourceUid());
- pw.print(": "); pw.print(js.getJob().getRequiredNetwork());
+ pw.print(": ");
+ pw.print(js.getJob().getRequiredNetwork());
+ pw.println();
}
}
}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java
new file mode 100644
index 0000000..57a3d99
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+/** Exception thrown when parsing errors occur. */
+public class CertParsingException extends Exception {
+
+ public CertParsingException(String message) {
+ super(message);
+ }
+
+ public CertParsingException(Exception cause) {
+ super(cause);
+ }
+
+ public CertParsingException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
new file mode 100644
index 0000000..985f5b6
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static javax.xml.xpath.XPathConstants.NODESET;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertStore;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/** Utility functions related to parsing and validating public-key certificates. */
+final class CertUtils {
+
+ private static final String CERT_FORMAT = "X.509";
+ private static final String CERT_PATH_ALG = "PKIX";
+ private static final String CERT_STORE_ALG = "Collection";
+ private static final String SIGNATURE_ALG = "SHA256withRSA";
+
+ private CertUtils() {}
+
+ enum MustExist {
+ FALSE,
+ EXACTLY_ONE,
+ AT_LEAST_ONE,
+ }
+
+ /**
+ * Decodes a byte array containing an encoded X509 certificate.
+ *
+ * @param certBytes the byte array containing the encoded X509 certificate
+ * @return the decoded X509 certificate
+ * @throws CertParsingException if any parsing error occurs
+ */
+ static X509Certificate decodeCert(byte[] certBytes) throws CertParsingException {
+ return decodeCert(new ByteArrayInputStream(certBytes));
+ }
+
+ /**
+ * Decodes an X509 certificate from an {@code InputStream}.
+ *
+ * @param inStream the input stream containing the encoded X509 certificate
+ * @return the decoded X509 certificate
+ * @throws CertParsingException if any parsing error occurs
+ */
+ static X509Certificate decodeCert(InputStream inStream) throws CertParsingException {
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance(CERT_FORMAT);
+ } catch (CertificateException e) {
+ // Should not happen, as X.509 is mandatory for all providers.
+ throw new RuntimeException(e);
+ }
+ try {
+ return (X509Certificate) certFactory.generateCertificate(inStream);
+ } catch (CertificateException e) {
+ throw new CertParsingException(e);
+ }
+ }
+
+ /**
+ * Parses a byte array as the content of an XML file, and returns the root node of the XML file.
+ *
+ * @param xmlBytes the byte array that is the XML file content
+ * @return the root node of the XML file
+ * @throws CertParsingException if any parsing error occurs
+ */
+ static Element getXmlRootNode(byte[] xmlBytes) throws CertParsingException {
+ try {
+ Document document =
+ DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .parse(new ByteArrayInputStream(xmlBytes));
+ document.getDocumentElement().normalize();
+ return document.getDocumentElement();
+ } catch (SAXException | ParserConfigurationException | IOException e) {
+ throw new CertParsingException(e);
+ }
+ }
+
+ /**
+ * Gets the text contents of certain XML child nodes, given a XML root node and a list of tags
+ * representing the path to locate the child nodes. The whitespaces and newlines in the text
+ * contents are stripped away.
+ *
+ * <p>For example, the list of tags [tag1, tag2, tag3] represents the XML tree like the
+ * following:
+ *
+ * <pre>
+ * <root>
+ * <tag1>
+ * <tag2>
+ * <tag3>abc</tag3>
+ * <tag3>def</tag3>
+ * </tag2>
+ * </tag1>
+ * <root>
+ * </pre>
+ *
+ * @param mustExist whether and how many nodes must exist. If the number of child nodes does not
+ * satisfy the requirement, CertParsingException will be thrown.
+ * @param rootNode the root node that serves as the starting point to locate the child nodes
+ * @param nodeTags the list of tags representing the relative path from the root node
+ * @return a list of strings that are the text contents of the child nodes
+ * @throws CertParsingException if any parsing error occurs
+ */
+ static List<String> getXmlNodeContents(MustExist mustExist, Element rootNode,
+ String... nodeTags)
+ throws CertParsingException {
+ String expression = String.join("/", nodeTags);
+
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList nodeList;
+ try {
+ nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
+ } catch (XPathExpressionException e) {
+ throw new CertParsingException(e);
+ }
+
+ switch (mustExist) {
+ case FALSE:
+ break;
+
+ case EXACTLY_ONE:
+ if (nodeList.getLength() != 1) {
+ throw new CertParsingException(
+ "The XML file must contain exactly one node with the path "
+ + expression);
+ }
+ break;
+
+ case AT_LEAST_ONE:
+ if (nodeList.getLength() == 0) {
+ throw new CertParsingException(
+ "The XML file must contain at least one node with the path "
+ + expression);
+ }
+ break;
+
+ default:
+ throw new UnsupportedOperationException(
+ "This enum value of MustExist is not supported: " + mustExist);
+ }
+
+ List<String> result = new ArrayList<>();
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ Node node = nodeList.item(i);
+ // Remove whitespaces and newlines.
+ result.add(node.getTextContent().replaceAll("\\s", ""));
+ }
+ return result;
+ }
+
+ /**
+ * Decodes a base64-encoded string.
+ *
+ * @param str the base64-encoded string
+ * @return the decoding decoding result
+ * @throws CertParsingException if the input string is not a properly base64-encoded string
+ */
+ static byte[] decodeBase64(String str) throws CertParsingException {
+ try {
+ return Base64.getDecoder().decode(str);
+ } catch (IllegalArgumentException e) {
+ throw new CertParsingException(e);
+ }
+ }
+
+ /**
+ * Verifies a public-key signature that is computed by RSA with SHA256.
+ *
+ * @param signerPublicKey the public key of the original signer
+ * @param signature the public-key signature
+ * @param signedBytes the bytes that have been signed
+ * @throws CertValidationException if the signature verification fails
+ */
+ static void verifyRsaSha256Signature(
+ PublicKey signerPublicKey, byte[] signature, byte[] signedBytes)
+ throws CertValidationException {
+ Signature verifier;
+ try {
+ verifier = Signature.getInstance(SIGNATURE_ALG);
+ } catch (NoSuchAlgorithmException e) {
+ // Should not happen, as SHA256withRSA is mandatory for all providers.
+ throw new RuntimeException(e);
+ }
+ try {
+ verifier.initVerify(signerPublicKey);
+ verifier.update(signedBytes);
+ if (!verifier.verify(signature)) {
+ throw new CertValidationException("The signature is invalid");
+ }
+ } catch (InvalidKeyException | SignatureException e) {
+ throw new CertValidationException(e);
+ }
+ }
+
+ /**
+ * Validates a leaf certificate, and returns the certificate path if the certificate is valid.
+ * If the given validation date is null, the current date will be used.
+ *
+ * @param validationDate the date for which the validity of the certificate should be
+ * determined
+ * @param trustedRoot the certificate of the trusted root CA
+ * @param intermediateCerts the list of certificates of possible intermediate CAs
+ * @param leafCert the leaf certificate that is to be validated
+ * @return the certificate path if the leaf cert is valid
+ * @throws CertValidationException if {@code leafCert} is invalid (e.g., is expired, or has
+ * invalid signature)
+ */
+ static CertPath validateCert(
+ @Nullable Date validationDate,
+ X509Certificate trustedRoot,
+ List<X509Certificate> intermediateCerts,
+ X509Certificate leafCert)
+ throws CertValidationException {
+ PKIXParameters pkixParams =
+ buildPkixParams(validationDate, trustedRoot, intermediateCerts, leafCert);
+ CertPath certPath = buildCertPath(pkixParams);
+
+ CertPathValidator certPathValidator;
+ try {
+ certPathValidator = CertPathValidator.getInstance(CERT_PATH_ALG);
+ } catch (NoSuchAlgorithmException e) {
+ // Should not happen, as PKIX is mandatory for all providers.
+ throw new RuntimeException(e);
+ }
+ try {
+ certPathValidator.validate(certPath, pkixParams);
+ } catch (CertPathValidatorException | InvalidAlgorithmParameterException e) {
+ throw new CertValidationException(e);
+ }
+ return certPath;
+ }
+
+ @VisibleForTesting
+ static CertPath buildCertPath(PKIXParameters pkixParams) throws CertValidationException {
+ CertPathBuilder certPathBuilder;
+ try {
+ certPathBuilder = CertPathBuilder.getInstance(CERT_PATH_ALG);
+ } catch (NoSuchAlgorithmException e) {
+ // Should not happen, as PKIX is mandatory for all providers.
+ throw new RuntimeException(e);
+ }
+ try {
+ return certPathBuilder.build(pkixParams).getCertPath();
+ } catch (CertPathBuilderException | InvalidAlgorithmParameterException e) {
+ throw new CertValidationException(e);
+ }
+ }
+
+ @VisibleForTesting
+ static PKIXParameters buildPkixParams(
+ @Nullable Date validationDate,
+ X509Certificate trustedRoot,
+ List<X509Certificate> intermediateCerts,
+ X509Certificate leafCert)
+ throws CertValidationException {
+ // Create a TrustAnchor from the trusted root certificate.
+ Set<TrustAnchor> trustedAnchors = new HashSet<>();
+ trustedAnchors.add(new TrustAnchor(trustedRoot, null));
+
+ // Create a CertStore from the list of intermediate certificates.
+ List<X509Certificate> certs = new ArrayList<>(intermediateCerts);
+ certs.add(leafCert);
+ CertStore certStore;
+ try {
+ certStore =
+ CertStore.getInstance(CERT_STORE_ALG, new CollectionCertStoreParameters(certs));
+ } catch (NoSuchAlgorithmException e) {
+ // Should not happen, as Collection is mandatory for all providers.
+ throw new RuntimeException(e);
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new CertValidationException(e);
+ }
+
+ // Create a CertSelector from the leaf certificate.
+ X509CertSelector certSelector = new X509CertSelector();
+ certSelector.setCertificate(leafCert);
+
+ // Build a PKIXParameters from TrustAnchor, CertStore, and CertSelector.
+ PKIXBuilderParameters pkixParams;
+ try {
+ pkixParams = new PKIXBuilderParameters(trustedAnchors, certSelector);
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new CertValidationException(e);
+ }
+ pkixParams.addCertStore(certStore);
+
+ // If validationDate is null, the current time will be used.
+ pkixParams.setDate(validationDate);
+ pkixParams.setRevocationEnabled(false);
+
+ return pkixParams;
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java
new file mode 100644
index 0000000..99a9ff7
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+/** Exception thrown when validation or verification fails. */
+public class CertValidationException extends Exception {
+
+ public CertValidationException(String message) {
+ super(message);
+ }
+
+ public CertValidationException(Exception cause) {
+ super(cause);
+ }
+
+ public CertValidationException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java
new file mode 100644
index 0000000..2c04a86
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.SecureRandom;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+/**
+ * Parses and holds the XML file containing the list of THM public-key certificates and related
+ * metadata.
+ */
+public final class CertXml {
+
+ private static final String METADATA_NODE_TAG = "metadata";
+ private static final String METADATA_SERIAL_NODE_TAG = "serial";
+ private static final String METADATA_REFRESH_INTERVAL_NODE_TAG = "refresh-interval";
+ private static final String ENDPOINT_CERT_LIST_TAG = "endpoints";
+ private static final String ENDPOINT_CERT_ITEM_TAG = "cert";
+ private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
+ private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";
+
+ private final long serial;
+ private final long refreshInterval;
+ private final List<X509Certificate> intermediateCerts;
+ private final List<X509Certificate> endpointCerts;
+
+ private CertXml(
+ long serial,
+ long refreshInterval,
+ List<X509Certificate> intermediateCerts,
+ List<X509Certificate> endpointCerts) {
+ this.serial = serial;
+ this.refreshInterval = refreshInterval;
+ this.intermediateCerts = intermediateCerts;
+ this.endpointCerts = endpointCerts;
+ }
+
+ /** Gets the serial number of the XML file containing public-key certificates. */
+ public long getSerial() {
+ return serial;
+ }
+
+ /**
+ * Gets the refresh interval in the XML file containing public-key certificates. The refresh
+ * interval denotes the number of seconds that the client should follow to contact the server to
+ * refresh the XML file.
+ */
+ public long getRefreshInterval() {
+ return refreshInterval;
+ }
+
+ @VisibleForTesting
+ List<X509Certificate> getAllIntermediateCerts() {
+ return intermediateCerts;
+ }
+
+ @VisibleForTesting
+ List<X509Certificate> getAllEndpointCerts() {
+ return endpointCerts;
+ }
+
+ /**
+ * Chooses a random endpoint certificate from the XML file, validates the chosen certificate,
+ * and returns the certificate path including the chosen certificate if it is valid.
+ *
+ * @param trustedRoot the trusted root certificate
+ * @return the certificate path including the chosen certificate if the certificate is valid
+ * @throws CertValidationException if the chosen certificate cannot be validated based on the
+ * trusted root certificate
+ */
+ public CertPath getRandomEndpointCert(X509Certificate trustedRoot)
+ throws CertValidationException {
+ return getEndpointCert(
+ new SecureRandom().nextInt(this.endpointCerts.size()),
+ /*validationDate=*/ null,
+ trustedRoot);
+ }
+
+ @VisibleForTesting
+ CertPath getEndpointCert(
+ int index, @Nullable Date validationDate, X509Certificate trustedRoot)
+ throws CertValidationException {
+ X509Certificate chosenCert = endpointCerts.get(index);
+ return CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, chosenCert);
+ }
+
+ /**
+ * Parses a byte array as the content of the XML file containing a list of endpoint
+ * certificates.
+ *
+ * @param bytes the bytes of the XML file
+ * @return a {@code CertXml} instance that contains the parsing result
+ * @throws CertParsingException if any parsing error occurs
+ */
+ public static CertXml parse(byte[] bytes) throws CertParsingException {
+ Element rootNode = CertUtils.getXmlRootNode(bytes);
+ return new CertXml(
+ parseSerial(rootNode),
+ parseRefreshInterval(rootNode),
+ parseIntermediateCerts(rootNode),
+ parseEndpointCerts(rootNode));
+ }
+
+ private static long parseSerial(Element rootNode) throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.EXACTLY_ONE,
+ rootNode,
+ METADATA_NODE_TAG,
+ METADATA_SERIAL_NODE_TAG);
+ return Long.parseLong(contents.get(0));
+ }
+
+ private static long parseRefreshInterval(Element rootNode) throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.EXACTLY_ONE,
+ rootNode,
+ METADATA_NODE_TAG,
+ METADATA_REFRESH_INTERVAL_NODE_TAG);
+ return Long.parseLong(contents.get(0));
+ }
+
+ private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
+ throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.FALSE,
+ rootNode,
+ INTERMEDIATE_CERT_LIST_TAG,
+ INTERMEDIATE_CERT_ITEM_TAG);
+ List<X509Certificate> res = new ArrayList<>();
+ for (String content : contents) {
+ res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+ }
+ return Collections.unmodifiableList(res);
+ }
+
+ private static List<X509Certificate> parseEndpointCerts(Element rootNode)
+ throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.AT_LEAST_ONE,
+ rootNode,
+ ENDPOINT_CERT_LIST_TAG,
+ ENDPOINT_CERT_ITEM_TAG);
+ List<X509Certificate> res = new ArrayList<>();
+ for (String content : contents) {
+ res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+ }
+ return Collections.unmodifiableList(res);
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java
new file mode 100644
index 0000000..878fc6e
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+/**
+ * Parses and holds the XML file containing the signature of the XML file containing the list of THM
+ * public-key certificates.
+ */
+public final class SigXml {
+
+ private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
+ private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";
+ private static final String SIGNER_CERT_NODE_TAG = "certificate";
+ private static final String SIGNATURE_NODE_TAG = "value";
+
+ private final List<X509Certificate> intermediateCerts;
+ private final X509Certificate signerCert;
+ private final byte[] signature;
+
+ private SigXml(
+ List<X509Certificate> intermediateCerts, X509Certificate signerCert, byte[] signature) {
+ this.intermediateCerts = intermediateCerts;
+ this.signerCert = signerCert;
+ this.signature = signature;
+ }
+
+ /**
+ * Verifies the signature contained in this XML file against a trusted root certificate and the
+ * binary content of another file. The signer's public-key certificate and possible intermediate
+ * CA certificates are included in this XML file, and will be validated against the trusted root
+ * certificate.
+ *
+ * @param trustedRoot the trusted root certificate
+ * @param signedFileBytes the original file content that has been signed
+ * @throws CertValidationException if the signature verification fails, or the signer's
+ * certificate contained in this XML file cannot be validated
+ * based on the trusted root certificate
+ */
+ public void verifyFileSignature(X509Certificate trustedRoot, byte[] signedFileBytes)
+ throws CertValidationException {
+ verifyFileSignature(trustedRoot, signedFileBytes, /*validationDate=*/ null);
+ }
+
+ @VisibleForTesting
+ void verifyFileSignature(
+ X509Certificate trustedRoot, byte[] signedFileBytes, @Nullable Date validationDate)
+ throws CertValidationException {
+ CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, signerCert);
+ CertUtils.verifyRsaSha256Signature(signerCert.getPublicKey(), signature, signedFileBytes);
+ }
+
+ /**
+ * Parses a byte array as the content of the XML file containing the signature and related
+ * certificates.
+ *
+ * @param bytes the bytes of the XML file
+ * @return a {@code SigXml} instance that contains the parsing result
+ * @throws CertParsingException if any parsing error occurs
+ */
+ public static SigXml parse(byte[] bytes) throws CertParsingException {
+ Element rootNode = CertUtils.getXmlRootNode(bytes);
+ return new SigXml(
+ parseIntermediateCerts(rootNode), parseSignerCert(rootNode),
+ parseFileSignature(rootNode));
+ }
+
+ private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
+ throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.FALSE,
+ rootNode,
+ INTERMEDIATE_CERT_LIST_TAG,
+ INTERMEDIATE_CERT_ITEM_TAG);
+ List<X509Certificate> res = new ArrayList<>();
+ for (String content : contents) {
+ res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+ }
+ return Collections.unmodifiableList(res);
+ }
+
+ private static X509Certificate parseSignerCert(Element rootNode) throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.EXACTLY_ONE, rootNode, SIGNER_CERT_NODE_TAG);
+ return CertUtils.decodeCert(CertUtils.decodeBase64(contents.get(0)));
+ }
+
+ private static byte[] parseFileSignature(Element rootNode) throws CertParsingException {
+ List<String> contents =
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.EXACTLY_ONE, rootNode, SIGNATURE_NODE_TAG);
+ return CertUtils.decodeBase64(contents.get(0));
+ }
+}
diff --git a/services/core/java/com/android/server/pm/InstantAppResolver.java b/services/core/java/com/android/server/pm/InstantAppResolver.java
index af446ba..6e898bb 100644
--- a/services/core/java/com/android/server/pm/InstantAppResolver.java
+++ b/services/core/java/com/android/server/pm/InstantAppResolver.java
@@ -377,7 +377,7 @@
failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
failureIntent.setLaunchToken(token);
ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null;
- boolean isWebIntent = origIntent.isBrowsableWebIntent();
+ boolean isWebIntent = origIntent.isWebIntent();
for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) {
if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) {
Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest"
@@ -448,7 +448,7 @@
instantAppInfo.getIntentFilters();
if (instantAppFilters == null || instantAppFilters.isEmpty()) {
// No filters on web intent; no matches, 2nd phase unnecessary.
- if (origIntent.isBrowsableWebIntent()) {
+ if (origIntent.isWebIntent()) {
return null;
}
// No filters; we need to start phase two
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 940d19f..2816bbd 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2746,12 +2746,6 @@
mSettings.getDisabledSystemPkgLPr(ps.name);
if (disabledPs.codePath == null || !disabledPs.codePath.exists()
|| disabledPs.pkg == null) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Possibly deleted app: " + ps.dumpState_temp()
- + "; path: " + (disabledPs.codePath == null ? "<<NULL>>":disabledPs.codePath)
- + "; pkg: " + (disabledPs.pkg==null?"<<NULL>>":disabledPs.pkg.toString()));
-}
possiblyDeletedUpdatedSystemApps.add(ps.name);
}
}
@@ -2803,10 +2797,6 @@
for (String deletedAppName : possiblyDeletedUpdatedSystemApps) {
PackageParser.Package deletedPkg = mPackages.get(deletedAppName);
mSettings.removeDisabledSystemPackageLPw(deletedAppName);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "remove update; name: " + deletedAppName + ", exists? " + (deletedPkg != null));
-}
final String msg;
if (deletedPkg == null) {
// should have found an update, but, we didn't; remove everything
@@ -5982,7 +5972,7 @@
if (!skipPackageCheck && intent.getPackage() != null) {
return false;
}
- if (!intent.isBrowsableWebIntent()) {
+ if (!intent.isWebIntent()) {
// for non web intents, we should not resolve externally if an app already exists to
// handle it or if the caller didn't explicitly request it.
if ((resolvedActivities != null && resolvedActivities.size() != 0)
@@ -6693,7 +6683,7 @@
ai.packageName, ai.versionCode, null /* splitName */);
}
}
- if (intent.isBrowsableWebIntent() && auxiliaryResponse == null) {
+ if (intent.isWebIntent() && auxiliaryResponse == null) {
return result;
}
final PackageSetting ps = mSettings.mPackages.get(mInstantAppInstallerActivity.packageName);
@@ -8534,8 +8524,6 @@
return false;
}
- // Temporary to catch potential issues with refactoring
- private static boolean REFACTOR_DEBUG = true;
/**
* Adds a new package to the internal data structures during platform initialization.
* <p>After adding, the package is known to the system and available for querying.
@@ -8576,10 +8564,6 @@
synchronized (mPackages) {
renamedPkgName = mSettings.getRenamedPackageLPr(pkg.mRealPackage);
final String realPkgName = getRealPackageName(pkg, renamedPkgName);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Add pkg: " + pkg.packageName + (realPkgName==null?"":", realName: " + realPkgName));
-}
if (realPkgName != null) {
ensurePackageRenamed(pkg, renamedPkgName);
}
@@ -8594,12 +8578,6 @@
if (DEBUG_INSTALL && isSystemPkgUpdated) {
Slog.d(TAG, "updatedPkg = " + disabledPkgSetting);
}
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "SSP? " + scanSystemPartition
- + ", exists? " + pkgAlreadyExists + (pkgAlreadyExists?" "+pkgSetting.toString():"")
- + ", upgraded? " + isSystemPkgUpdated + (isSystemPkgUpdated?" "+disabledPkgSetting.toString():""));
-}
final SharedUserSetting sharedUserSetting = (pkg.mSharedUserId != null)
? mSettings.getSharedUserLPw(pkg.mSharedUserId,
@@ -8611,12 +8589,6 @@
Log.d(TAG, "Shared UserID " + pkg.mSharedUserId
+ " (uid=" + sharedUserSetting.userId + "):"
+ " packages=" + sharedUserSetting.packages);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Shared UserID " + pkg.mSharedUserId
- + " (uid=" + sharedUserSetting.userId + "):"
- + " packages=" + sharedUserSetting.packages);
-}
}
if (scanSystemPartition) {
@@ -8625,10 +8597,6 @@
// version on /data, cycle through all of its children packages and
// remove children that are no longer defined.
if (isSystemPkgUpdated) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Disable child packages");
-}
final int scannedChildCount = (pkg.childPackages != null)
? pkg.childPackages.size() : 0;
final int disabledChildCount = disabledPkgSetting.childPackageNames != null
@@ -8640,19 +8608,11 @@
for (int j = 0; j < scannedChildCount; j++) {
PackageParser.Package childPkg = pkg.childPackages.get(j);
if (childPkg.packageName.equals(disabledChildPackageName)) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Ignore " + disabledChildPackageName);
-}
disabledPackageAvailable = true;
break;
}
}
if (!disabledPackageAvailable) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Disable " + disabledChildPackageName);
-}
mSettings.removeDisabledSystemPackageLPw(disabledChildPackageName);
}
}
@@ -8661,44 +8621,17 @@
disabledPkgSetting /* pkgSetting */, null /* disabledPkgSetting */,
null /* originalPkgSetting */, null, parseFlags, scanFlags,
(pkg == mPlatformPackage), user);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Scan disabled system package");
-Slog.e("TODD",
- "Pre: " + request.pkgSetting.dumpState_temp());
-}
-final ScanResult result =
scanPackageOnlyLI(request, mFactoryTest, -1L);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Post: " + (result.success?result.pkgSetting.dumpState_temp():"FAILED scan"));
-}
}
}
}
final boolean newPkgChangedPaths =
pkgAlreadyExists && !pkgSetting.codePathString.equals(pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "paths changed? " + newPkgChangedPaths
- + "; old: " + pkg.codePath
- + ", new: " + (pkgSetting==null?"<<NULL>>":pkgSetting.codePathString));
-}
final boolean newPkgVersionGreater =
pkgAlreadyExists && pkg.getLongVersionCode() > pkgSetting.versionCode;
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "version greater? " + newPkgVersionGreater
- + "; old: " + pkg.getLongVersionCode()
- + ", new: " + (pkgSetting==null?"<<NULL>>":pkgSetting.versionCode));
-}
final boolean isSystemPkgBetter = scanSystemPartition && isSystemPkgUpdated
&& newPkgChangedPaths && newPkgVersionGreater;
-if (REFACTOR_DEBUG) {
- Slog.e("TODD",
- "system better? " + isSystemPkgBetter);
-}
if (isSystemPkgBetter) {
// The version of the application on /system is greater than the version on
// /data. Switch back to the application on /system.
@@ -8714,13 +8647,6 @@
+ " name: " + pkgSetting.name
+ "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
+ "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "System package changed;"
- + " name: " + pkgSetting.name
- + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
- + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-}
final InstallArgs args = createInstallArgsForExisting(
packageFlagsToInstallFlags(pkgSetting), pkgSetting.codePathString,
@@ -8732,10 +8658,6 @@
}
if (scanSystemPartition && isSystemPkgUpdated && !isSystemPkgBetter) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "THROW exception; system pkg version not good enough");
-}
// The version of the application on the /system partition is less than or
// equal to the version on the /data partition. Throw an exception and use
// the application already installed on the /data partition.
@@ -8766,11 +8688,6 @@
logCriticalInfo(Log.WARN,
"System package signature mismatch;"
+ " name: " + pkgSetting.name);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "System package signature mismatch;"
- + " name: " + pkgSetting.name);
-}
try (PackageFreezer freezer = freezePackage(pkg.packageName,
"scanPackageInternalLI")) {
deletePackageLIF(pkg.packageName, null, true, null, 0, null, false, null);
@@ -8785,13 +8702,6 @@
+ " name: " + pkgSetting.name
+ "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
+ "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "System package enabled;"
- + " name: " + pkgSetting.name
- + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
- + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
-}
InstallArgs args = createInstallArgsForExisting(
packageFlagsToInstallFlags(pkgSetting), pkgSetting.codePathString,
pkgSetting.resourcePathString, getAppDexInstructionSets(pkgSetting));
@@ -8808,35 +8718,13 @@
+ " name: " + pkgSetting.name
+ "; old: " + pkgSetting.codePathString + " @ " + pkgSetting.versionCode
+ "; new: " + pkg.codePath + " @ " + pkg.codePath);
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "System package disabled;"
- + " name: " + pkgSetting.name
- + "; old: " + pkgSetting.codePathString + " @ " + pkgSetting.versionCode
- + "; new: " + pkg.codePath + " @ " + pkg.codePath);
-}
}
}
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Scan package");
-Slog.e("TODD",
- "Pre: " + (pkgSetting==null?"<<NONE>>":pkgSetting.dumpState_temp()));
-}
final PackageParser.Package scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags
| SCAN_UPDATE_SIGNATURE, currentTime, user);
-if (REFACTOR_DEBUG) {
-pkgSetting = mSettings.getPackageLPr(pkg.packageName);
-Slog.e("TODD",
- "Post: " + (pkgSetting==null?"<<NONE>>":pkgSetting.dumpState_temp()));
-}
if (shouldHideSystemApp) {
-if (REFACTOR_DEBUG) {
-Slog.e("TODD",
- "Disable package: " + pkg.packageName);
-}
synchronized (mPackages) {
mSettings.disableSystemPackageLPw(pkg.packageName, true);
}
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 3e2bd4a..ea05b74 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -97,35 +97,6 @@
+ " " + name + "/" + appId + "}";
}
- // Temporary to catch potential issues with refactoring
- public String dumpState_temp() {
- String flags = "";
- flags += ((pkgFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
- flags += ((pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
- if ("".equals(flags)) {
- flags = "-";
- }
- String privFlags = "";
- privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
- privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
- privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
- if ("".equals(privFlags)) {
- privFlags = "-";
- }
- return "PackageSetting{"
- + Integer.toHexString(System.identityHashCode(this))
- + " " + name + (realName == null ? "" : "("+realName+")") + "/" + appId + (sharedUser==null?"":" u:" + sharedUser.name+"("+sharedUserId+")")
- + ", ver:" + versionCode
- + ", path: " + codePath
- + ", pABI: " + primaryCpuAbiString
- + ", sABI: " + secondaryCpuAbiString
- + ", oABI: " + cpuAbiOverrideString
- + ", flags: " + flags
- + ", privFlags: " + privFlags
- + ", pkg: " + (pkg == null ? "<<NULL>>" : pkg.dumpState_temp())
- + "}";
- }
-
public void copyFrom(PackageSetting orig) {
super.copyFrom(orig);
doCopy(orig);
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index f498cdd..c280739 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -113,7 +113,6 @@
new StatFs(Environment.getRootDirectory().getAbsolutePath());
private final StatFs mStatFsTemp =
new StatFs(Environment.getDownloadCacheDirectory().getAbsolutePath());
- private final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
public StatsCompanionService(Context context) {
super();
@@ -677,12 +676,13 @@
}
private synchronized BluetoothActivityEnergyInfo pullBluetoothData() {
+ final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
- SynchronousResultReceiver bluetoothReceiver = null;
- bluetoothReceiver = new SynchronousResultReceiver("bluetooth");
+ SynchronousResultReceiver bluetoothReceiver = new SynchronousResultReceiver("bluetooth");
adapter.requestControllerActivityEnergyInfo(bluetoothReceiver);
return awaitControllerInfo(bluetoothReceiver);
} else {
+ Slog.e(TAG, "Failed to get bluetooth adapter!");
return null;
}
}
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index b435605..4394a99 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
-import android.util.ArrayMap;
import android.view.SurfaceControl;
import android.graphics.Rect;
@@ -124,17 +123,30 @@
}
}
- @VisibleForTesting
- ArrayMap<WindowContainer, DimState> mDimLayerUsers = new ArrayMap<>();
-
/**
* The {@link WindowContainer} that our Dim's are bounded to. We may be dimming on behalf of the
* host, some controller of it, or one of the hosts children.
*/
private WindowContainer mHost;
+ private WindowContainer mLastRequestedDimContainer;
+ @VisibleForTesting
+ DimState mDimState;
+
+ private final SurfaceAnimatorStarter mSurfaceAnimatorStarter;
+
+ @VisibleForTesting
+ interface SurfaceAnimatorStarter {
+ void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t,
+ AnimationAdapter anim, boolean hidden);
+ }
Dimmer(WindowContainer host) {
+ this(host, SurfaceAnimator::startAnimation);
+ }
+
+ Dimmer(WindowContainer host, SurfaceAnimatorStarter surfaceAnimatorStarter) {
mHost = host;
+ mSurfaceAnimatorStarter = surfaceAnimatorStarter;
}
private SurfaceControl makeDimLayer() {
@@ -146,29 +158,32 @@
}
/**
- * Retreive the DimState for a given child of the host.
+ * Retrieve the DimState, creating one if it doesn't exist.
*/
private DimState getDimState(WindowContainer container) {
- DimState state = mDimLayerUsers.get(container);
- if (state == null) {
+ if (mDimState == null) {
final SurfaceControl ctl = makeDimLayer();
- state = new DimState(ctl);
+ mDimState = new DimState(ctl);
/**
* See documentation on {@link #dimAbove} to understand lifecycle management of Dim's
* via state resetting for Dim's with containers.
*/
if (container == null) {
- state.mDontReset = true;
+ mDimState.mDontReset = true;
}
- mDimLayerUsers.put(container, state);
}
- return state;
+
+ mLastRequestedDimContainer = container;
+ return mDimState;
}
private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer,
float alpha) {
final DimState d = getDimState(container);
if (container != null) {
+ // The dim method is called from WindowState.prepareSurfaces(), which is always called
+ // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
+ // relative to the highest Z layer with a dim.
t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
} else {
t.setLayer(d.mDimLayer, Integer.MAX_VALUE);
@@ -237,11 +252,8 @@
* a chance to request dims to continue.
*/
void resetDimStates() {
- for (int i = mDimLayerUsers.size() - 1; i >= 0; i--) {
- final DimState state = mDimLayerUsers.valueAt(i);
- if (!state.mDontReset) {
- state.mDimming = false;
- }
+ if (mDimState != null && !mDimState.mDontReset) {
+ mDimState.mDimming = false;
}
}
@@ -254,30 +266,25 @@
* @return true if any Dims were updated.
*/
boolean updateDims(SurfaceControl.Transaction t, Rect bounds) {
- boolean didSomething = false;
- for (int i = mDimLayerUsers.size() - 1; i >= 0; i--) {
- DimState state = mDimLayerUsers.valueAt(i);
- WindowContainer container = mDimLayerUsers.keyAt(i);
-
- // TODO: We want to animate the addition and removal of Dim's instead of immediately
- // acting. When we do this we need to take care to account for the "Replacing Windows"
- // case (and seamless dim transfer).
- if (!state.mDimming) {
- mDimLayerUsers.removeAt(i);
- startDimExit(container, state.mSurfaceAnimator, t);
- } else {
- didSomething = true;
- // TODO: Once we use geometry from hierarchy this falls away.
- t.setSize(state.mDimLayer, bounds.width(), bounds.height());
- t.setPosition(state.mDimLayer, bounds.left, bounds.top);
- if (!state.isVisible) {
- state.isVisible = true;
- t.show(state.mDimLayer);
- startDimEnter(container, state.mSurfaceAnimator, t);
- }
- }
+ if (mDimState == null) {
+ return false;
}
- return didSomething;
+
+ if (!mDimState.mDimming) {
+ startDimExit(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t);
+ mDimState = null;
+ return false;
+ } else {
+ // TODO: Once we use geometry from hierarchy this falls away.
+ t.setSize(mDimState.mDimLayer, bounds.width(), bounds.height());
+ t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top);
+ if (!mDimState.isVisible) {
+ mDimState.isVisible = true;
+ t.show(mDimState.mDimLayer);
+ startDimEnter(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t);
+ }
+ return true;
+ }
}
private void startDimEnter(WindowContainer container, SurfaceAnimator animator,
@@ -292,7 +299,7 @@
private void startAnim(WindowContainer container, SurfaceAnimator animator,
SurfaceControl.Transaction t, float startAlpha, float endAlpha) {
- animator.startAnimation(t, new LocalAnimationAdapter(
+ mSurfaceAnimatorStarter.startAnimation(animator, t, new LocalAnimationAdapter(
new AlphaAnimationSpec(startAlpha, endAlpha, getDimDuration(container)),
mHost.mService.mSurfaceAnimationRunner), false /* hidden */);
}
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 0512a08..83baee1 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -53,7 +53,8 @@
SurfaceControl mLeash;
private final Animatable mAnimatable;
private final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
- private final Runnable mAnimationFinishedCallback;
+ @VisibleForTesting
+ final Runnable mAnimationFinishedCallback;
private boolean mAnimationStartDelayed;
/**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d565a6a..676fb9f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -904,16 +904,9 @@
public static WindowManagerService main(final Context context, final InputManagerService im,
final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore,
WindowManagerPolicy policy) {
- return main(context, im, haveInputMethods, showBootMsgs, onlyCore, policy,
- new SurfaceAnimationRunner());
- }
-
- public static WindowManagerService main(final Context context, final InputManagerService im,
- final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore,
- WindowManagerPolicy policy, SurfaceAnimationRunner surfaceAnimationRunner) {
DisplayThread.getHandler().runWithScissors(() ->
sInstance = new WindowManagerService(context, im, haveInputMethods, showBootMsgs,
- onlyCore, policy, surfaceAnimationRunner), 0);
+ onlyCore, policy), 0);
return sInstance;
}
@@ -936,7 +929,7 @@
private WindowManagerService(Context context, InputManagerService inputManager,
boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore,
- WindowManagerPolicy policy, SurfaceAnimationRunner surfaceAnimationRunner) {
+ WindowManagerPolicy policy) {
installLock(this, INDEX_WINDOW);
mContext = context;
mHaveInputMethods = haveInputMethods;
@@ -1063,7 +1056,7 @@
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG_WM);
mHoldingScreenWakeLock.setReferenceCounted(false);
- mSurfaceAnimationRunner = surfaceAnimationRunner;
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner();
mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean(
com.android.internal.R.bool.config_allowTheaterModeWakeFromWindowLayout);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 3bee1e8..48d29e3 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4480,8 +4480,13 @@
if (!mAnimatingExit && mAppDied) {
mIsDimming = true;
dimmer.dimAbove(getPendingTransaction(), this, DEFAULT_DIM_AMOUNT_DEAD_WINDOW);
- } else if ((mAttrs.flags & FLAG_DIM_BEHIND) != 0
- && !mAnimatingExit && isVisible() && !mWinAnimator.mLastHidden) {
+ } else if ((mAttrs.flags & FLAG_DIM_BEHIND) != 0 && isVisibleNow()
+ && !mWinAnimator.mLastHidden) {
+ // Only show a dim behind when the following is satisfied:
+ // 1. The window has the flag FLAG_DIM_BEHIND
+ // 2. The WindowToken is not hidden so dims aren't shown when the window is exiting.
+ // 3. The WS is considered visible according to the isVisible() method
+ // 4. The WSA is not hidden.
mIsDimming = true;
dimmer.dimBelow(getPendingTransaction(), this, mAttrs.dimAmount);
}
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem
new file mode 100644
index 0000000..b5d513c
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem
@@ -0,0 +1,49 @@
+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
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem
new file mode 100644
index 0000000..c412709
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem
@@ -0,0 +1,2 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem
new file mode 100644
index 0000000..9137b16
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem
new file mode 100644
index 0000000..e3abeaa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem
@@ -0,0 +1,70 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+BuwwuQxvQDF4pmQd
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem
new file mode 100644
index 0000000..5c16a1f
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem
@@ -0,0 +1,204 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ ce:df:04:67:90:d7:00:64
+ Signature Algorithm: sha512WithRSAEncryption
+ Issuer: CN = Google CryptAuthVault
+ Validity
+ Not Before: Jan 11 06:49:33 2018 GMT
+ Not After : Jan 6 06:49:33 2038 GMT
+ Subject: CN = Google CryptAuthVault
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (8196 bit)
+ Modulus:
+ 09:c1:6f:d3:93:35:b1:e2:cf:60:85:bb:a0:58:af:
+ 87:bb:b9:a2:d0:ad:32:6e:eb:ca:ad:14:8e:96:d3:
+ 40:f7:10:c4:94:1b:63:37:14:40:5e:c2:6c:f6:13:
+ 75:60:e2:77:81:2e:3b:c6:5e:73:4d:e8:c2:7b:1d:
+ b8:58:4a:b9:de:87:df:22:10:e2:e7:86:8a:a0:81:
+ a0:6d:43:eb:e3:7c:7c:5f:51:f1:ac:13:98:0b:1b:
+ 36:0e:b7:54:89:f1:e8:8c:1c:05:99:e3:16:5b:08:
+ 21:fb:4e:17:1a:88:24:16:21:88:a8:4d:26:13:ed:
+ 17:58:bd:76:7f:8d:e4:78:c4:2a:69:09:3b:7a:57:
+ 85:88:51:5c:ca:7b:3b:02:ff:7a:9c:73:84:15:c5:
+ 61:3e:da:0e:0f:69:9d:a3:3a:0b:ee:21:9a:14:3f:
+ d2:ad:a8:cb:36:2d:02:a1:66:ac:47:a7:6e:d2:3a:
+ 1b:8e:d2:cc:c7:3e:b2:13:53:b9:82:ed:ab:6d:63:
+ fd:9d:ee:03:74:70:07:d4:b3:7e:ef:7d:17:ae:ad:
+ 96:04:d7:bf:b2:bb:61:2e:7b:de:13:77:4d:06:3f:
+ fa:79:0b:7c:5c:bf:a0:5d:8b:41:16:2f:28:71:7e:
+ 36:fd:01:1f:34:0b:ce:a0:c4:60:16:88:2d:c3:1f:
+ de:2d:82:7b:f6:fb:c1:98:e2:b4:ba:85:00:8d:6c:
+ c2:a1:b3:80:7f:d0:a4:1d:ce:ad:7e:75:33:f0:7b:
+ 50:21:76:9c:1e:cf:6b:c9:27:bb:89:01:0c:69:33:
+ d9:0e:35:8c:02:f4:5b:fa:ad:8f:8d:d5:c0:90:d6:
+ d1:8f:7c:3a:ea:90:88:2a:89:ad:3a:c6:76:fe:db:
+ fe:98:67:55:50:6f:16:bb:48:8d:83:1f:c8:ad:f8:
+ 8f:2e:55:2d:77:2d:57:90:92:91:68:f9:de:e9:77:
+ 9a:f1:85:1c:90:fe:ea:79:de:0d:1e:0f:52:49:98:
+ 37:49:21:4f:8b:7f:46:a9:d0:97:06:04:d1:ae:84:
+ ae:ad:7e:46:87:fa:fc:3a:16:4b:37:81:2c:16:15:
+ 25:a1:e1:47:2f:66:ae:e7:51:fd:57:10:ca:72:ca:
+ b7:2e:99:b2:de:0b:ca:1e:9a:3c:c1:dc:a5:3c:06:
+ 65:b1:be:e2:65:e7:59:e4:da:2c:49:c6:be:2b:e5:
+ 76:4b:13:5c:ec:62:a5:c3:bf:7d:72:5f:0e:cb:38:
+ 6f:48:25:1b:33:f7:18:3d:3e:1f:91:19:07:d7:28:
+ 63:33:53:e3:b4:19:77:d7:6b:c4:77:6b:2f:1b:4f:
+ a2:2f:8c:7a:a9:06:80:27:72:17:20:59:3b:c1:b6:
+ 93:f2:6e:cb:38:93:ea:1c:be:e7:45:57:26:79:7a:
+ 52:3c:63:ec:c5:54:60:0b:08:48:7b:07:82:a1:13:
+ 0e:cb:a4:41:b6:ca:bd:32:a0:de:0c:4f:54:d3:7f:
+ bc:a5:b0:ca:1e:ca:2d:c1:48:68:e1:28:3c:e8:12:
+ b6:49:95:b2:aa:d7:9b:b6:0b:13:a0:6f:ce:6f:27:
+ bb:8a:f0:25:e0:05:f5:8b:1e:83:d0:44:63:ce:93:
+ 45:19:80:23:b7:d6:e1:e0:c4:3e:7c:60:09:45:2f:
+ 71:84:80:87:99:9c:6b:22:32:22:bd:0a:4e:e5:e4:
+ 7a:fc:18:6d:54:47:31:59:b2:f4:36:aa:ce:c3:fc:
+ 60:55:c8:a8:5b:c3:5d:10:85:4b:2f:e0:32:4a:0d:
+ 6f:d6:68:a9:d9:26:8d:9f:95:06:c8:c0:28:8f:ae:
+ fc:76:9f:ea:98:1d:a4:66:5b:54:62:84:16:66:95:
+ 1c:29:2b:b4:fa:7f:83:2e:24:13:8a:a6:77:f4:88:
+ 69:c7:06:f2:8e:84:6f:d5:fd:7d:ba:93:4c:36:6f:
+ df:89:1c:1a:0c:4e:bc:12:55:21:a3:47:a6:b4:e3:
+ 82:f6:04:9d:16:83:29:39:66:66:80:94:98:19:47:
+ ce:f6:df:eb:c6:16:06:62:33:d0:af:2f:60:f7:71:
+ f6:bf:7c:2e:25:20:a5:b4:c7:b4:a6:48:62:b2:67:
+ 6e:03:e7:6c:2f:62:70:fd:b8:da:4b:01:e3:26:93:
+ c7:54:16:73:5e:2f:4c:db:f5:95:4b:6f:e3:32:44:
+ 99:a6:a8:ca:73:65:11:32:45:7f:f4:f3:d1:02:89:
+ c6:d4:fe:97:f0:48:03:3f:24:10:93:00:70:ab:1f:
+ a1:3c:7d:39:3b:5f:b8:bf:39:a5:76:c2:83:16:ca:
+ 2a:62:a7:e9:1c:f1:50:2c:d2:ce:03:1b:a0:2d:33:
+ e9:6f:19:c7:2d:a0:2c:88:6a:32:6f:00:5c:01:7b:
+ b1:a9:70:ad:18:ab:57:89:56:b0:cf:4c:3a:15:44:
+ 81:f8:dc:bf:fa:a6:41:26:2b:d4:04:6b:c3:07:a0:
+ 1d:1a:b1:3c:0d:ea:87:5d:64:7e:ae:58:48:55:3b:
+ 5b:bf:9f:26:b5:87:e9:5b:8b:67:40:75:7e:3d:be:
+ 96:ca:0c:35:e9:05:03:55:0b:0f:c0:ca:ed:a0:05:
+ 22:1f:58:d7:2e:09:b7:ff:f1:32:e3:8f:70:3e:4f:
+ 58:d1:85:99:49:3b:a0:f7:b0:88:85:0c:2e:35:d2:
+ 59:c8:2b:41:56:1b:64:40:d6:fe:fa:28:30:9a:28:
+ e3:b5:60:d5:f4:0f:7a:17:a3:6c:e3:44:d6:70:48:
+ 69:e4:55:84:97
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Subject Key Identifier:
+ D7:7B:A1:87:92:8B:B8:52:1A:29:91:A5:36:5A:1C:B4:32:EB:EC:D1
+ X509v3 Authority Key Identifier:
+ keyid:D7:7B:A1:87:92:8B:B8:52:1A:29:91:A5:36:5A:1C:B4:32:EB:EC:D1
+
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ Signature Algorithm: sha512WithRSAEncryption
+ 01:9f:2d:35:d6:f7:ae:3e:99:53:70:d7:b2:2d:e2:a1:59:19:
+ ff:cb:d5:4c:40:4c:a1:68:b9:6c:0b:50:e2:df:95:8d:f2:8c:
+ 23:8f:8f:a0:ba:02:e5:0d:db:44:73:fa:6f:ce:09:3e:66:40:
+ 2e:a6:54:24:ae:c5:3d:d3:b7:6f:c2:40:cf:6f:8a:d6:64:1e:
+ 61:8d:ba:fd:c9:5c:84:1c:ad:5b:1e:1e:d1:49:49:09:ac:1d:
+ cd:47:0a:87:d6:5c:96:cf:f2:df:69:57:db:57:80:ad:69:a1:
+ 11:85:38:73:64:2a:7f:33:07:8e:4e:2b:e0:d9:7a:52:96:ec:
+ b9:b3:c9:44:11:b3:02:fd:d1:ae:22:c0:4c:8a:d5:f7:5c:b9:
+ 5b:61:ad:8d:0f:76:87:64:ba:3a:5f:dc:90:30:57:d3:0a:38:
+ 30:60:6d:a7:78:36:2e:a6:7b:dc:2c:83:65:ae:52:58:72:b6:
+ 12:ac:8b:e8:42:7f:57:7e:c9:e3:53:70:17:cf:e8:dc:eb:12:
+ eb:3b:71:2d:5c:38:62:f5:ef:90:4d:f7:27:bd:1b:20:97:f1:
+ e3:f5:26:6b:d7:0d:4b:d4:f3:c9:a3:e2:a3:b1:16:af:56:fc:
+ da:1d:79:41:02:fb:d4:b4:4a:23:26:b9:11:de:3f:9b:e7:3b:
+ d0:07:a7:60:56:b3:34:cd:48:4c:43:dc:d1:a7:94:7f:c6:a1:
+ de:67:fd:13:4d:07:bd:90:46:bc:42:e4:73:b9:12:24:2b:95:
+ 9b:87:be:84:8b:1d:d2:fb:6b:93:b1:b8:f8:eb:69:e4:e0:4d:
+ 28:3d:1f:29:89:8a:29:4b:86:44:14:47:eb:29:6d:78:1c:ea:
+ 61:f5:9b:1c:23:89:7f:1d:f0:e6:4c:d7:e0:a7:21:65:eb:65:
+ 2b:bf:35:41:9e:8c:fe:b1:b8:53:81:b3:c7:3d:c0:97:39:4a:
+ e1:9a:9c:fd:23:9a:01:93:21:00:65:8b:a7:4a:fc:d8:ff:d4:
+ 97:52:cc:fa:a6:c5:c7:26:65:73:2d:07:27:7b:f6:10:4e:da:
+ 56:cd:5f:f0:18:3e:dc:8e:30:e5:6d:fc:ec:98:60:9f:67:c8:
+ ea:3c:1a:22:72:5e:48:51:57:72:66:c2:60:b9:f1:19:1d:7c:
+ 4a:65:0e:f0:2f:b4:7a:8c:aa:e3:fc:6b:42:0d:ec:b5:5a:bd:
+ 90:4f:c5:57:e4:94:e4:f5:c2:50:16:38:23:0d:66:80:c8:20:
+ 69:18:46:90:bd:82:5c:68:ec:64:d8:3f:96:6a:9e:5a:c7:c9:
+ d4:7d:6f:fd:f6:f5:45:03:41:09:2a:c4:95:57:3c:3b:75:a4:
+ 6d:cb:45:29:7d:9b:31:d9:27:f3:a6:0d:26:ca:69:a0:07:cf:
+ 8d:63:ab:7a:f1:d2:f9:0b:fc:71:02:fe:66:11:0f:30:18:99:
+ 8f:e3:98:90:a6:8e:53:e8:b5:55:32:b9:2d:2d:fe:5e:23:3c:
+ 65:00:b4:08:5b:f0:20:a5:21:fd:24:a0:aa:a4:8b:5d:c5:f8:
+ ac:00:82:2c:f5:ef:17:1d:b5:49:24:0d:09:75:ee:eb:b4:08:
+ fe:4d:46:34:87:4d:31:4a:ac:9f:49:26:e9:70:32:7d:94:8a:
+ 12:64:e2:6f:15:07:56:3b:10:7c:73:7b:d9:64:61:a1:32:e4:
+ 45:9b:4e:ac:50:7b:dc:1e:3a:3c:2b:09:db:ab:23:b1:0c:68:
+ de:aa:de:98:5a:dc:ba:59:c3:62:9f:45:9c:89:1d:f7:bc:61:
+ c8:cf:05:4d:c4:d9:e6:b9:8d:f4:f1:b3:6c:32:f6:3d:8e:c9:
+ 9d:df:b8:5d:9a:6c:27:99:08:1e:ec:02:1a:ed:33:0f:8d:a2:
+ a6:d2:f5:7f:d7:3b:45:48:e0:d6:08:8d:8a:ee:49:66:2f:63:
+ 2d:38:93:06:7e:a5:1e:39:f8:cf:00:d6:d2:de:53:09:04:01:
+ fd:ab:12:cc:ec:a9:e0:fa:77:ea:56:db:7e:34:6f:4c:c5:c4:
+ db:3f:cd:05:90:16:bf:e8:9c:46:82:38:ec:8b:00:e6:86:bd:
+ 8c:4c:26:0e:2b:f7:83:f9:66:62:90:c3:a2:7b:2b:c7:9b:6b:
+ e0:c5:80:9d:5a:b8:5a:19:f7:37:b7:aa:10:d9:83:be:99:72:
+ 3b:7b:a7:77:17:76:bd:d1:45:18:90:12:20:8d:ef:73:bb:44:
+ 4b:c6:70:41:86:ea:11:c8:6c:2f:b9:00:25:aa:03:03:05:92:
+ 33:4c:ed:15:9f:07:b0:ec:a4:4b:77:32:e1:59:b8:a2:92:18:
+ bc:d6:ae:8b:83:ad:9a:5a:36:dd:17:a5:1e:e8:05:57:72:1f:
+ 9d:aa:6a:fe:f5:a5:6d:d1:8e:84:4d:34:bb:ee:7a:d0:ca:09:
+ 72:2c:a2:1e:37:1e:9c:10:70:e3:11:35:e5:3d:81:9b:08:09:
+ 6b:74:a0:1d:4c:0e:27:82:70:59:9f:31:90:ff:cc:e0:b4:ff:
+ 73:bc:82:55:03:a7:2e:38:09:fc:18:51:2c:ad:be:c6:37:6d:
+ 10:48:3c:32:25:6a:d8:8b:a7:fe:9b:ae:03:f6:b3:8a:e0:9c:
+ f8:b7:e9:44:7e:37:1f:25:e3:3f:51:c3:65:86:60:13:70:c1:
+ d7:58:f0:a8:2a:43:9f:96:5e:0f:05:cf:d6:8a:0b:f5:c5:8c:
+ 3b:d1:16:78:a5:a8:b4:ec:bc:c9:c4:d1:d8:86:fa:44:dd
\ No newline at end of file
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml
new file mode 100644
index 0000000..6519f49
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <serial>
+ 1000
+ </serial>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <endpoints>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml
new file mode 100644
index 0000000..3da0122
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <serial>
+ 1000
+ </serial>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <endpoints>
+ <cert>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </cert>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml
new file mode 100644
index 0000000..4370ff0
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <endpoints>
+ <cert>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </cert>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml
new file mode 100644
index 0000000..0f4e8a3
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <serial>
+ 1000
+ </serial>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <endpoints>
+ <cert>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </cert>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml
new file mode 100644
index 0000000..a2685aa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <serial>
+ 1000
+ </serial>
+ <serial>
+ 1000
+ </serial>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <endpoints>
+ <cert>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </cert>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml
new file mode 100644
index 0000000..5dc8ffa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+ <certificate>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </certificate>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml
new file mode 100644
index 0000000..aa81295
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+ <value>
+ VEVTVA==
+ </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml
new file mode 100644
index 0000000..09d0f44
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+ <certificate>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </certificate>
+ <value>
+ VEVTVA==
+ </value>
+ <value>
+ VEVTVA==
+ </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml
new file mode 100644
index 0000000..44e8993
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+ <certificate>
+ signer certificate 1
+ </certificate>
+ <certificate>
+ signer certificate 2
+ </certificate>
+ <value>
+ VEVTVA==
+ </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml
new file mode 100644
index 0000000..e59bf36
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <serial>
+ 1000
+ </serial>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <endpoints>
+ <cert>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </cert>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml
new file mode 100644
index 0000000..be4893f
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+ <metadata>
+ <serial>
+ 1000
+ </serial>
+ <creation-time>
+ 1515697631
+ </creation-time>
+ <refresh-interval>
+ 2592000
+ </refresh-interval>
+ <previous>
+ <serial>
+ 0
+ </serial>
+ <hash>
+ 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+ </hash>
+ </previous>
+ </metadata>
+ <intermediates>
+ <cert>
+ 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
+ +a0fa5B3pxc8BN0LsHA0stT/Y2o=
+ </cert>
+ <cert>
+ MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+ Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa
+ Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs
+ dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+ AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX
+ TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9
+ f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ
+ EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf
+ lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE
+ hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU
+ 0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD
+ K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
+ hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds
+ E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn
+ AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n
+ N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE
+ tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2
+ gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb
+ RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz
+ 7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw
+ pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n
+ UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp
+ HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g
+ </cert>
+ </intermediates>
+ <endpoints>
+ <cert>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </cert>
+ <cert>
+ MIICrDCCAZSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOjE4MDYGA1UEAwwvR29v
+ Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZSBJbnRlcm1lZGlhdGUwHhcN
+ MTgwMTEyMDEwMzA5WhcNMTkwMTEyMDEwMzA5WjArMSkwJwYDVQQDDCBHb29nbGUg
+ Q3J5cHRBdXRoVmF1bHQgSW5zdGFuY2UgMjBZMBMGByqGSM49AgEGCCqGSM49AwEH
+ A0IABGhmBQyWdjsXKJRbkW4iIrvt6iqhX5t2XGt/vZS9CoOl0fs+EvJXo4kgrnx8
+ /8SGxz3pwRkFhY943QYy6a1gv/2jgZUwgZIwCQYDVR0TBAIwADAdBgNVHQ4EFgQU
+ xFmLyxUS2JHKURBtewBKRP6kQBgwVgYDVR0jBE8wTYAU0dKv4xTaEmdwHyox3tY8
+ H8XVDGKhMaQvMC0xKzApBgNVBAMMIkdvb2dsZSBDcnlwdEF1dGhWYXVsdCBJbnRl
+ cm1lZGlhdGWCAhAAMA4GA1UdDwEB/wQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEA
+ EJWpl7HU6LxukLqhw2tVZr7IRrKIucRk+RKaaiMx1Hx2jsTTskiJRiZas/xoPSqX
+ z1K5DVgI486i7HyqnWkGH5xVzCsv+rya5FOSTS3jVtgtoA4HFEqeeAcDowPDqVw3
+ yFTA55ukZnzVaPLpDfPqkhzWiuLQ/4fI6YCmOnWB8KtHTMdyGsDSAkpoxpok++NJ
+ Lu79BoBLe2ucjN383lTlieLxmrmHjF9ryYSQczcm0v6irMOMxEovw5iT4LHiEhbm
+ DfOPW909fe/s+K3TGZ3Q6U77x8g5k9dVovMgA4pFwtREtknFjeK1wXR3/eXGcP3W
+ 0bMX1yTWYJQFWCG3DFoC5w==
+ </cert>
+ <cert>
+ MIIDkjCCAXqgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+ Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDU2MjNa
+ Fw0yMDA3MTMwMDU2MjNaMCsxKTAnBgNVBAMMIEdvb2dsZSBDcnlwdEF1dGhWYXVs
+ dCBJbnN0YW5jZSAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEimGC4KEks4JL
+ 83DNAAGCA5sKqqxqUPgV8gxgsijlMPL0TOtyJhJ2vSFIULEa0coVFbu+fAdxt3CV
+ DbzD0vWAmaOBiDCBhTAJBgNVHRMEAjAAMB0GA1UdDgQWBBTVeDueShnq1LCqkCFL
+ MAYtpxkCuDBJBgNVHSMEQjBAgBRE9RxHT7U3EP1vdjRzNYMrU7EseKEkpCIwIDEe
+ MBwGA1UEAwwVR29vZ2xlIENyeXB0QXV0aFZhdWx0ggIQATAOBgNVHQ8BAf8EBAMC
+ AwgwDQYJKoZIhvcNAQELBQADggIBADUZCjfBwBL9SSZDkMwE3n08schTBAgZCCrv
+ XOQVPGrfbFUcMv1mT4e8a5zxE98HsCS6K4HB40RtTXkmt6nuN+NyBrAmZJrBCqvG
+ IYtGsBLLEnojgWuSpQIBQeQy9it3RFdSR/1FIPssrWUB5KrtRvd+07+Mo7tI91jE
+ EunOrocu46g6p/OKSIZ7UmwZzczn2CJsrxuNPgqdlza3ytb+TTm536ZHnqaefSZD
+ rrruNneTXoqjC2OFn/OVLHQ1ee/vrHiX70P8p1V09cccDiwMCIZskNACYgWRdLYU
+ F5aYGueoFajrb4zmMoy8DK/4lh1EsfWMsrsQK6whmPidzgz37nb3rPpiHTdxu1Fc
+ 2XM9QV3Jfj2U5FMOZTcDha7W7kb++gSnQEPTM0+Zu6lTJmcZgK4RJ7lmIfK/eVbJ
+ 6V/wplOzXSxO3jBb2LNhLbhkUzcg68EIEPxaBpXYVOU2tSo2FMgi7/YLwQLorc6Q
+ h1pUZep8T8SLpvI02GvsB8TroFr2tCvCe5A1VxBQDx9IE7nEd2N30XxqReFk8Y82
+ xZMUOA4DL33NI45KWjhcawm0tzAPFfKjta/zYvnf7rwwE4r2PVuOXdet5eN8zBje
+ yJbEpjCemADujcwtYQ8hScyj/eCT2KNbZ9ibY2yrZEEuRQanq8CLAbpvSZYIHVhg
+ Clar7+38
+ </cert>
+ </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml
new file mode 100644
index 0000000..9c0adcd
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+ <certificate>
+ MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+ R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+ NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+ YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+ tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+ 4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+ tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+ HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+ GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+ UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+ 33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+ 7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+ hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+ 79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+ M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+ JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+ BuwwuQxvQDF4pmQd
+ </certificate>
+ <value>
+ VEVTVA==
+ </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml
new file mode 100644
index 0000000..f94b109
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+ <intermediates>
+ <cert>
+ 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
+ +a0fa5B3pxc8BN0LsHA0stT/Y2o=
+ </cert>
+ </intermediates>
+ <certificate>
+ MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+ Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa
+ Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs
+ dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+ AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX
+ TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9
+ f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ
+ EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf
+ lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE
+ hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU
+ 0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD
+ K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
+ hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds
+ E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn
+ AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n
+ N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE
+ tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2
+ gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb
+ RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz
+ 7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw
+ pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n
+ UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp
+ HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g
+ </certificate>
+ <value>
+ zELcMKbEb82mjWdhaV62Po4Tn/fEnHg7TMQzlz3cpDP3uzGKXg4fvCa+yDYrEYqm17uywOfFQpJs
+ pVjoUMINdnYogO44mul+E+m/klzSQN3GbmvYDtKpFsSGqsymyFSg8Bv2LeDLx2Pisc3sLUhxwKN4
+ 8B6MwfZ3qUnRa5/ySk0bzEYYiRWsYR5oY7vK0kAI5c4Oi77E0W440FaEhnT7WxucFUnAhZbOSwXA
+ apE39BXu6ZbAPpTSc4f+uMErF7cRGbIODcAQJko6yjliBmvnCxj0ct7qzwUpgJojy/G5DLaVx3MF
+ Xnee9peYCrKFWpB+Z/7io0/Fqs/fDF7U25BsXA==
+ </value>
+</signature>
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java
new file mode 100644
index 0000000..ac6d293
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.io.InputStream;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.w3c.dom.Element;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CertUtilsTest {
+
+ private static final String XML_STR = ""
+ + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<!-- comment 1 -->"
+ + "<root>\n\n\n\r\r\r"
+ + " <node1>\r\n\r\n"
+ + " node1-1</node1>"
+ + " <!-- comment 2 -->"
+ + " <node1>node1-2"
+ + " \n\r\n\r</node1>"
+ + " <node2>"
+ + " <node1> node2-node1-1</node1>"
+ + " <node1>node2-node1-2 </node1>"
+ + " <!-- comment 3 -->"
+ + " <node1> node2-node1-3 </node1>"
+ + " </node2>"
+ + "</root>";
+
+ private static final String SIGNED_STR = "abcdefg\n";
+ private static final String SIGNATURE_BASE64 = ""
+ + "KxBt9B3pwL3/59SrjTJTpuhc9JRxLOUNwNr3J4EEdXj5BqkYOUeXIOjyBGp8XaOnmuW8WmBxhko3"
+ + "yTR3/M9x0/pJuKDgqQSSFG+I56O/IAri7DmMBfY8QqcgiF8RaR86G7mWXUIdu8ixEtpKa//T4bN7"
+ + "c8Txvt96ApAcW0wJDihfCqDEXyi56pFCp+qEZuL4fS8iZtZTUkvxim1tb2/IsZ9OyDd9BWxp+JTs"
+ + "zihzH6xqnUCa1aELSUZnU8OzWGeuKpVDQDbDMtQpcxJ9o+6L6wO5vmQutZAulgw5gRPGhYWVs8+0"
+ + "ATdNEbv8TSomkXkZ3/lMYnmPXKmaHxcP4330DA==";
+ private static final PublicKey SIGNER_PUBLIC_KEY = TestData.INTERMEDIATE_CA_2.getPublicKey();
+
+ @Test
+ public void decodeCert_readPemFile_succeeds_singleBlock() throws Exception {
+ InputStream f = TestData.openTestFile("pem/valid-cert.pem");
+ X509Certificate cert = CertUtils.decodeCert(f);
+ assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
+ }
+
+ @Test
+ public void decodeCert_readPemFile_succeeds_multipleBlocks() throws Exception {
+ InputStream in = TestData.openTestFile("pem/valid-cert-multiple-blocks.pem");
+ X509Certificate cert = CertUtils.decodeCert(in);
+ assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
+ }
+
+ @Test
+ public void decodeCert_readPemFile_throwsIfNoBeginEndLines() throws Exception {
+ InputStream in = TestData.openTestFile("pem/invalid-cert-1-no-begin-end.pem");
+ assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+ }
+
+ @Test
+ public void decodeCert_readPemFile_throwsIfEmptyBlock() throws Exception {
+ InputStream in = TestData.openTestFile("pem/invalid-cert-2-empty-block.pem");
+ assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+ }
+
+ @Test
+ public void decodeCert_readPemFile_throwsIfInvalidCert() throws Exception {
+ InputStream in = TestData.openTestFile("pem/invalid-cert-3-invalid-key.pem");
+ assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+ }
+
+ @Test
+ public void decodeCert_readBytes_succeeds() throws Exception {
+ X509Certificate cert = CertUtils.decodeCert(TestData.INTERMEDIATE_CA_2.getEncoded());
+ assertThat(cert.getIssuerX500Principal().getName())
+ .isEqualTo("CN=Google CryptAuthVault Intermediate");
+ }
+
+ @Test
+ public void decodeCert_readBytes_throwsIfInvalidCert() throws Exception {
+ byte[] modifiedCertBytes = TestData.INTERMEDIATE_CA_1.getEncoded();
+ modifiedCertBytes[0] ^= (byte) 1;
+ assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(modifiedCertBytes));
+ }
+
+ @Test
+ public void decodeBase64_succeeds() throws Exception {
+ assertThat(CertUtils.decodeBase64("VEVTVA==")).isEqualTo("TEST".getBytes(UTF_8));
+ }
+
+ @Test
+ public void decodeBase64_succeedsIfEmptyInput() throws Exception {
+ assertThat(CertUtils.decodeBase64("")).hasLength(0);
+ }
+
+ @Test
+ public void decodeBase64_throwsIfInvalidInput() throws Exception {
+ assertThrows(CertParsingException.class, () -> CertUtils.decodeBase64("EVTVA=="));
+ }
+
+ @Test
+ public void getXmlRootNode_succeeds() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ assertThat(root.getTagName()).isEqualTo("root");
+ }
+
+ @Test
+ public void getXmlRootNode_throwsIfEmptyInput() throws Exception {
+ assertThrows(CertParsingException.class, () -> CertUtils.getXmlRootNode(new byte[0]));
+ }
+
+ @Test
+ public void getXmlNodeContents_singleLevel_succeeds() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ assertThat(CertUtils.getXmlNodeContents(CertUtils.MustExist.FALSE, root, "node1"))
+ .containsExactly("node1-1", "node1-2");
+ }
+
+ @Test
+ public void getXmlNodeContents_multipleLevels_succeeds() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ assertThat(CertUtils.getXmlNodeContents(CertUtils.MustExist.FALSE, root, "node2", "node1"))
+ .containsExactly("node2-node1-1", "node2-node1-2", "node2-node1-3");
+ }
+
+ @Test
+ public void getXmlNodeContents_mustExistFalse_succeedsIfNotExist() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ assertThat(
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.FALSE, root, "node2", "node-not-exist"))
+ .isEmpty();
+ }
+
+ @Test
+ public void getXmlNodeContents_mustExistAtLeastOne_throwsIfNotExist() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.AT_LEAST_ONE, root, "node2",
+ "node-not-exist"));
+ assertThat(expected.getMessage()).contains("must contain at least one");
+ }
+
+ @Test
+ public void getXmlNodeContents_mustExistExactlyOne_throwsIfNotExist() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.EXACTLY_ONE, root, "node-not-exist",
+ "node1"));
+ assertThat(expected.getMessage()).contains("must contain exactly one");
+ }
+
+ @Test
+ public void getXmlNodeContents_mustExistExactlyOne_throwsIfMultipleExist() throws Exception {
+ Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertUtils.getXmlNodeContents(
+ CertUtils.MustExist.EXACTLY_ONE, root, "node2", "node1"));
+ assertThat(expected.getMessage()).contains("must contain exactly one");
+ }
+
+ @Test
+ public void verifyRsaSha256Signature_succeeds() throws Exception {
+ CertUtils.verifyRsaSha256Signature(
+ SIGNER_PUBLIC_KEY,
+ Base64.getDecoder().decode(SIGNATURE_BASE64),
+ SIGNED_STR.getBytes(UTF_8));
+ }
+
+ @Test
+ public void verifyRsaSha256Signature_throwsIfMismatchSignature() throws Exception {
+ byte[] modifiedBytes = SIGNED_STR.getBytes(UTF_8);
+ modifiedBytes[0] ^= (byte) 1;
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ CertUtils.verifyRsaSha256Signature(
+ SIGNER_PUBLIC_KEY, Base64.getDecoder().decode(SIGNATURE_BASE64),
+ modifiedBytes));
+ }
+
+ @Test
+ public void verifyRsaSha256Signature_throwsIfWrongKeyType() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+ keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
+ PublicKey publicKey = keyPairGenerator.generateKeyPair().getPublic();
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ CertUtils.verifyRsaSha256Signature(
+ publicKey,
+ Base64.getDecoder().decode(SIGNATURE_BASE64),
+ SIGNED_STR.getBytes(UTF_8)));
+ }
+
+ @Test
+ public void buildCertPath_succeedsWithoutIntermediates() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+ X509Certificate leafCert = TestData.INTERMEDIATE_CA_1;
+ CertPath certPath = CertUtils.buildCertPath(
+ CertUtils.buildPkixParams(
+ TestData.DATE_ALL_CERTS_VALID, rootCert, Collections.emptyList(),
+ leafCert));
+ assertThat(certPath.getCertificates()).containsExactly(
+ TestData.INTERMEDIATE_CA_1).inOrder();
+ }
+
+ @Test
+ public void buildCertPath_succeedsWithIntermediates() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+ List<X509Certificate> intermediateCerts =
+ Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+ X509Certificate leafCert = TestData.LEAF_CERT_2;
+ CertPath certPath =
+ CertUtils.buildCertPath(
+ CertUtils.buildPkixParams(
+ TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+ leafCert));
+ assertThat(certPath.getCertificates())
+ .containsExactly(
+ TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
+ TestData.INTERMEDIATE_CA_1)
+ .inOrder();
+ }
+
+ @Test
+ public void buildCertPath_succeedsWithIntermediates_ignoreUnrelatedIntermedateCert()
+ throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+ List<X509Certificate> intermediateCerts =
+ Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+ X509Certificate leafCert = TestData.LEAF_CERT_1;
+ CertPath certPath =
+ CertUtils.buildCertPath(
+ CertUtils.buildPkixParams(
+ TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+ leafCert));
+ assertThat(certPath.getCertificates())
+ .containsExactly(TestData.LEAF_CERT_1, TestData.INTERMEDIATE_CA_1)
+ .inOrder();
+ }
+
+ @Test
+ public void buildCertPath_throwsIfWrongRootCommonName() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
+ List<X509Certificate> intermediateCerts =
+ Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+ X509Certificate leafCert = TestData.LEAF_CERT_1;
+
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ CertUtils.buildCertPath(
+ CertUtils.buildPkixParams(
+ TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+ leafCert)));
+ }
+
+ @Test
+ public void buildCertPath_throwsIfMissingIntermediateCert() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
+ List<X509Certificate> intermediateCerts = Collections.emptyList();
+ X509Certificate leafCert = TestData.LEAF_CERT_1;
+
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ CertUtils.buildCertPath(
+ CertUtils.buildPkixParams(
+ TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+ leafCert)));
+ }
+
+ @Test
+ public void validateCert_succeeds() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+ List<X509Certificate> intermediateCerts =
+ Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+ X509Certificate leafCert = TestData.LEAF_CERT_2;
+ CertUtils.validateCert(TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+ leafCert);
+ }
+
+ @Test
+ public void validateCert_throwsIfExpired() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+ List<X509Certificate> intermediateCerts =
+ Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+ X509Certificate leafCert = TestData.LEAF_CERT_2;
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ CertUtils.validateCert(
+ TestData.DATE_LEAF_CERT_2_EXPIRED, rootCert, intermediateCerts,
+ leafCert));
+ }
+
+ @Test
+ public void validateCert_throwsIfWrongRootWithTheSameCommonName() throws Exception {
+ X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_KEY;
+ List<X509Certificate> intermediateCerts =
+ Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+ X509Certificate leafCert = TestData.LEAF_CERT_2;
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ CertUtils.validateCert(
+ TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+ leafCert));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java
new file mode 100644
index 0000000..52269d9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CertXmlTest {
+
+ private byte[] certXmlBytes;
+
+ @Before
+ public void setUp() throws Exception {
+ certXmlBytes = TestData.readTestFile("xml/valid-cert-file.xml");
+ }
+
+ @Test
+ public void parse_succeeds() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ assertThat(certXml.getSerial()).isEqualTo(1000L);
+ assertThat(certXml.getRefreshInterval()).isEqualTo(2592000L);
+ }
+
+ @Test
+ public void parse_succeedsIfNoIntermediateCerts() throws Exception {
+ CertXml certXml =
+ CertXml.parse(TestData.readTestFile("xml/valid-cert-file-no-intermediates.xml"));
+ assertThat(certXml.getAllIntermediateCerts()).isEmpty();
+ }
+
+ @Test
+ public void parse_checkIntermediateCerts() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ List<X509Certificate> intermediateCerts = certXml.getAllIntermediateCerts();
+ assertThat(intermediateCerts)
+ .containsExactly(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2)
+ .inOrder();
+ }
+
+ @Test
+ public void parse_checkEndpointCerts() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ List<X509Certificate> endpointCerts = certXml.getAllEndpointCerts();
+ assertThat(endpointCerts).hasSize(3);
+ assertThat(endpointCerts).containsAllOf(TestData.LEAF_CERT_1, TestData.LEAF_CERT_2);
+ }
+
+ @Test
+ public void parse_throwsIfNoEndpointCert() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-cert-file-no-endpoint-cert.xml")));
+ assertThat(expected.getMessage()).contains("at least one");
+ }
+
+ @Test
+ public void parse_throwsIfNoRefreshInterval() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-cert-file-no-refresh-interval.xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+
+ @Test
+ public void parse_throwsIfNoSerial() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-cert-file-no-serial.xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+
+ @Test
+ public void parse_throwsIfTwoRefreshIntervals() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-cert-file-two-refresh-intervals"
+ + ".xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+
+ @Test
+ public void parse_throwsIfTwoSerials() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ CertXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-cert-file-two-serials.xml")));
+ assertThat(expected.getMessage()).contains("exactly one node");
+ }
+
+ @Test
+ public void parseAndValidateAllCerts_succeeds() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ for (int index = 0; index < certXml.getAllEndpointCerts().size(); index++) {
+ assertThat(
+ certXml.getEndpointCert(
+ index, TestData.DATE_ALL_CERTS_VALID, TestData.ROOT_CA_TRUSTED))
+ .isNotNull();
+ }
+ }
+
+ @Test
+ public void parseAndValidate_returnsExpectedCertPath() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ CertPath certPath =
+ certXml.getEndpointCert(
+ /*index=*/ 1, // TestData.LEAF_CERT_2
+ TestData.DATE_ALL_CERTS_VALID,
+ TestData.ROOT_CA_TRUSTED);
+ assertThat(certPath.getCertificates())
+ .containsExactly(
+ TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
+ TestData.INTERMEDIATE_CA_1)
+ .inOrder();
+ }
+
+ @Test
+ public void validateCert_throwsIfRootCertWithDifferentCommonName() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ certXml.getEndpointCert(
+ /*index=*/ 0, // TestData.LEAF_CERT_1
+ TestData.DATE_ALL_CERTS_VALID,
+ TestData.ROOT_CA_DIFFERENT_COMMON_NAME));
+ }
+
+ @Test
+ public void validateCert_throwsIfRootCertWithDifferentPublicKey() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ certXml.getEndpointCert(
+ /*index=*/ 0, // TestData.LEAF_CERT_1
+ TestData.DATE_ALL_CERTS_VALID,
+ TestData.ROOT_CA_DIFFERENT_KEY));
+ }
+
+ @Test
+ public void validateCert_throwsIfExpired() throws Exception {
+ CertXml certXml = CertXml.parse(certXmlBytes);
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ certXml.getEndpointCert(
+ /*index=*/ 1, // TestData.LEAF_CERT_2
+ TestData.DATE_LEAF_CERT_2_EXPIRED,
+ TestData.ROOT_CA_TRUSTED));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java
new file mode 100644
index 0000000..4d87006
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class SigXmlTest {
+
+ private byte[] certXmlBytes;
+ private byte[] sigXmlBytes;
+
+ @Before
+ public void setUp() throws Exception {
+ certXmlBytes = TestData.readTestFile("xml/valid-cert-file.xml");
+ sigXmlBytes = TestData.readTestFile("xml/valid-sig-file.sig.xml");
+ }
+
+ @Test
+ public void parseAndVerifyFileSignature_succeeds() throws Exception {
+ SigXml sigXml = SigXml.parse(sigXmlBytes);
+ sigXml.verifyFileSignature(
+ TestData.ROOT_CA_TRUSTED, certXmlBytes, TestData.DATE_ALL_CERTS_VALID);
+ }
+
+ @Test
+ public void parseAndVerifyFileSignature_throwsIfExpiredCert() throws Exception {
+ SigXml sigXml = SigXml.parse(sigXmlBytes);
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ sigXml.verifyFileSignature(
+ TestData.ROOT_CA_TRUSTED, certXmlBytes,
+ TestData.DATE_INTERMEDIATE_CA_2_EXPIRED));
+ }
+
+ @Test
+ public void parseAndVerifyFileSignature_throwsIfInvalidSignature() throws Exception {
+ SigXml sigXml = SigXml.parse(sigXmlBytes);
+ byte[] modifiedBytes = sigXmlBytes.clone();
+ modifiedBytes[0] ^= (byte) 1; // Flip one bit
+ CertValidationException expected =
+ expectThrows(
+ CertValidationException.class,
+ () ->
+ sigXml.verifyFileSignature(
+ TestData.ROOT_CA_TRUSTED, modifiedBytes,
+ TestData.DATE_ALL_CERTS_VALID));
+ assertThat(expected.getMessage()).contains("signature is invalid");
+ }
+
+ @Test
+ public void parseAndVerifyFileSignature_throwsIfRootCertWithWrongCommonName() throws Exception {
+ SigXml sigXml = SigXml.parse(sigXmlBytes);
+ assertThrows(
+ CertValidationException.class,
+ () ->
+ sigXml.verifyFileSignature(
+ TestData.ROOT_CA_DIFFERENT_COMMON_NAME,
+ certXmlBytes,
+ TestData.DATE_ALL_CERTS_VALID));
+ }
+
+ @Test
+ public void parse_succeedsWithoutIntermediateCerts() throws Exception {
+ SigXml.parse(TestData.readTestFile("xml/valid-sig-file-no-intermediates.sig.xml"));
+ }
+
+ @Test
+ public void parse_throwsIfNoSignerCert() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ SigXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-sig-file-no-signer-cert.sig.xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+
+ @Test
+ public void parse_throwsIfTwoSignerCerts() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ SigXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-sig-file-two-signer-certs.sig.xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+
+ @Test
+ public void parse_throwsIfNoSignatureValue() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ SigXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-sig-file-no-signature.sig.xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+
+ @Test
+ public void parse_throwsIfTwoSignatureValues() throws Exception {
+ CertParsingException expected =
+ expectThrows(
+ CertParsingException.class,
+ () ->
+ SigXml.parse(
+ TestData.readTestFile(
+ "xml/invalid-sig-file-two-signatures.sig.xml")));
+ assertThat(expected.getMessage()).contains("exactly one");
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java
new file mode 100644
index 0000000..5eb4166
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.certificate;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Date;
+
+/** Constants used by the tests in the folder. */
+public final class TestData {
+
+ private static final String TEST_FILE_FOLDER_NAME = "KeyStoreRecoveryControllerTest";
+
+ private TestData() {}
+
+ // Some test data that is generated by using OpenSSL command line tools.
+ private static final String ROOT_CA_TRUSTED_BASE64 = ""
+ + "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";
+ private static final String ROOT_CA_DIFFERENT_KEY_BASE64 = ""
+ + "MIIFJjCCAw6gAwIBAgIJANpazyIWdcb/MA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNV"
+ + "BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMjEwMzI0MjVaFw0zODAx"
+ + "MTYwMzI0MjVaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCAiIw"
+ + "DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKkAUqkRxdVy9UpI9BjQnylGVPRW"
+ + "aQsCwT2iWJ7fuCnQQon1U9nOyw2R5GYKcA8Zy+4Co++6nzRblYYXJG3Fzsj+kxei"
+ + "pZGmU11djRJDOhHPPe5jSW37Y0czWaj8jx4xvMim18dGYR7fg6SsOOYXA2y5tlvZ"
+ + "xLjvw5qpL62J5bVoAAxjng/Oc2Osu+vpv6M50pUZr0OEiFi59WlwrCCZpf1/80bT"
+ + "j2ebCKKAtTYa6+Q+oMGGxb3imSRpmQPtFcvhUPmAaocUjYM/FeIGNRv14oED/aXz"
+ + "khuPb+QNkXgk9yiokE10IeAk6oNUNDyuiMNIFy67lUrwc45lv9y0s/8fHj9pvKse"
+ + "n3+UOKAuV9atUXLdFKQwnQPt4SOmHPkXoj+5tv32RSvVeYhb0ZOpQPkRhxv4wgs7"
+ + "NldNbKhzVDM9K4M5Q2TrPK1yJKrc5/z0bDzmPOcH4AAXPvSt5PZOs0NlXUJ99BcA"
+ + "KE1sWArUhipz5mx0hxPTNEM9/8bMb//HkbZtx2log1/fc207W/AFd2FICOpRY+Sb"
+ + "CJs9WjstpisppotONvgXxZbZiypGKxpeZOb4s6y3iXtZ0FWXUZrc65b8S06fDVCa"
+ + "ZomNFDWhspHFKyueBgU6cR9K97cDo85Juy6RhnouXxi+XpXPdGFwPqVm1glFYos8"
+ + "5+Scbwwx69RihN1nAgMBAAGjYzBhMB0GA1UdDgQWBBRoosUAVtfHHeMl8/5x1m2m"
+ + "arEOoTAfBgNVHSMEGDAWgBRoosUAVtfHHeMl8/5x1m2marEOoTAPBgNVHRMBAf8E"
+ + "BTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAYbLVQlsi"
+ + "KdzCPsaybSrUtMrmkad8Gy+/QR3J16adxy/2WpxsZ1Su0YA4tFzOSWqt74C0mnUi"
+ + "i+3prW7nyEOwezs+NH0SreF4B7tO1FSj157LoHxR2WmCCwul8FfsMi0x6MvEf40v"
+ + "bMeLrRAA4ysRITua1INb05fzJpczoaW/Q1lBPXConvolnIMvYsLbCZX4/OhQQCLa"
+ + "mJx8mGrt1wcNp7Kvh+3JfuOXw+WGeCuB5sSWBsOUvhfN+8sgyk1Dtq5c7rVKKtqz"
+ + "gqHfCNZ+lYa6Vkii7plIcWYJXXa7DMmozX07mrDqdJZEocv6XCoZAKDlJorB8pQT"
+ + "im47RF60X/FCHSCK2cHbG5M+kF84xwj5fTLztLM1+RlJSJiGk6jhxJRQ+hl0Vkhp"
+ + "+u7UbUDwkUF/CJB3d1Gtfm+QtzFVKe27ClU5YFSKCXRV/K4KnkZqpyG8Py+PUtUf"
+ + "WRahp4hkWFkIoLeTnJwgAFRvp/KCtSW0/trI/vfInzqBk/qWIVhxYB8Qv9DXtKBX"
+ + "3AZ36HM2dxmjef/rpYRphuEN0ZwYdynsGy9dF0SihbR8curSg67sbtYfyw0xURhU"
+ + "Nk99YMy7T0EUYnaki/PIPK/gnJjZTX1FLCyHUR38fDJIWkGB4xr8pSrwmRoPSvNF"
+ + "dFr3YlFWwHWd1gXlwJtzeMSuVgoKVtZGmmk=";
+ private static final String ROOT_CA_DIFFERENT_COMMON_NAME_BASE64 = ""
+ + "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"
+ + "6OAzRvlk6CZWG3obkt3yaB1NhBEw8hiYvX9F";
+ private static final String INTERMEDIATE_CA_1_BASE64 = ""
+ + "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"
+ + "+a0fa5B3pxc8BN0LsHA0stT/Y2o=";
+ private static final String INTERMEDIATE_CA_2_BASE64 = ""
+ + "MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v"
+ + "Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa"
+ + "Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs"
+ + "dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC"
+ + "AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX"
+ + "TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9"
+ + "f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ"
+ + "EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf"
+ + "lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE"
+ + "hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU"
+ + "0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD"
+ + "K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI"
+ + "hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds"
+ + "E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn"
+ + "AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n"
+ + "N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE"
+ + "tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2"
+ + "gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb"
+ + "RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz"
+ + "7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw"
+ + "pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n"
+ + "UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp"
+ + "HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g";
+ private static final String LEAF_CERT_1_BASE64 = ""
+ + "MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi"
+ + "R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1"
+ + "NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW"
+ + "YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu"
+ + "tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl"
+ + "4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21"
+ + "tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu"
+ + "HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr"
+ + "GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb"
+ + "UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe"
+ + "33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5"
+ + "7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ"
+ + "hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa"
+ + "79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4"
+ + "M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf"
+ + "JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp"
+ + "BuwwuQxvQDF4pmQd";
+ private static final String LEAF_CERT_2_BASE64 = ""
+ + "MIICrDCCAZSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOjE4MDYGA1UEAwwvR29v"
+ + "Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZSBJbnRlcm1lZGlhdGUwHhcN"
+ + "MTgwMTEyMDEwMzA5WhcNMTkwMTEyMDEwMzA5WjArMSkwJwYDVQQDDCBHb29nbGUg"
+ + "Q3J5cHRBdXRoVmF1bHQgSW5zdGFuY2UgMjBZMBMGByqGSM49AgEGCCqGSM49AwEH"
+ + "A0IABGhmBQyWdjsXKJRbkW4iIrvt6iqhX5t2XGt/vZS9CoOl0fs+EvJXo4kgrnx8"
+ + "/8SGxz3pwRkFhY943QYy6a1gv/2jgZUwgZIwCQYDVR0TBAIwADAdBgNVHQ4EFgQU"
+ + "xFmLyxUS2JHKURBtewBKRP6kQBgwVgYDVR0jBE8wTYAU0dKv4xTaEmdwHyox3tY8"
+ + "H8XVDGKhMaQvMC0xKzApBgNVBAMMIkdvb2dsZSBDcnlwdEF1dGhWYXVsdCBJbnRl"
+ + "cm1lZGlhdGWCAhAAMA4GA1UdDwEB/wQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEA"
+ + "EJWpl7HU6LxukLqhw2tVZr7IRrKIucRk+RKaaiMx1Hx2jsTTskiJRiZas/xoPSqX"
+ + "z1K5DVgI486i7HyqnWkGH5xVzCsv+rya5FOSTS3jVtgtoA4HFEqeeAcDowPDqVw3"
+ + "yFTA55ukZnzVaPLpDfPqkhzWiuLQ/4fI6YCmOnWB8KtHTMdyGsDSAkpoxpok++NJ"
+ + "Lu79BoBLe2ucjN383lTlieLxmrmHjF9ryYSQczcm0v6irMOMxEovw5iT4LHiEhbm"
+ + "DfOPW909fe/s+K3TGZ3Q6U77x8g5k9dVovMgA4pFwtREtknFjeK1wXR3/eXGcP3W"
+ + "0bMX1yTWYJQFWCG3DFoC5w==";
+
+ /** The cert of the root CA. */
+ static final X509Certificate ROOT_CA_TRUSTED = decodeBase64Cert(ROOT_CA_TRUSTED_BASE64);
+ /** This root CA cert has a different Common Name than ROOT_CA_TRUSTED. */
+ static final X509Certificate ROOT_CA_DIFFERENT_COMMON_NAME =
+ decodeBase64Cert(ROOT_CA_DIFFERENT_COMMON_NAME_BASE64);
+ /** This root CA cert has the same CN as ROOT_CA_TRUSTED, but a different public key. */
+ static final X509Certificate ROOT_CA_DIFFERENT_KEY =
+ decodeBase64Cert(ROOT_CA_DIFFERENT_KEY_BASE64);
+ /** This intermediate CA cert is signed by the corresponding private key of ROOT_CA_TRUSTED. */
+ static final X509Certificate INTERMEDIATE_CA_1 = decodeBase64Cert(INTERMEDIATE_CA_1_BASE64);
+ /** This intermediate CA cert is signed by the private key of INTERMEDIATE_CA_1. */
+ static final X509Certificate INTERMEDIATE_CA_2 = decodeBase64Cert(INTERMEDIATE_CA_2_BASE64);
+ /** This leaf cert is signed by the corresponding private key of INTERMEDIATE_CA_1. */
+ static final X509Certificate LEAF_CERT_1 = decodeBase64Cert(LEAF_CERT_1_BASE64);
+ /** This leaf cert is signed by the corresponding private key of INTERMEDIATE_CA_2. */
+ static final X509Certificate LEAF_CERT_2 = decodeBase64Cert(LEAF_CERT_2_BASE64);
+
+ private static X509Certificate decodeBase64Cert(String str) {
+ try {
+ byte[] bytes = Base64.getDecoder().decode(str);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+
+ static final Date DATE_ALL_CERTS_VALID = new Date(1516406400000L); // Jan 20, 2018
+ static final Date DATE_LEAF_CERT_2_EXPIRED = new Date(1547254989001L); // Jan 12, 2019
+ static final Date DATE_INTERMEDIATE_CA_2_EXPIRED = new Date(1673397483001L); // Jan 11, 2023
+
+ static InputStream openTestFile(String relativePath) throws Exception {
+ Context context = InstrumentationRegistry.getContext();
+ return context.getResources().getAssets().open(TEST_FILE_FOLDER_NAME + "/" + relativePath);
+ }
+
+ static byte[] readTestFile(String relativePath) throws Exception {
+ InputStream in = openTestFile(relativePath);
+ return ByteStreams.toByteArray(in);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
index 396fef4..57dd808 100644
--- a/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
@@ -18,11 +18,9 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.graphics.Rect;
@@ -32,24 +30,17 @@
import android.view.SurfaceSession;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.stubbing.Answer;
/**
* Build/Install/Run:
* atest FrameworksServicesTests:com.android.server.wm.DimmerTests;
*/
@Presubmit
-@Ignore("b/72450130")
@RunWith(AndroidJUnit4.class)
public class DimmerTests extends WindowTestsBase {
- public DimmerTests() {
- super(spy(new SurfaceAnimationRunner()));
- }
-
private class TestWindowContainer extends WindowContainer<TestWindowContainer> {
final SurfaceControl mControl = mock(SurfaceControl.class);
final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
@@ -71,14 +62,11 @@
private class MockSurfaceBuildingContainer extends WindowContainer<TestWindowContainer> {
final SurfaceSession mSession = new SurfaceSession();
+ final SurfaceControl mHostControl = mock(SurfaceControl.class);
final SurfaceControl.Transaction mHostTransaction = mock(SurfaceControl.Transaction.class);
MockSurfaceBuildingContainer() {
super(sWm);
- mSurfaceControl = sWm.makeSurfaceBuilder(mSession)
- .setName("test surface")
- .setSize(1, 1)
- .build();
}
class MockSurfaceBuilder extends SurfaceControl.Builder {
@@ -88,34 +76,29 @@
@Override
public SurfaceControl build() {
- return spy(sWm.makeSurfaceBuilder(mSession)
- .setName("test surface")
- .setSize(1, 1)
- .build());
+ return mock(SurfaceControl.class);
}
}
@Override
- SurfaceControl.Builder makeSurface() {
- return sWm.makeSurfaceBuilder(mSession)
- .setName("test surface")
- .setSize(1, 1);
- }
-
- @Override
SurfaceControl.Builder makeChildSurface(WindowContainer child) {
return new MockSurfaceBuilder(mSession);
}
@Override
+ public SurfaceControl getSurfaceControl() {
+ return mHostControl;
+ }
+
+ @Override
public SurfaceControl.Transaction getPendingTransaction() {
return mHostTransaction;
}
}
- MockSurfaceBuildingContainer mHost;
- Dimmer mDimmer;
- SurfaceControl.Transaction mTransaction;
+ private MockSurfaceBuildingContainer mHost;
+ private Dimmer mDimmer;
+ private SurfaceControl.Transaction mTransaction;
@Before
public void setUp() throws Exception {
@@ -123,13 +106,9 @@
mHost = new MockSurfaceBuildingContainer();
mTransaction = mock(SurfaceControl.Transaction.class);
- mDimmer = new Dimmer(mHost);
-
- doAnswer((Answer<Void>) invocation -> {
- Runnable runnable = invocation.getArgument(3);
- runnable.run();
- return null;
- }).when(sWm.mSurfaceAnimationRunner).startAnimation(any(), any(), any(), any());
+ mDimmer = new Dimmer(mHost,
+ (surfaceAnimator, t, anim, hidden) -> surfaceAnimator.mAnimationFinishedCallback
+ .run());
}
@Test
@@ -137,7 +116,7 @@
final float alpha = 0.8f;
mDimmer.dimAbove(mTransaction, alpha);
- SurfaceControl dimLayer = getDimLayer(null);
+ SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
@@ -149,12 +128,12 @@
public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() throws Exception {
float alpha = 0.8f;
mDimmer.dimAbove(mTransaction, alpha);
- final SurfaceControl firstSurface = getDimLayer(null);
+ final SurfaceControl firstSurface = getDimLayer();
alpha = 0.9f;
mDimmer.dimAbove(mTransaction, alpha);
- assertEquals(firstSurface, getDimLayer(null));
+ assertEquals(firstSurface, getDimLayer());
verify(mTransaction).setAlpha(firstSurface, 0.9f);
}
@@ -167,18 +146,18 @@
Rect bounds = new Rect(0, 0, width, height);
mDimmer.updateDims(mTransaction, bounds);
- verify(mTransaction).setSize(getDimLayer(null), width, height);
- verify(mTransaction).show(getDimLayer(null));
+ verify(mTransaction).setSize(getDimLayer(), width, height);
+ verify(mTransaction).show(getDimLayer());
}
@Test
public void testDimAboveNoChildNotReset() throws Exception {
mDimmer.dimAbove(mTransaction, 0.8f);
- SurfaceControl dimLayer = getDimLayer(null);
+ SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
mDimmer.updateDims(mTransaction, new Rect());
- verify(mTransaction).show(getDimLayer(null));
+ verify(mTransaction).show(getDimLayer());
verify(dimLayer, never()).destroy();
}
@@ -189,12 +168,12 @@
final float alpha = 0.8f;
mDimmer.dimAbove(mTransaction, child, alpha);
- SurfaceControl mDimLayer = getDimLayer(child);
+ SurfaceControl dimLayer = getDimLayer();
- assertNotNull("Dimmer should have created a surface", mDimLayer);
+ assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(mDimLayer, alpha);
- verify(mTransaction).setRelativeLayer(mDimLayer, child.mControl, 1);
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
}
@Test
@@ -204,12 +183,12 @@
final float alpha = 0.8f;
mDimmer.dimBelow(mTransaction, child, alpha);
- SurfaceControl mDimLayer = getDimLayer(child);
+ SurfaceControl dimLayer = getDimLayer();
- assertNotNull("Dimmer should have created a surface", mDimLayer);
+ assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(mDimLayer, alpha);
- verify(mTransaction).setRelativeLayer(mDimLayer, child.mControl, -1);
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
}
@Test
@@ -219,7 +198,7 @@
final float alpha = 0.8f;
mDimmer.dimAbove(mTransaction, child, alpha);
- SurfaceControl dimLayer = getDimLayer(child);
+ SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
mDimmer.updateDims(mTransaction, new Rect());
@@ -233,7 +212,7 @@
final float alpha = 0.8f;
mDimmer.dimAbove(mTransaction, child, alpha);
- SurfaceControl dimLayer = getDimLayer(child);
+ SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
mDimmer.dimAbove(mTransaction, child, alpha);
@@ -242,7 +221,29 @@
verify(dimLayer, never()).destroy();
}
- private SurfaceControl getDimLayer(WindowContainer windowContainer) {
- return mDimmer.mDimLayerUsers.get(windowContainer).mDimLayer;
+ @Test
+ public void testDimUpdateWhileDimming() throws Exception {
+ Rect bounds = new Rect();
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+
+ SurfaceControl dimLayer = getDimLayer();
+ bounds.set(0, 0, 10, 10);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction, times(1)).show(dimLayer);
+ verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 0, 0);
+
+ bounds.set(10, 10, 30, 30);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 10, 10);
+ }
+
+ private SurfaceControl getDimLayer() {
+ return mDimmer.mDimState.mDimLayer;
}
}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 81fd889..000cf38 100644
--- a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -69,11 +69,6 @@
private Runnable mRunnableWhenAddingSplashScreen;
static synchronized WindowManagerService getWindowManagerService(Context context) {
- return getWindowManagerService(context, new SurfaceAnimationRunner());
- }
-
- static synchronized WindowManagerService getWindowManagerService(Context context,
- SurfaceAnimationRunner surfaceAnimationRunner) {
if (sWm == null) {
// We only want to do this once for the test process as we don't want WM to try to
// register a bunch of local services again.
@@ -111,7 +106,7 @@
}
sWm = WindowManagerService.main(context, ims, true, false,
- false, new TestWindowManagerPolicy(), surfaceAnimationRunner);
+ false, new TestWindowManagerPolicy());
}
return sWm;
}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
index 7918901..69b1378 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -84,16 +84,6 @@
WindowState mChildAppWindowBelow;
HashSet<WindowState> mCommonWindows;
- private final SurfaceAnimationRunner mSurfaceAnimationRunner;
-
- public WindowTestsBase() {
- this(new SurfaceAnimationRunner());
- }
-
- public WindowTestsBase(SurfaceAnimationRunner surfaceAnimationRunner) {
- mSurfaceAnimationRunner = surfaceAnimationRunner;
- }
-
@Before
public void setUp() throws Exception {
if (!sOneTimeSetupDone) {
@@ -108,7 +98,7 @@
final Context context = InstrumentationRegistry.getTargetContext();
AttributeCache.init(context);
- sWm = TestWindowManagerPolicy.getWindowManagerService(context, mSurfaceAnimationRunner);
+ sWm = TestWindowManagerPolicy.getWindowManagerService(context);
beforeCreateDisplay();
context.getDisplay().getDisplayInfo(mDisplayInfo);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 36a2a95..3b0fd1f 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -654,7 +654,7 @@
public boolean isAppInactive(String packageName, int userId) {
try {
userId = ActivityManager.getService().handleIncomingUser(Binder.getCallingPid(),
- Binder.getCallingUid(), userId, false, true, "isAppInactive", null);
+ Binder.getCallingUid(), userId, false, false, "isAppInactive", null);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 63f970a..e0b0bbf 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -787,6 +787,10 @@
builder.append(isLong ? " PROPERTY_HAS_CDMA_VOICE_PRIVACY" : " priv");
}
+ if (can(properties, PROPERTY_IS_RTT)) {
+ builder.append(isLong ? " PROPERTY_IS_RTT" : " rtt");
+ }
+
builder.append("]");
return builder.toString();
}
@@ -2646,6 +2650,7 @@
* side of the coll.
*/
public final void sendRttSessionRemotelyTerminated() {
+ unsetRttProperty();
mListeners.forEach((l) -> l.onRttSessionRemotelyTerminated(Connection.this));
}
diff --git a/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java b/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
index 1e8382f..e8e4dc2 100644
--- a/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
@@ -59,10 +59,10 @@
*/
@Test
public void testChannelFinalize() throws Exception {
- WifiP2pManager.Channel channel = new WifiP2pManager.Channel(mContextMock,
- mTestLooper.getLooper(), null, null, mDut);
-
- leakageDetectorRule.assertUnreleasedResourceCount(channel, 1);
+ try (WifiP2pManager.Channel channel = new WifiP2pManager.Channel(mContextMock,
+ mTestLooper.getLooper(), null, null, mDut)) {
+ leakageDetectorRule.assertUnreleasedResourceCount(channel, 1);
+ }
}
/**