Merge "Removed Parcelable from FieldClassification."
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index e2fd82d..21e203b 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -87,7 +87,6 @@
 import android.util.TypedValue;
 import android.util.apk.ApkSignatureSchemeV2Verifier;
 import android.util.apk.ApkSignatureVerifier;
-import android.util.apk.SignatureNotFoundException;
 import android.view.Gravity;
 
 import com.android.internal.R;
@@ -1561,41 +1560,35 @@
 
         boolean systemDir = (parseFlags & PARSE_IS_SYSTEM_DIR) != 0;
         int minSignatureScheme = ApkSignatureVerifier.VERSION_JAR_SIGNATURE_SCHEME;
-        if ((parseFlags & PARSE_IS_EPHEMERAL) != 0 || pkg.applicationInfo.isStaticSharedLibrary()) {
+        if (pkg.applicationInfo.isStaticSharedLibrary()) {
             // must use v2 signing scheme
             minSignatureScheme = ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2;
         }
-        try {
-            ApkSignatureVerifier.Result verified =
-                    ApkSignatureVerifier.verify(apkPath, minSignatureScheme, systemDir);
-            if (pkg.mCertificates == null) {
-                pkg.mCertificates = verified.certs;
-                pkg.mSignatures = verified.sigs;
-                pkg.mSigningKeys = new ArraySet<>(verified.certs.length);
-                for (int i = 0; i < verified.certs.length; i++) {
-                    Certificate[] signerCerts = verified.certs[i];
-                    Certificate signerCert = signerCerts[0];
-                    pkg.mSigningKeys.add(signerCert.getPublicKey());
-                }
-            } else {
-                if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) {
-                    throw new PackageParserException(
-                            INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
-                            apkPath + " has mismatched certificates");
-                }
-            }
-        } catch (SignatureNotFoundException e) {
+        ApkSignatureVerifier.Result verified =
+                ApkSignatureVerifier.verify(apkPath, minSignatureScheme, systemDir);
+        if (verified.signatureSchemeVersion
+                < ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2) {
+            // TODO (b/68860689): move this logic to packagemanagerserivce
             if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
                 throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
-                        "No APK Signature Scheme v2 signature in ephemeral package " + apkPath,
-                        e);
+                        "No APK Signature Scheme v2 signature in ephemeral package " + apkPath);
             }
-            if (pkg.applicationInfo.isStaticSharedLibrary()) {
-                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
-                        "Static shared libs must use v2 signature scheme " + apkPath);
+        }
+        if (pkg.mCertificates == null) {
+            pkg.mCertificates = verified.certs;
+            pkg.mSignatures = verified.sigs;
+            pkg.mSigningKeys = new ArraySet<>(verified.certs.length);
+            for (int i = 0; i < verified.certs.length; i++) {
+                Certificate[] signerCerts = verified.certs[i];
+                Certificate signerCert = signerCerts[0];
+                pkg.mSigningKeys.add(signerCert.getPublicKey());
             }
-            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
-                    "No APK Signature Scheme v2 signature in package " + apkPath, e);
+        } else {
+            if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) {
+                throw new PackageParserException(
+                        INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+                        apkPath + " has mismatched certificates");
+            }
         }
     }
 
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index 61b0eb0..8623524 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -36,15 +36,26 @@
 import java.util.List;
 
 /**
- * The ShortcutManager manages an app's <em>shortcuts</em>. Shortcuts provide users with quick
- * access to activities other than an app's main activity in the currently-active launcher, provided
- * that the launcher supports app shortcuts.  For example, an email app may publish the "compose new
- * email" action, which will directly open the compose activity.  The {@link ShortcutInfo} class
- * contains information about each of the shortcuts themselves.
+ * The ShortcutManager performs operations on an app's set of <em>shortcuts</em>. The
+ * {@link ShortcutInfo} class contains information about each of the shortcuts themselves.
+ *
+ * <p>An app's shortcuts represent specific tasks and actions that users can take within your app.
+ * When a user selects a shortcut in the currently-active launcher, your app opens an activity other
+ * than the app's starting activity, provided that the currently-active launcher supports app
+ * shortcuts.</p>
+ *
+ * <p>The types of shortcuts that you create for your app depend on the app's key use cases. For
+ * example, an email app may publish the "compose new email" shortcut, which allows the app to
+ * directly open the compose activity.</p>
+ *
+ * <p class="note"><b>Note:</b> Only main activities&mdash;activities that handle the
+ * {@link Intent#ACTION_MAIN} action and the {@link Intent#CATEGORY_LAUNCHER} category&mdash;can
+ * have shortcuts. If an app has multiple main activities, you need to define the set of shortcuts
+ * for <em>each</em> activity.
  *
  * <p>This page discusses the implementation details of the <code>ShortcutManager</code> class. For
- * guidance on performing operations on app shortcuts within your app, see the
- * <a href="/guide/topics/ui/shortcuts.html">App Shortcuts</a> feature guide.
+ * definitions of key terms and guidance on performing operations on shortcuts within your app, see
+ * the <a href="/guide/topics/ui/shortcuts.html">App Shortcuts</a> feature guide.
  *
  * <h3>Shortcut characteristics</h3>
  *
@@ -69,8 +80,8 @@
  * <ul>
  *     <li>The user removes it.
  *     <li>The publisher app associated with the shortcut is uninstalled.
- *     <li>The user performs the clear data action on the publisher app from the device's
- *     <b>Settings</b> app.
+ *     <li>The user selects <b>Clear data</b> from the publisher app's <i>Storage</i> screen, within
+ *     the system's <b>Settings</b> app.
  * </ul>
  *
  * <p>Because the system performs
@@ -84,12 +95,15 @@
  * <p>When the launcher displays an app's shortcuts, they should appear in the following order:
  *
  * <ul>
- *   <li>Static shortcuts (if {@link ShortcutInfo#isDeclaredInManifest()} is {@code true}),
- *   and then show dynamic shortcuts (if {@link ShortcutInfo#isDynamic()} is {@code true}).
- *   <li>Within each shortcut type (static and dynamic), sort the shortcuts in order of increasing
+ *   <li>Static shortcuts&mdash;shortcuts whose {@link ShortcutInfo#isDeclaredInManifest()} method
+ *   returns {@code true}&mdash;followed by dynamic shortcuts&mdash;shortcuts whose
+ *   {@link ShortcutInfo#isDynamic()} method returns {@code true}.
+ *   <li>Within each shortcut type (static and dynamic), shortcuts are sorted in order of increasing
  *   rank according to {@link ShortcutInfo#getRank()}.
  * </ul>
  *
+ * <h4>Shortcut ranks</h4>
+ *
  * <p>Shortcut ranks are non-negative, sequential integers that determine the order in which
  * shortcuts appear, assuming that the shortcuts are all in the same category. You can update ranks
  * of existing shortcuts when you call {@link #updateShortcuts(List)},
@@ -103,64 +117,99 @@
  *
  * <h3>Options for static shortcuts</h3>
  *
- * The following list includes descriptions for the different attributes within a static shortcut:
+ * The following list includes descriptions for the different attributes within a static shortcut.
+ * You must provide a value for {@code android:shortcutId}, {@code android:shortcutShortLabel}; all
+ * other values are optional.
+ *
  * <dl>
  *   <dt>{@code android:shortcutId}</dt>
- *   <dd>Mandatory shortcut ID.
- *   <p>
- *   This must be a string literal.
- *   A resource string, such as <code>@string/foo</code>, cannot be used.
+ *   <dd><p>A string literal, which represents the shortcut when a {@code ShortcutManager} object
+ *   performs operations on it.</p>
+ *   <p class="note"><b>Note: </b>You cannot set this attribute's value to a resource string, such
+ *   as <code>@string/foo</code>.</p>
  *   </dd>
  *
  *   <dt>{@code android:enabled}</dt>
- *   <dd>Default is {@code true}.  Can be set to {@code false} in order
- *   to disable a static shortcut that was published in a previous version and set a custom
- *   disabled message.  If a custom disabled message is not needed, then a static shortcut can
- *   be simply removed from the XML file rather than keeping it with {@code enabled="false"}.</dd>
+ *   <dd><p>Whether the user can interact with the shortcut from a supported launcher.</p>
+ *   <p>The default value is {@code true}. If you set it to {@code false}, you should also set
+ *   {@code android:shortcutDisabledMessage} to a message that explains why you've disabled the
+ *   shortcut. If you don't think you need to provide such a message, it's easiest to just remove
+ *   the shortcut from the XML file entirely, rather than changing the values of its
+ *   {@code android:enabled} and {@code android:shortcutDisabledMessage} attributes.
+ *   </dd>
  *
  *   <dt>{@code android:icon}</dt>
- *   <dd>Shortcut icon.</dd>
+ *   <dd><p>The <a href="/topic/performance/graphics/index.html">bitmap</a> or
+ *   <a href="/guide/practices/ui_guidelines/icon_design_adaptive.html">adaptive icon</a> that the
+ *   launcher uses when displaying the shortcut to the user. This value can be either the path to an
+ *   image or the resource file that contains the image. Use adaptive icons whenever possible to
+ *   improve performance and consistency.</p>
+ *   <p class="note"><b>Note: </b>Shortcut icons cannot include
+ *   <a href="/training/material/drawables.html#DrawableTint">tints</a>.
+ *   </dd>
  *
  *   <dt>{@code android:shortcutShortLabel}</dt>
- *   <dd>Mandatory shortcut short label.
- *   See {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.
- *   <p>
- *   This must be a resource string, such as <code>@string/shortcut_label</code>.
+ *   <dd><p>A concise phrase that describes the shortcut's purpose. For more information, see
+ *   {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.</p>
+ *   <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ *   <code>@string/shortcut_label</code>.</p>
  *   </dd>
  *
  *   <dt>{@code android:shortcutLongLabel}</dt>
- *   <dd>Shortcut long label.
- *   See {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.
- *   <p>
- *   This must be a resource string, such as <code>@string/shortcut_long_label</code>.
+ *   <dd><p>An extended phrase that describes the shortcut's purpose. If there's enough space, the
+ *   launcher displays this value instead of {@code android:shortcutShortLabel}. For more
+ *   information, see {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.</p>
+ *   <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ *   <code>@string/shortcut_long_label</code>.</p>
  *   </dd>
  *
  *   <dt>{@code android:shortcutDisabledMessage}</dt>
- *   <dd>When {@code android:enabled} is set to
- *   {@code false}, this attribute is used to display a custom disabled message.
- *   <p>
- *   This must be a resource string, such as <code>@string/shortcut_disabled_message</code>.
+ *   <dd><p>The message that appears in a supported launcher when the user attempts to launch a
+ *   disabled shortcut. This attribute's value has no effect if {@code android:enabled} is
+ *   {@code true}. The message should explain to the user why the shortcut is now disabled.</p>
+ *   <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ *   <code>@string/shortcut_disabled_message</code>.</p>
  *   </dd>
+ * </dl>
  *
+ * <h3>Inner elements that define static shortcuts</h3>
+ *
+ * <p>The XML file that lists an app's static shortcuts supports the following elements inside each
+ * {@code &lt;shortcut&gt;} element. You must include an {@code intent} inner element for each
+ * static shortcut that you define.</p>
+ *
+ * <dl>
  *   <dt>{@code intent}</dt>
- *   <dd>Intent to launch when the user selects the shortcut.
- *   {@code android:action} is mandatory.
- *   See <a href="{@docRoot}guide/topics/ui/settings.html#Intents">Using intents</a> for the
- *   other supported tags.
+ *   <dd><p>The action that the system launches when the user selects the shortcut. This intent must
+ *   provide a value for the {@code android:action} attribute.</p>
  *   <p>You can provide multiple intents for a single shortcut so that the last defined activity is
  *   launched with the other activities in the
  *   <a href="/guide/components/tasks-and-back-stack.html">back stack</a>. See
- *   {@link android.app.TaskStackBuilder} for details.
- *   <p><b>Note:</b> String resources may not be used within an {@code <intent>} element.
+ *   <a href="/guide/topics/ui/shortcuts.html#static">Using Static Shortcuts</a> and the
+ *   {@link android.app.TaskStackBuilder} class reference for details.</p>
+ *   <p class="note"><b>Note:</b> This {@code intent} element cannot include string resources.</p>
+ *   <p>For more information, see
+ *   <a href="{@docRoot}guide/topics/ui/settings.html#Intents">Using intents</a>.</p>
  *   </dd>
+ *
  *   <dt>{@code categories}</dt>
- *   <dd>Specify shortcut categories.  Currently only
- *   {@link ShortcutInfo#SHORTCUT_CATEGORY_CONVERSATION} is defined in the framework.
+ *   <dd><p>Provides a grouping for the types of actions that your app's shortcuts perform, such as
+ *   creating new chat messages.</p>
+ *   <p>For a list of supported shortcut categories, see the {@link ShortcutInfo} class reference
+ *   for a list of supported shortcut categories.
  *   </dd>
  * </dl>
  *
  * <h3>Updating shortcuts</h3>
  *
+ * <p>Each app's launcher icon can contain at most {@link #getMaxShortcutCountPerActivity()} number
+ * of static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts
+ * that an app can create, though.
+ *
+ * <p>When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut,
+ * the pinned shortcut is still visible and launchable.  This allows an app to have more than
+ * {@link #getMaxShortcutCountPerActivity()} number of shortcuts.
+ *
  * <p>As an example, suppose {@link #getMaxShortcutCountPerActivity()} is 5:
  * <ol>
  *     <li>A chat app publishes 5 dynamic shortcuts for the 5 most recent
@@ -168,18 +217,13 @@
  *
  *     <li>The user pins all 5 of the shortcuts.
  *
- *     <li>Later, the user has started 3 additional conversations (c6, c7, and c8),
- *     so the publisher app
- *     re-publishes its dynamic shortcuts.  The new dynamic shortcut list is:
- *     c4, c5, ..., c8.
- *     The publisher app has to remove c1, c2, and c3 because it can't have more than
- *     5 dynamic shortcuts.
- *
- *     <li>However, even though c1, c2, and c3 are no longer dynamic shortcuts, the pinned
- *     shortcuts for these conversations are still available and launchable.
- *
- *     <li>At this point, the user can access a total of 8 shortcuts that link to activities in
- *     the publisher app, including the 3 pinned shortcuts, even though an app can have at most 5
+ *     <li>Later, the user has started 3 additional conversations (c6, c7, and c8), so the publisher
+ *     app re-publishes its dynamic shortcuts. The new dynamic shortcut list is: c4, c5, ..., c8.
+ *     <p>The publisher app has to remove c1, c2, and c3 because it can't have more than 5 dynamic
+ *     shortcuts. However, c1, c2, and c3 are still pinned shortcuts that the user can access and
+ *     launch.
+ *     <p>At this point, the user can access a total of 8 shortcuts that link to activities in the
+ *     publisher app, including the 3 pinned shortcuts, even though an app can have at most 5
  *     dynamic shortcuts.
  *
  *     <li>The app can use {@link #updateShortcuts(List)} to update <em>any</em> of the existing
@@ -196,44 +240,23 @@
  * Dynamic shortcuts can be published with any set of {@link Intent#addFlags Intent} flags.
  * Typically, {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} is specified, possibly along with other
  * flags; otherwise, if the app is already running, the app is simply brought to
- * the foreground, and the target activity may not appear.
+ * the foreground, and the target activity might not appear.
  *
  * <p>Static shortcuts <b>cannot</b> have custom intent flags.
  * The first intent of a static shortcut will always have {@link Intent#FLAG_ACTIVITY_NEW_TASK}
  * and {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} set. This means, when the app is already running, all
- * the existing activities in your app will be destroyed when a static shortcut is launched.
+ * the existing activities in your app are destroyed when a static shortcut is launched.
  * If this behavior is not desirable, you can use a <em>trampoline activity</em>, or an invisible
  * activity that starts another activity in {@link Activity#onCreate}, then calls
  * {@link Activity#finish()}:
  * <ol>
  *     <li>In the <code>AndroidManifest.xml</code> file, the trampoline activity should include the
  *     attribute assignment {@code android:taskAffinity=""}.
- *     <li>In the shortcuts resource file, the intent within the static shortcut should point at
+ *     <li>In the shortcuts resource file, the intent within the static shortcut should reference
  *     the trampoline activity.
  * </ol>
  *
- * <h3>Handling system locale changes</h3>
- *
- * <p>Apps should update dynamic and pinned shortcuts when the system locale changes using the
- * {@link Intent#ACTION_LOCALE_CHANGED} broadcast. When the system locale changes,
- * <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate limiting</a> is reset, so even
- * background apps can add and update dynamic shortcuts until the rate limit is reached again.
- *
- * <h3>Shortcut limits</h3>
- *
- * <p>Only main activities&mdash;activities that handle the {@code MAIN} action and the
- * {@code LAUNCHER} category&mdash;can have shortcuts. If an app has multiple main activities, you
- * need to define the set of shortcuts for <em>each</em> activity.
- *
- * <p>Each launcher icon can have at most {@link #getMaxShortcutCountPerActivity()} number of
- * static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts that
- * an app can create.
- *
- * <p>When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut,
- * the pinned shortcut is still visible and launchable.  This allows an app to have more than
- * {@link #getMaxShortcutCountPerActivity()} number of shortcuts.
- *
- * <h4>Rate limiting</h4>
+ * <h3>Rate limiting</h3>
  *
  * <p>When <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate limiting</a> is active,
  * {@link #isRateLimitingActive()} returns {@code true}.
@@ -243,8 +266,20 @@
  * <ul>
  *   <li>An app comes to the foreground.
  *   <li>The system locale changes.
- *   <li>The user performs the <strong>inline reply</strong> action on a notification.
+ *   <li>The user performs the <a href="/guide/topics/ui/notifiers/notifications.html#direct">inline
+ *   reply</a> action on a notification.
  * </ul>
+ *
+ * <h3>Handling system locale changes</h3>
+ *
+ * <p>Apps should update dynamic and pinned shortcuts when they receive the
+ * {@link Intent#ACTION_LOCALE_CHANGED} broadcast, indicating that the system locale has changed.
+ * <p>When the system locale changes, <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate
+ * limiting</a> is reset, so even background apps can add and update dynamic shortcuts until the
+ * rate limit is reached again.
+ *
+ * <h3>Retrieving class instances</h3>
+ * <!-- Provides a heading for the content filled in by the @SystemService annotation below -->
  */
 @SystemService(Context.SHORTCUT_SERVICE)
 public class ShortcutManager {
diff --git a/core/java/android/util/apk/ApkSignatureVerifier.java b/core/java/android/util/apk/ApkSignatureVerifier.java
index 73a9478..75c1000 100644
--- a/core/java/android/util/apk/ApkSignatureVerifier.java
+++ b/core/java/android/util/apk/ApkSignatureVerifier.java
@@ -65,18 +65,14 @@
      *                  v2 stripping rollback protection, or verify integrity of the APK.
      *
      * @throws PackageParserException if the APK's signature failed to verify.
-     * @throws SignatureNotFoundException if a signature corresponding to minLevel or greater
-     * is not found, except in the case of no JAR signature.
      */
     public static Result verify(String apkPath, int minSignatureSchemeVersion, boolean systemDir)
-            throws PackageParserException, SignatureNotFoundException {
-        boolean verified = false;
-        Certificate[][] signerCerts;
-        int level = VERSION_APK_SIGNATURE_SCHEME_V2;
+            throws PackageParserException {
 
         // first try v2
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
         try {
+            Certificate[][] signerCerts;
             signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
             Signature[] signerSigs = convertToSignatures(signerCerts);
 
@@ -93,12 +89,12 @@
             } finally {
                 closeQuietly(jarFile);
             }
-            return new Result(signerCerts, signerSigs);
+            return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
         } catch (SignatureNotFoundException e) {
             // not signed with v2, try older if allowed
             if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
-                throw new SignatureNotFoundException(
-                        "No APK Signature Scheme v2 signature found for " + apkPath, e);
+                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+                        "No APK Signature Scheme v2 signature in package " + apkPath, e);
             }
         } catch (PackageParserException e) {
             // preserve any new exceptions explicitly thrown here
@@ -178,7 +174,7 @@
                     }
                 }
             }
-            return new Result(lastCerts, lastSigs);
+            return new Result(lastCerts, lastSigs, VERSION_JAR_SIGNATURE_SCHEME);
         } catch (GeneralSecurityException e) {
             throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                     "Failed to collect certificates from " + apkPath, e);
@@ -254,10 +250,12 @@
     public static class Result {
         public final Certificate[][] certs;
         public final Signature[] sigs;
+        public final int signatureSchemeVersion;
 
-        public Result(Certificate[][] certs, Signature[] sigs) {
+        public Result(Certificate[][] certs, Signature[] sigs, int signingVersion) {
             this.certs = certs;
             this.sigs = sigs;
+            this.signatureSchemeVersion = signingVersion;
         }
     }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 3f8da093..2c82ac4 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -327,6 +327,8 @@
     // This is used to reduce the race between window focus changes being dispatched from
     // the window manager and input events coming through the input system.
     @GuardedBy("this")
+    boolean mWindowFocusChanged;
+    @GuardedBy("this")
     boolean mUpcomingWindowFocus;
     @GuardedBy("this")
     boolean mUpcomingInTouchMode;
@@ -2472,14 +2474,14 @@
         final boolean hasWindowFocus;
         final boolean inTouchMode;
         synchronized (this) {
+            if (!mWindowFocusChanged) {
+                return;
+            }
+            mWindowFocusChanged = false;
             hasWindowFocus = mUpcomingWindowFocus;
             inTouchMode = mUpcomingInTouchMode;
         }
 
-        if (mAttachInfo.mHasWindowFocus == hasWindowFocus) {
-            return;
-        }
-
         if (mAdded) {
             profileRendering(hasWindowFocus);
 
@@ -7181,6 +7183,7 @@
 
     public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
         synchronized (this) {
+            mWindowFocusChanged = true;
             mUpcomingWindowFocus = hasFocus;
             mUpcomingInTouchMode = inTouchMode;
         }
diff --git a/core/res/res/interpolator/aggressive_ease.xml b/core/res/res/interpolator/aggressive_ease.xml
new file mode 100644
index 0000000..620424f
--- /dev/null
+++ b/core/res/res/interpolator/aggressive_ease.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0.2"
+    android:controlY1="0"
+    android:controlX2="0"
+    android:controlY2="1"/>
\ No newline at end of file
diff --git a/core/res/res/interpolator/emphasized_deceleration.xml b/core/res/res/interpolator/emphasized_deceleration.xml
new file mode 100644
index 0000000..60c315c
--- /dev/null
+++ b/core/res/res/interpolator/emphasized_deceleration.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0.1"
+    android:controlY1="0.8"
+    android:controlX2="0.2"
+    android:controlY2="1"/>
\ No newline at end of file
diff --git a/core/res/res/interpolator/exaggerated_ease.xml b/core/res/res/interpolator/exaggerated_ease.xml
new file mode 100644
index 0000000..4961c1c
--- /dev/null
+++ b/core/res/res/interpolator/exaggerated_ease.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.08, 0.166666, 0.4 C 0.225, 0.94, 0.25, 1, 1, 1"/>
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index fa2499f..5c73d54 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -21,6 +21,9 @@
 import android.app.Application;
 import android.app.usage.StorageStats;
 import android.app.usage.StorageStatsManager;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -52,11 +55,6 @@
 
 import com.android.internal.R;
 import com.android.internal.util.ArrayUtils;
-import com.android.settingslib.core.lifecycle.Lifecycle;
-import com.android.settingslib.core.lifecycle.LifecycleObserver;
-import com.android.settingslib.core.lifecycle.events.OnDestroy;
-import com.android.settingslib.core.lifecycle.events.OnPause;
-import com.android.settingslib.core.lifecycle.events.OnResume;
 
 import java.io.File;
 import java.io.IOException;
@@ -595,7 +593,7 @@
                 .replaceAll("").toLowerCase();
     }
 
-    public class Session implements LifecycleObserver, OnPause, OnResume, OnDestroy {
+    public class Session implements LifecycleObserver {
         final Callbacks mCallbacks;
         boolean mResumed;
 
@@ -621,6 +619,7 @@
             }
         }
 
+        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
         public void onResume() {
             if (DEBUG_LOCKING) Log.v(TAG, "resume about to acquire lock...");
             synchronized (mEntriesMap) {
@@ -633,6 +632,7 @@
             if (DEBUG_LOCKING) Log.v(TAG, "...resume releasing lock");
         }
 
+        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
         public void onPause() {
             if (DEBUG_LOCKING) Log.v(TAG, "pause about to acquire lock...");
             synchronized (mEntriesMap) {
@@ -752,6 +752,7 @@
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
         }
 
+        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
         public void onDestroy() {
             if (!mHasLifecycle) {
                 // TODO: Legacy, remove this later once all usages are switched to Lifecycle
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
new file mode 100644
index 0000000..2b6d09f
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
@@ -0,0 +1,108 @@
+/*
+ * 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.settingslib.fuelgauge;
+
+import android.os.IDeviceIdleController;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import android.util.Log;
+
+/**
+ * Handles getting/changing the whitelist for the exceptions to battery saving features.
+ */
+public class PowerWhitelistBackend {
+
+    private static final String TAG = "PowerWhitelistBackend";
+
+    private static final String DEVICE_IDLE_SERVICE = "deviceidle";
+
+    private static PowerWhitelistBackend sInstance;
+
+    private final IDeviceIdleController mDeviceIdleService;
+    private final ArraySet<String> mWhitelistedApps = new ArraySet<>();
+    private final ArraySet<String> mSysWhitelistedApps = new ArraySet<>();
+
+    public PowerWhitelistBackend() {
+        mDeviceIdleService = IDeviceIdleController.Stub.asInterface(
+                ServiceManager.getService(DEVICE_IDLE_SERVICE));
+        refreshList();
+    }
+
+    @VisibleForTesting
+    PowerWhitelistBackend(IDeviceIdleController deviceIdleService) {
+        mDeviceIdleService = deviceIdleService;
+        refreshList();
+    }
+
+    public int getWhitelistSize() {
+        return mWhitelistedApps.size();
+    }
+
+    public boolean isSysWhitelisted(String pkg) {
+        return mSysWhitelistedApps.contains(pkg);
+    }
+
+    public boolean isWhitelisted(String pkg) {
+        return mWhitelistedApps.contains(pkg);
+    }
+
+    public void addApp(String pkg) {
+        try {
+            mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
+            mWhitelistedApps.add(pkg);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+        }
+    }
+
+    public void removeApp(String pkg) {
+        try {
+            mDeviceIdleService.removePowerSaveWhitelistApp(pkg);
+            mWhitelistedApps.remove(pkg);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+        }
+    }
+
+    @VisibleForTesting
+    public void refreshList() {
+        mSysWhitelistedApps.clear();
+        mWhitelistedApps.clear();
+        try {
+            String[] whitelistedApps = mDeviceIdleService.getFullPowerWhitelist();
+            for (String app : whitelistedApps) {
+                mWhitelistedApps.add(app);
+            }
+            String[] sysWhitelistedApps = mDeviceIdleService.getSystemPowerWhitelist();
+            for (String app : sysWhitelistedApps) {
+                mSysWhitelistedApps.add(app);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+        }
+    }
+
+    public static PowerWhitelistBackend getInstance() {
+        if (sInstance == null) {
+            sInstance = new PowerWhitelistBackend();
+        }
+        return sInstance;
+    }
+
+}
diff --git a/packages/SettingsLib/tests/robotests/Android.mk b/packages/SettingsLib/tests/robotests/Android.mk
index 2738027..02a4973 100644
--- a/packages/SettingsLib/tests/robotests/Android.mk
+++ b/packages/SettingsLib/tests/robotests/Android.mk
@@ -49,7 +49,7 @@
 
 LOCAL_JAVA_LIBRARIES := \
     junit \
-    platform-robolectric-3.4.2-prebuilt
+    platform-robolectric-3.5.1-prebuilt
 
 LOCAL_INSTRUMENTATION_FOR := SettingsLibShell
 LOCAL_MODULE := SettingsLibRoboTests
@@ -74,4 +74,4 @@
 
 LOCAL_ROBOTEST_TIMEOUT := 36000
 
-include prebuilts/misc/common/robolectric/3.4.2/run_robotests.mk
+include prebuilts/misc/common/robolectric/3.5.1/run_robotests.mk
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java
index 698e442..df850be 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java
@@ -38,32 +38,25 @@
         final String resDir = appRoot + "/tests/robotests/res";
         final String assetsDir = appRoot + config.assetDir();
 
-        final AndroidManifest manifest = new AndroidManifest(Fs.fileFromPath(manifestPath),
-                Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)) {
+        return new AndroidManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir),
+            Fs.fileFromPath(assetsDir), "com.android.settingslib") {
             @Override
             public List<ResourcePath> getIncludedResourcePaths() {
                 List<ResourcePath> paths = super.getIncludedResourcePaths();
-                SettingsLibRobolectricTestRunner.getIncludedResourcePaths(getPackageName(), paths);
+                paths.add(new ResourcePath(
+                    null,
+                    Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"),
+                    null));
+                paths.add(new ResourcePath(
+                    null,
+                    Fs.fileFromPath("./frameworks/base/core/res/res"),
+                    null));
+                paths.add(new ResourcePath(
+                    null,
+                    Fs.fileFromPath("./frameworks/support/v7/appcompat/res"),
+                    null));
                 return paths;
             }
         };
-        manifest.setPackageName("com.android.settingslib");
-        return manifest;
     }
-
-    static void getIncludedResourcePaths(String packageName, List<ResourcePath> paths) {
-        paths.add(new ResourcePath(
-                null,
-                Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"),
-                null));
-        paths.add(new ResourcePath(
-                null,
-                Fs.fileFromPath("./frameworks/base/core/res/res"),
-                null));
-        paths.add(new ResourcePath(
-                null,
-                Fs.fileFromPath("./frameworks/support/v7/appcompat/res"),
-                null));
-    }
-
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerWhitelistBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerWhitelistBackendTest.java
new file mode 100644
index 0000000..fc0019d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerWhitelistBackendTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.settingslib.fuelgauge;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.os.IDeviceIdleController;
+
+import com.android.settingslib.SettingsLibRobolectricTestRunner;
+import com.android.settingslib.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsLibRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class PowerWhitelistBackendTest {
+
+    private static final String PACKAGE_ONE = "com.example.packageone";
+    private static final String PACKAGE_TWO = "com.example.packagetwo";
+
+    private PowerWhitelistBackend mPowerWhitelistBackend;
+    @Mock
+    private IDeviceIdleController mDeviceIdleService;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(new String[] {}).when(mDeviceIdleService).getFullPowerWhitelist();
+        doReturn(new String[] {}).when(mDeviceIdleService).getSystemPowerWhitelist();
+        doNothing().when(mDeviceIdleService).addPowerSaveWhitelistApp(anyString());
+        doNothing().when(mDeviceIdleService).removePowerSaveWhitelistApp(anyString());
+        mPowerWhitelistBackend = new PowerWhitelistBackend(mDeviceIdleService);
+    }
+
+    @Test
+    public void testIsWhitelisted() throws Exception {
+        doReturn(new String[] {PACKAGE_ONE}).when(mDeviceIdleService).getFullPowerWhitelist();
+        mPowerWhitelistBackend.refreshList();
+
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_ONE)).isTrue();
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_TWO)).isFalse();
+
+        mPowerWhitelistBackend.addApp(PACKAGE_TWO);
+
+        verify(mDeviceIdleService, atLeastOnce()).addPowerSaveWhitelistApp(PACKAGE_TWO);
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_ONE)).isTrue();
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_TWO)).isTrue();
+
+        mPowerWhitelistBackend.removeApp(PACKAGE_TWO);
+
+        verify(mDeviceIdleService, atLeastOnce()).removePowerSaveWhitelistApp(PACKAGE_TWO);
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_ONE)).isTrue();
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_TWO)).isFalse();
+
+        mPowerWhitelistBackend.removeApp(PACKAGE_ONE);
+
+        verify(mDeviceIdleService, atLeastOnce()).removePowerSaveWhitelistApp(PACKAGE_ONE);
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_ONE)).isFalse();
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_TWO)).isFalse();
+    }
+
+    @Test
+    public void testIsSystemWhitelisted() throws Exception {
+        doReturn(new String[] {PACKAGE_ONE}).when(mDeviceIdleService).getSystemPowerWhitelist();
+        mPowerWhitelistBackend.refreshList();
+
+        assertThat(mPowerWhitelistBackend.isSysWhitelisted(PACKAGE_ONE)).isTrue();
+        assertThat(mPowerWhitelistBackend.isSysWhitelisted(PACKAGE_TWO)).isFalse();
+        assertThat(mPowerWhitelistBackend.isWhitelisted(PACKAGE_ONE)).isFalse();
+
+    }
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index efa0bf8..2d7a6ad 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3884,7 +3884,8 @@
         IsInCall = telecomManager.isInCall();
         Binder.restoreCallingIdentity(ident);
 
-        return (IsInCall || getMode() == AudioManager.MODE_IN_COMMUNICATION);
+        return (IsInCall || getMode() == AudioManager.MODE_IN_COMMUNICATION ||
+                getMode() == AudioManager.MODE_IN_CALL);
     }
 
     /**
diff --git a/services/core/java/com/android/server/location/ContextHubService.java b/services/core/java/com/android/server/location/ContextHubService.java
index 1ad0cf9..b5fc9d0 100644
--- a/services/core/java/com/android/server/location/ContextHubService.java
+++ b/services/core/java/com/android/server/location/ContextHubService.java
@@ -269,13 +269,9 @@
         checkPermissions();
         int[] returnArray = new int[mContextHubInfo.length];
 
-        Log.d(TAG, "System supports " + returnArray.length + " hubs");
         for (int i = 0; i < returnArray.length; ++i) {
             returnArray[i] = i;
-            Log.d(TAG, String.format("Hub %s is mapped to %d",
-                    mContextHubInfo[i].getName(), returnArray[i]));
         }
-
         return returnArray;
     }
 
@@ -425,12 +421,6 @@
         for (int i = 0; i < foundInstances.size(); i++) {
             retArray[i] = foundInstances.get(i).intValue();
         }
-
-        if (retArray.length == 0) {
-            Log.d(TAG, "No nanoapps found on hub ID " + hubHandle + " using NanoAppFilter: "
-                    + filter);
-        }
-
         return retArray;
     }
 
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.java
deleted file mode 100644
index 38f5b45..0000000
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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;
-
-import android.security.keystore.AndroidKeyStoreSecretKey;
-
-/**
- * Private key stored in AndroidKeyStore. Used to wrap recoverable keys before writing them to disk.
- *
- * <p>Identified by a generation ID, which increments whenever a new platform key is generated. A
- * new key must be generated whenever the user disables their lock screen, as the decryption key is
- * tied to lock-screen authentication.
- *
- * <p>One current platform key exists per profile on the device. (As each must be tied to a
- * different user's lock screen.)
- *
- * @hide
- */
-public class PlatformEncryptionKey {
-
-    private final int mGenerationId;
-    private final AndroidKeyStoreSecretKey mKey;
-
-    /**
-     * A new instance.
-     *
-     * @param generationId The generation ID of the key.
-     * @param key The secret key handle. Can be used to encrypt WITHOUT requiring screen unlock.
-     */
-    public PlatformEncryptionKey(int generationId, AndroidKeyStoreSecretKey key) {
-        mGenerationId = generationId;
-        mKey = key;
-    }
-
-    /**
-     * Returns the generation ID of the key.
-     */
-    public int getGenerationId() {
-        return mGenerationId;
-    }
-
-    /**
-     * Returns the actual key, which can only be used to encrypt.
-     */
-    public AndroidKeyStoreSecretKey getKey() {
-        return mKey;
-    }
-}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
index b22ba4e..40c7889 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
@@ -16,6 +16,7 @@
 
 package com.android.server.locksettings.recoverablekeystore;
 
+import android.security.keystore.AndroidKeyStoreSecretKey;
 import android.security.keystore.KeyProperties;
 import android.security.keystore.KeyProtection;
 import android.util.Log;
@@ -55,7 +56,7 @@
      * @hide
      */
     public static RecoverableKeyGenerator newInstance(
-            PlatformEncryptionKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
+            AndroidKeyStoreSecretKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
             throws NoSuchAlgorithmException {
         // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
         // material, so that it can be synced to disk in encrypted form.
@@ -65,11 +66,11 @@
 
     private final KeyGenerator mKeyGenerator;
     private final RecoverableKeyStorage mRecoverableKeyStorage;
-    private final PlatformEncryptionKey mPlatformKey;
+    private final AndroidKeyStoreSecretKey mPlatformKey;
 
     private RecoverableKeyGenerator(
             KeyGenerator keyGenerator,
-            PlatformEncryptionKey platformKey,
+            AndroidKeyStoreSecretKey platformKey,
             RecoverableKeyStorage recoverableKeyStorage) {
         mKeyGenerator = keyGenerator;
         mRecoverableKeyStorage = recoverableKeyStorage;
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
index a0e34c3..f18e796 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
@@ -44,7 +44,6 @@
     private static final String APPLICATION_KEY_ALGORITHM = "AES";
     private static final int GCM_TAG_LENGTH_BITS = 128;
 
-    private final int mPlatformKeyGenerationId;
     private final byte[] mNonce;
     private final byte[] mKeyMaterial;
 
@@ -56,8 +55,8 @@
      *     {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
      *     not expose its key material.
      */
-    public static WrappedKey fromSecretKey(PlatformEncryptionKey wrappingKey, SecretKey key)
-            throws InvalidKeyException, KeyStoreException {
+    public static WrappedKey fromSecretKey(
+            SecretKey wrappingKey, SecretKey key) throws InvalidKeyException, KeyStoreException {
         if (key.getEncoded() == null) {
             throw new InvalidKeyException(
                     "key does not expose encoded material. It cannot be wrapped.");
@@ -71,7 +70,7 @@
                     "Android does not support AES/GCM/NoPadding. This should never happen.");
         }
 
-        cipher.init(Cipher.WRAP_MODE, wrappingKey.getKey());
+        cipher.init(Cipher.WRAP_MODE, wrappingKey);
         byte[] encryptedKeyMaterial;
         try {
             encryptedKeyMaterial = cipher.wrap(key);
@@ -91,10 +90,7 @@
             }
         }
 
-        return new WrappedKey(
-                /*nonce=*/ cipher.getIV(),
-                /*keyMaterial=*/ encryptedKeyMaterial,
-                /*platformKeyGenerationId=*/ wrappingKey.getGenerationId());
+        return new WrappedKey(/*mNonce=*/ cipher.getIV(), /*mKeyMaterial=*/ encryptedKeyMaterial);
     }
 
     /**
@@ -102,14 +98,12 @@
      *
      * @param nonce The nonce with which the key material was encrypted.
      * @param keyMaterial The encrypted bytes of the key material.
-     * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
      *
      * @hide
      */
-    public WrappedKey(byte[] nonce, byte[] keyMaterial, int platformKeyGenerationId) {
+    public WrappedKey(byte[] nonce, byte[] keyMaterial) {
         mNonce = nonce;
         mKeyMaterial = keyMaterial;
-        mPlatformKeyGenerationId = platformKeyGenerationId;
     }
 
     /**
@@ -130,13 +124,15 @@
         return mKeyMaterial;
     }
 
+
     /**
      * Returns the generation ID of the platform key, with which this key was wrapped.
      *
      * @hide
      */
     public int getPlatformKeyGenerationId() {
-        return mPlatformKeyGenerationId;
+        // TODO(robertberry) Implement. See ag/3362855.
+        return 1;
     }
 
     /**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
new file mode 100644
index 0000000..79bf5aa
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -0,0 +1,185 @@
+/*
+ * 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.storage;
+
+import android.annotation.Nullable;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Database of recoverable key information.
+ *
+ * @hide
+ */
+public class RecoverableKeyStoreDb {
+    private static final String TAG = "RecoverableKeyStoreDb";
+    private static final int IDLE_TIMEOUT_SECONDS = 30;
+
+    private final RecoverableKeyStoreDbHelper mKeyStoreDbHelper;
+
+    /**
+     * A new instance, storing the database in the user directory of {@code context}.
+     *
+     * @hide
+     */
+    public static RecoverableKeyStoreDb newInstance(Context context) {
+        RecoverableKeyStoreDbHelper helper = new RecoverableKeyStoreDbHelper(context);
+        helper.setWriteAheadLoggingEnabled(true);
+        helper.setIdleConnectionTimeout(IDLE_TIMEOUT_SECONDS);
+        return new RecoverableKeyStoreDb(helper);
+    }
+
+    private RecoverableKeyStoreDb(RecoverableKeyStoreDbHelper keyStoreDbHelper) {
+        this.mKeyStoreDbHelper = keyStoreDbHelper;
+    }
+
+    /**
+     * Inserts a key into the database.
+     *
+     * @param uid Uid of the application to whom the key belongs.
+     * @param alias The alias of the key in the AndroidKeyStore.
+     * @param wrappedKey The wrapped bytes of the key.
+     * @param generationId The generation ID of the platform key that wrapped the key.
+     * @return The primary key of the inserted row, or -1 if failed.
+     *
+     * @hide
+     */
+    public long insertKey(int uid, String alias, WrappedKey wrappedKey, int generationId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        values.put(KeysEntry.COLUMN_NAME_UID, uid);
+        values.put(KeysEntry.COLUMN_NAME_ALIAS, alias);
+        values.put(KeysEntry.COLUMN_NAME_NONCE, wrappedKey.getNonce());
+        values.put(KeysEntry.COLUMN_NAME_WRAPPED_KEY, wrappedKey.getKeyMaterial());
+        values.put(KeysEntry.COLUMN_NAME_LAST_SYNCED_AT, -1);
+        values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, generationId);
+        return db.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
+    }
+
+    /**
+     * Gets the key with {@code alias} for the app with {@code uid}.
+     *
+     * @hide
+     */
+    @Nullable public WrappedKey getKey(int uid, String alias) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+        String[] projection = {
+                KeysEntry._ID,
+                KeysEntry.COLUMN_NAME_NONCE,
+                KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+                KeysEntry.COLUMN_NAME_GENERATION_ID};
+        String selection =
+                KeysEntry.COLUMN_NAME_UID + " = ? AND "
+                + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
+        String[] selectionArguments = { Integer.toString(uid), alias };
+
+        try (
+            Cursor cursor = db.query(
+                KeysEntry.TABLE_NAME,
+                projection,
+                selection,
+                selectionArguments,
+                /*groupBy=*/ null,
+                /*having=*/ null,
+                /*orderBy=*/ null)
+        ) {
+            int count = cursor.getCount();
+            if (count == 0) {
+                return null;
+            }
+            if (count > 1) {
+                Log.wtf(TAG,
+                        String.format(Locale.US,
+                                "%d WrappedKey entries found for uid=%d alias='%s'. "
+                                        + "Should only ever be 0 or 1.", count, uid, alias));
+                return null;
+            }
+            cursor.moveToFirst();
+            byte[] nonce = cursor.getBlob(
+                    cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
+            byte[] keyMaterial = cursor.getBlob(
+                    cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
+            return new WrappedKey(nonce, keyMaterial);
+        }
+    }
+
+    /**
+     * Returns all keys for the given {@code uid} and {@code platformKeyGenerationId}.
+     *
+     * @param uid User id of the profile to which all the keys are associated.
+     * @param platformKeyGenerationId The generation ID of the platform key that wrapped these keys.
+     *     (i.e., this should be the most recent generation ID, as older platform keys are not
+     *     usable.)
+     *
+     * @hide
+     */
+    public Map<String, WrappedKey> getAllKeys(int uid, int platformKeyGenerationId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+        String[] projection = {
+                KeysEntry._ID,
+                KeysEntry.COLUMN_NAME_NONCE,
+                KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+                KeysEntry.COLUMN_NAME_ALIAS};
+        String selection =
+                KeysEntry.COLUMN_NAME_UID + " = ? AND "
+                + KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
+        String[] selectionArguments = {
+                Integer.toString(uid), Integer.toString(platformKeyGenerationId) };
+
+        try (
+            Cursor cursor = db.query(
+                KeysEntry.TABLE_NAME,
+                projection,
+                selection,
+                selectionArguments,
+                /*groupBy=*/ null,
+                /*having=*/ null,
+                /*orderBy=*/ null)
+        ) {
+            HashMap<String, WrappedKey> keys = new HashMap<>();
+            while (cursor.moveToNext()) {
+                byte[] nonce = cursor.getBlob(
+                        cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
+                byte[] keyMaterial = cursor.getBlob(
+                        cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
+                String alias = cursor.getString(
+                        cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
+                keys.put(alias, new WrappedKey(nonce, keyMaterial));
+            }
+            return keys;
+        }
+    }
+
+    /**
+     * Closes all open connections to the database.
+     */
+    public void close() {
+        mKeyStoreDbHelper.close();
+    }
+
+    // TODO: Add method for updating the 'last synced' time.
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
new file mode 100644
index 0000000..c54d0a6
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
@@ -0,0 +1,61 @@
+/*
+ * 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.storage;
+
+import android.provider.BaseColumns;
+
+/**
+ * Contract for recoverable key database. Describes the tables present.
+ */
+class RecoverableKeyStoreDbContract {
+    /**
+     * Table holding wrapped keys, and information about when they were last synced.
+     */
+    static class KeysEntry implements BaseColumns {
+        static final String TABLE_NAME = "keys";
+
+        /**
+         * The uid of the application that generated the key.
+         */
+        static final String COLUMN_NAME_UID = "uid";
+
+        /**
+         * The alias of the key, as set in AndroidKeyStore.
+         */
+        static final String COLUMN_NAME_ALIAS = "alias";
+
+        /**
+         * Nonce with which the key was encrypted.
+         */
+        static final String COLUMN_NAME_NONCE = "nonce";
+
+        /**
+         * Encrypted bytes of the key.
+         */
+        static final String COLUMN_NAME_WRAPPED_KEY = "wrapped_key";
+
+        /**
+         * Generation ID of the platform key that was used to encrypt this key.
+         */
+        static final String COLUMN_NAME_GENERATION_ID = "platform_key_generation_id";
+
+        /**
+         * Timestamp of when this key was last synced with remote storage, or -1 if never synced.
+         */
+        static final String COLUMN_NAME_LAST_SYNCED_AT = "last_synced_at";
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
new file mode 100644
index 0000000..e3783c4
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
@@ -0,0 +1,43 @@
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+
+/**
+ * Helper for creating the recoverable key database.
+ */
+class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
+    private static final int DATABASE_VERSION = 1;
+    private static final String DATABASE_NAME = "recoverablekeystore.db";
+
+    private static final String SQL_CREATE_ENTRIES =
+            "CREATE TABLE " + KeysEntry.TABLE_NAME + "( "
+                    + KeysEntry._ID + " INTEGER PRIMARY KEY,"
+                    + KeysEntry.COLUMN_NAME_UID + " INTEGER UNIQUE,"
+                    + KeysEntry.COLUMN_NAME_ALIAS + " TEXT UNIQUE,"
+                    + KeysEntry.COLUMN_NAME_NONCE + " BLOB,"
+                    + KeysEntry.COLUMN_NAME_WRAPPED_KEY + " BLOB,"
+                    + KeysEntry.COLUMN_NAME_GENERATION_ID + " INTEGER,"
+                    + KeysEntry.COLUMN_NAME_LAST_SYNCED_AT + " INTEGER)";
+
+    private static final String SQL_DELETE_ENTRIES =
+            "DROP TABLE IF EXISTS " + KeysEntry.TABLE_NAME;
+
+    RecoverableKeyStoreDbHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(SQL_CREATE_ENTRIES);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL(SQL_DELETE_ENTRIES);
+        onCreate(db);
+    }
+}
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 0b089fb..384efdd 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -21,6 +21,9 @@
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -96,21 +99,23 @@
     private final ArrayMap<IBinder, ClientRecord> mAllClientRecords =
             new ArrayMap<IBinder, ClientRecord>();
     private int mCurrentUserId = -1;
-    private boolean mGlobalBluetoothA2dpOn = false;
     private final IAudioService mAudioService;
     private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
     private final Handler mHandler = new Handler();
-    private final AudioRoutesInfo mAudioRoutesInfo = new AudioRoutesInfo();
     private final IntArray mActivePlayerMinPriorityQueue = new IntArray();
     private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray();
 
+    private final BroadcastReceiver mReceiver = new MediaRouterServiceBroadcastReceiver();
+    BluetoothDevice mBluetoothDevice;
+    int mAudioRouteMainType = AudioRoutesInfo.MAIN_SPEAKER;
+    boolean mGlobalBluetoothA2dpOn = false;
+
     public MediaRouterService(Context context) {
         mContext = context;
         Watchdog.getInstance().addMonitor(this);
 
         mAudioService = IAudioService.Stub.asInterface(
                 ServiceManager.getService(Context.AUDIO_SERVICE));
-
         mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
         mAudioPlayerStateMonitor.registerListener(
                 new AudioPlayerStateMonitor.OnAudioPlayerActiveStateChangedListener() {
@@ -170,44 +175,30 @@
                 @Override
                 public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
                     synchronized (mLock) {
-                        if (newRoutes.mainType != mAudioRoutesInfo.mainType) {
+                        if (newRoutes.mainType != mAudioRouteMainType) {
                             if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET
                                     | AudioRoutesInfo.MAIN_HEADPHONES
                                     | AudioRoutesInfo.MAIN_USB)) == 0) {
                                 // headset was plugged out.
-                                mGlobalBluetoothA2dpOn = newRoutes.bluetoothName != null;
+                                mGlobalBluetoothA2dpOn = mBluetoothDevice != null;
                             } else {
                                 // headset was plugged in.
                                 mGlobalBluetoothA2dpOn = false;
                             }
-                            mAudioRoutesInfo.mainType = newRoutes.mainType;
+                            mAudioRouteMainType = newRoutes.mainType;
                         }
-                        if (!TextUtils.equals(
-                                newRoutes.bluetoothName, mAudioRoutesInfo.bluetoothName)) {
-                            if (newRoutes.bluetoothName == null) {
-                                // BT was disconnected.
-                                mGlobalBluetoothA2dpOn = false;
-                            } else {
-                                // BT was connected or changed.
-                                mGlobalBluetoothA2dpOn = true;
-                            }
-                            mAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName;
-                        }
-                        // Although a Bluetooth device is connected before a new audio playback is
-                        // started, dispatchAudioRoutChanged() can be called after
-                        // onAudioPlayerActiveStateChanged(). That causes restoreBluetoothA2dp()
-                        // is called before mGlobalBluetoothA2dpOn is updated.
-                        // Calling restoreBluetoothA2dp() here could prevent that.
-                        restoreBluetoothA2dp();
+                        // The new audio routes info could be delivered with several seconds delay.
+                        // In order to avoid such delay, Bluetooth device info will be updated
+                        // via MediaRouterServiceBroadcastReceiver.
                     }
                 }
             });
         } catch (RemoteException e) {
             Slog.w(TAG, "RemoteException in the audio service.");
         }
-        synchronized (mLock) {
-            mGlobalBluetoothA2dpOn = (audioRoutes != null && audioRoutes.bluetoothName != null);
-        }
+
+        IntentFilter intentFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+        context.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null);
     }
 
     public void systemRunning() {
@@ -415,14 +406,12 @@
 
     void restoreBluetoothA2dp() {
         try {
-            boolean btConnected = false;
             boolean a2dpOn = false;
             synchronized (mLock) {
-                btConnected = mAudioRoutesInfo.bluetoothName != null;
                 a2dpOn = mGlobalBluetoothA2dpOn;
             }
             // We don't need to change a2dp status when bluetooth is not connected.
-            if (btConnected) {
+            if (mBluetoothDevice != null) {
                 Slog.v(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")");
                 mAudioService.setBluetoothA2dpOn(a2dpOn);
             }
@@ -661,6 +650,25 @@
         return false;
     }
 
+    final class MediaRouterServiceBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
+                int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
+                        BluetoothProfile.STATE_DISCONNECTED);
+                if (state == BluetoothProfile.STATE_DISCONNECTED) {
+                    mGlobalBluetoothA2dpOn = false;
+                    mBluetoothDevice = null;
+                } else if (state == BluetoothProfile.STATE_CONNECTED) {
+                    mGlobalBluetoothA2dpOn = true;
+                    mBluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                    // To ensure that BT A2DP is on, call restoreBluetoothA2dp().
+                    restoreBluetoothA2dp();
+                }
+            }
+        }
+    }
+
     /**
      * Information about a particular client of the media router.
      * The contents of this object is guarded by mLock.
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
index c13c779..298a988 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
@@ -48,24 +48,24 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class RecoverableKeyGeneratorTest {
-    private static final int TEST_GENERATION_ID = 3;
     private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
     private static final String KEY_ALGORITHM = "AES";
     private static final String TEST_ALIAS = "karlin";
     private static final String WRAPPING_KEY_ALIAS = "RecoverableKeyGeneratorTestWrappingKey";
 
-    @Mock RecoverableKeyStorage mRecoverableKeyStorage;
+    @Mock
+    RecoverableKeyStorage mRecoverableKeyStorage;
 
     @Captor ArgumentCaptor<KeyProtection> mKeyProtectionArgumentCaptor;
 
-    private PlatformEncryptionKey mPlatformKey;
+    private AndroidKeyStoreSecretKey mPlatformKey;
     private SecretKey mKeyHandle;
     private RecoverableKeyGenerator mRecoverableKeyGenerator;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mPlatformKey = new PlatformEncryptionKey(TEST_GENERATION_ID, generateAndroidKeyStoreKey());
+        mPlatformKey = generateAndroidKeyStoreKey();
         mKeyHandle = generateKey();
         mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(
                 mPlatformKey, mRecoverableKeyStorage);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java
index c056160..8371fe5 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java
@@ -61,8 +61,7 @@
 
     @Test
     public void fromSecretKey_createsWrappedKeyThatCanBeUnwrapped() throws Exception {
-        PlatformEncryptionKey wrappingKey = new PlatformEncryptionKey(
-                GENERATION_ID, generateAndroidKeyStoreKey());
+        SecretKey wrappingKey = generateAndroidKeyStoreKey();
         SecretKey rawKey = generateKey();
 
         WrappedKey wrappedKey = WrappedKey.fromSecretKey(wrappingKey, rawKey);
@@ -70,7 +69,7 @@
         Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
         cipher.init(
                 Cipher.UNWRAP_MODE,
-                wrappingKey.getKey(),
+                wrappingKey,
                 new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
         SecretKey unwrappedKey = (SecretKey) cipher.unwrap(
                 wrappedKey.getKeyMaterial(), KEY_ALGORITHM, Cipher.SECRET_KEY);
@@ -78,28 +77,15 @@
     }
 
     @Test
-    public void fromSecretKey_returnsAKeyWithTheGenerationIdOfTheWrappingKey() throws Exception {
-        PlatformEncryptionKey wrappingKey = new PlatformEncryptionKey(
-                GENERATION_ID, generateAndroidKeyStoreKey());
-        SecretKey rawKey = generateKey();
-
-        WrappedKey wrappedKey = WrappedKey.fromSecretKey(wrappingKey, rawKey);
-
-        assertEquals(GENERATION_ID, wrappedKey.getPlatformKeyGenerationId());
-    }
-
-    @Test
     public void decryptWrappedKeys_decryptsWrappedKeys() throws Exception {
         String alias = "karlin";
-        AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
+        PlatformDecryptionKey platformKey = generatePlatformDecryptionKey();
         SecretKey appKey = generateKey();
-        WrappedKey wrappedKey = WrappedKey.fromSecretKey(
-                new PlatformEncryptionKey(GENERATION_ID, platformKey), appKey);
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey.getKey(), appKey);
         HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
         keysByAlias.put(alias, wrappedKey);
 
-        Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
-                new PlatformDecryptionKey(GENERATION_ID, platformKey), keysByAlias);
+        Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(platformKey, keysByAlias);
 
         assertEquals(1, unwrappedKeys.size());
         assertTrue(unwrappedKeys.containsKey(alias));
@@ -109,31 +95,26 @@
     @Test
     public void decryptWrappedKeys_doesNotDieIfSomeKeysAreUnwrappable() throws Exception {
         String alias = "karlin";
-        AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
         SecretKey appKey = generateKey();
-        WrappedKey wrappedKey = WrappedKey.fromSecretKey(
-                new PlatformEncryptionKey(GENERATION_ID, platformKey), appKey);
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), appKey);
         HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
         keysByAlias.put(alias, wrappedKey);
 
         Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
-                new PlatformDecryptionKey(GENERATION_ID, platformKey), keysByAlias);
+                generatePlatformDecryptionKey(), keysByAlias);
 
         assertEquals(0, unwrappedKeys.size());
     }
 
     @Test
     public void decryptWrappedKeys_throwsIfPlatformKeyGenerationIdDoesNotMatch() throws Exception {
-        AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
-        WrappedKey wrappedKey = WrappedKey.fromSecretKey(
-                new PlatformEncryptionKey(GENERATION_ID, platformKey), generateKey());
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), generateKey());
         HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
         keysByAlias.put("benji", wrappedKey);
 
         try {
             WrappedKey.unwrapKeys(
-                    new PlatformDecryptionKey(/*generationId=*/ 2, platformKey),
-                    keysByAlias);
+                    generatePlatformDecryptionKey(/*generationId=*/ 2), keysByAlias);
             fail("Should have thrown.");
         } catch (BadPlatformKeyException e) {
             assertEquals(
@@ -161,4 +142,12 @@
                 .build());
         return (AndroidKeyStoreSecretKey) keyGenerator.generateKey();
     }
+
+    private PlatformDecryptionKey generatePlatformDecryptionKey() throws Exception {
+        return generatePlatformDecryptionKey(GENERATION_ID);
+    }
+
+    private PlatformDecryptionKey generatePlatformDecryptionKey(int generationId) throws Exception {
+        return new PlatformDecryptionKey(generationId, generateAndroidKeyStoreKey());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
new file mode 100644
index 0000000..5cb88dd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.storage;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RecoverableKeyStoreDbTest {
+    private static final String DATABASE_FILE_NAME = "recoverablekeystore.db";
+
+    private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
+    private File mDatabaseFile;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
+        mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
+    }
+
+    @After
+    public void tearDown() {
+        mRecoverableKeyStoreDb.close();
+        mDatabaseFile.delete();
+    }
+
+    @Test
+    public void insertKey_replacesOldKey() {
+        int userId = 12;
+        String alias = "test";
+        WrappedKey oldWrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce1"), getUtf8Bytes("keymaterial1"));
+        mRecoverableKeyStoreDb.insertKey(
+                userId, alias, oldWrappedKey, /*generationId=*/ 1);
+        byte[] nonce = getUtf8Bytes("nonce2");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial2");
+        WrappedKey newWrappedKey = new WrappedKey(nonce, keyMaterial);
+
+        mRecoverableKeyStoreDb.insertKey(
+                userId, alias, newWrappedKey, /*generationId=*/ 2);
+
+        WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getKey_returnsNullIfNoKey() {
+        WrappedKey key = mRecoverableKeyStoreDb.getKey(
+                /*userId=*/ 1, /*alias=*/ "hello");
+
+        assertNull(key);
+    }
+
+    @Test
+    public void getKey_returnsInsertedKey() {
+        int userId = 12;
+        int generationId = 6;
+        String alias = "test";
+        byte[] nonce = getUtf8Bytes("nonce");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial");
+        WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
+        mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
+
+        WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
+
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getAllKeys_getsKeysWithUserIdAndGenerationId() {
+        int userId = 12;
+        int generationId = 6;
+        String alias = "test";
+        byte[] nonce = getUtf8Bytes("nonce");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial");
+        WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
+        mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(userId, generationId);
+
+        assertEquals(1, keys.size());
+        assertTrue(keys.containsKey(alias));
+        WrappedKey retrievedKey = keys.get(alias);
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getAllKeys_doesNotReturnKeysWithBadGenerationId() {
+        int userId = 12;
+        WrappedKey wrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
+        mRecoverableKeyStoreDb.insertKey(
+                userId, /*alias=*/ "test", wrappedKey, /*generationId=*/ 5);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
+                userId, /*generationId=*/ 7);
+
+        assertTrue(keys.isEmpty());
+    }
+
+    @Test
+    public void getAllKeys_doesNotReturnKeysWithBadUserId() {
+        int generationId = 12;
+        WrappedKey wrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
+        mRecoverableKeyStoreDb.insertKey(
+                /*userId=*/ 1, /*alias=*/ "test", wrappedKey, generationId);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
+                /*userId=*/ 2, generationId);
+
+        assertTrue(keys.isEmpty());
+    }
+
+    private static byte[] getUtf8Bytes(String s) {
+        return s.getBytes(StandardCharsets.UTF_8);
+    }
+}