Merge "Implements onNanoAppAborted callback"
diff --git a/api/current.txt b/api/current.txt
index 9bdcdad..5df52b9 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -37692,11 +37692,8 @@
field public static final android.os.Parcelable.Creator<android.service.autofill.EditDistanceScorer> CREATOR;
}
- public final class FieldClassification implements android.os.Parcelable {
- method public int describeContents();
+ public final class FieldClassification {
method public java.util.List<android.service.autofill.FieldClassification.Match> getMatches();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<android.service.autofill.FieldClassification> CREATOR;
}
public static final class FieldClassification.Match {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 89df421..4bb4c50 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2676,6 +2676,7 @@
* @see UserManager#DISALLOW_UNIFIED_PASSWORD
*/
public boolean isUsingUnifiedPassword(@NonNull ComponentName admin) {
+ throwIfParentInstance("isUsingUnifiedPassword");
if (mService != null) {
try {
return mService.isUsingUnifiedPassword(admin);
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—activities that handle the
+ * {@link Intent#ACTION_MAIN} action and the {@link Intent#CATEGORY_LAUNCHER} category—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—shortcuts whose {@link ShortcutInfo#isDeclaredInManifest()} method
+ * returns {@code true}—followed by dynamic shortcuts—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 <shortcut>} 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—activities that handle the {@code MAIN} action and the
- * {@code LAUNCHER} category—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/service/autofill/FieldClassification.java b/core/java/android/service/autofill/FieldClassification.java
index b28c6f8..001b291 100644
--- a/core/java/android/service/autofill/FieldClassification.java
+++ b/core/java/android/service/autofill/FieldClassification.java
@@ -20,31 +20,39 @@
import android.annotation.NonNull;
import android.os.Parcel;
-import android.os.Parcelable;
import android.view.autofill.Helper;
import com.android.internal.util.Preconditions;
-import com.google.android.collect.Lists;
-
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
/**
* Represents the <a href="AutofillService.html#FieldClassification">field classification</a>
* results for a given field.
*/
-// TODO(b/70291841): let caller handle Parcelable...
-public final class FieldClassification implements Parcelable {
+public final class FieldClassification {
- private final Match mMatch;
+ private final ArrayList<Match> mMatches;
/** @hide */
- public FieldClassification(@NonNull Match match) {
- mMatch = Preconditions.checkNotNull(match);
+ public FieldClassification(@NonNull ArrayList<Match> matches) {
+ mMatches = Preconditions.checkNotNull(matches);
+ Collections.sort(mMatches, new Comparator<Match>() {
+ @Override
+ public int compare(Match o1, Match o2) {
+ if (o1.mScore > o2.mScore) return -1;
+ if (o1.mScore < o2.mScore) return 1;
+ return 0;
+ }}
+ );
}
/**
- * Gets the {@link Match matches} with the highest {@link Match#getScore() scores}.
+ * Gets the {@link Match matches} with the highest {@link Match#getScore() scores} (sorted in
+ * descending order).
*
* <p><b>Note:</b> There's no guarantee of how many matches will be returned. In fact,
* the Android System might return just the top match to minimize the impact of field
@@ -52,43 +60,48 @@
*/
@NonNull
public List<Match> getMatches() {
- return Lists.newArrayList(mMatch);
+ return mMatches;
}
@Override
public String toString() {
if (!sDebug) return super.toString();
- return "FieldClassification: " + mMatch;
+ return "FieldClassification: " + mMatches;
}
- /////////////////////////////////////
- // Parcelable "contract" methods. //
- /////////////////////////////////////
-
- @Override
- public int describeContents() {
- return 0;
+ private void writeToParcel(Parcel parcel) {
+ parcel.writeInt(mMatches.size());
+ for (int i = 0; i < mMatches.size(); i++) {
+ mMatches.get(i).writeToParcel(parcel);
+ }
}
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- mMatch.writeToParcel(parcel);
- }
-
- public static final Parcelable.Creator<FieldClassification> CREATOR =
- new Parcelable.Creator<FieldClassification>() {
-
- @Override
- public FieldClassification createFromParcel(Parcel parcel) {
- return new FieldClassification(Match.readFromParcel(parcel));
+ private static FieldClassification readFromParcel(Parcel parcel) {
+ final int size = parcel.readInt();
+ final ArrayList<Match> matches = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ matches.add(i, Match.readFromParcel(parcel));
}
- @Override
- public FieldClassification[] newArray(int size) {
- return new FieldClassification[size];
+ return new FieldClassification(matches);
+ }
+
+ static FieldClassification[] readArrayFromParcel(Parcel parcel) {
+ final int length = parcel.readInt();
+ final FieldClassification[] fcs = new FieldClassification[length];
+ for (int i = 0; i < length; i++) {
+ fcs[i] = readFromParcel(parcel);
}
- };
+ return fcs;
+ }
+
+ static void writeArrayToParcel(@NonNull Parcel parcel, @NonNull FieldClassification[] fcs) {
+ parcel.writeInt(fcs.length);
+ for (int i = 0; i < fcs.length; i++) {
+ fcs[i].writeToParcel(parcel);
+ }
+ }
/**
* Represents the score of a {@link UserData} entry for the field.
@@ -151,23 +164,5 @@
private static Match readFromParcel(@NonNull Parcel parcel) {
return new Match(parcel.readString(), parcel.readFloat());
}
-
- /** @hide */
- public static Match[] readArrayFromParcel(@NonNull Parcel parcel) {
- final int length = parcel.readInt();
- final Match[] matches = new Match[length];
- for (int i = 0; i < length; i++) {
- matches[i] = readFromParcel(parcel);
- }
- return matches;
- }
-
- /** @hide */
- public static void writeArrayToParcel(@NonNull Parcel parcel, @NonNull Match[] matches) {
- parcel.writeInt(matches.length);
- for (int i = 0; i < matches.length; i++) {
- matches[i].writeToParcel(parcel);
- }
- }
}
}
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index 07fab61..df62446 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -25,7 +25,6 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.service.autofill.FieldClassification.Match;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -157,7 +156,8 @@
final AutofillId[] detectedFields = event.mDetectedFieldIds;
parcel.writeParcelableArray(detectedFields, flags);
if (detectedFields != null) {
- Match.writeArrayToParcel(parcel, event.mDetectedMatches);
+ FieldClassification.writeArrayToParcel(parcel,
+ event.mDetectedFieldClassifications);
}
}
}
@@ -251,7 +251,7 @@
@Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;
@Nullable private final AutofillId[] mDetectedFieldIds;
- @Nullable private final Match[] mDetectedMatches;
+ @Nullable private final FieldClassification[] mDetectedFieldClassifications;
/**
* Returns the type of the event.
@@ -370,11 +370,11 @@
final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(size);
for (int i = 0; i < size; i++) {
final AutofillId id = mDetectedFieldIds[i];
- final Match match = mDetectedMatches[i];
+ final FieldClassification fc = mDetectedFieldClassifications[i];
if (sVerbose) {
- Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", match=" + match);
+ Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", fc=" + fc);
}
- map.put(id, new FieldClassification(match));
+ map.put(id, fc);
}
return map;
}
@@ -455,7 +455,7 @@
* and belonged to datasets.
* @param manuallyFilledDatasetIds The ids of datasets that had values matching the
* respective entry on {@code manuallyFilledFieldIds}.
- * @param detectedMatches the field classification matches.
+ * @param detectedFieldClassifications the field classification matches.
*
* @throws IllegalArgumentException If the length of {@code changedFieldIds} and
* {@code changedDatasetIds} doesn't match.
@@ -471,7 +471,8 @@
@Nullable ArrayList<String> changedDatasetIds,
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
- @Nullable AutofillId[] detectedFieldIds, @Nullable Match[] detectedMatches) {
+ @Nullable AutofillId[] detectedFieldIds,
+ @Nullable FieldClassification[] detectedFieldClassifications) {
mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED,
"eventType");
mDatasetId = datasetId;
@@ -496,7 +497,7 @@
mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
mDetectedFieldIds = detectedFieldIds;
- mDetectedMatches = detectedMatches;
+ mDetectedFieldClassifications = detectedFieldClassifications;
}
@Override
@@ -510,7 +511,8 @@
+ ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
+ ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
+ ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds)
- + ", detectedMaches =" + Arrays.toString(mDetectedMatches)
+ + ", detectedFieldClassifications ="
+ + Arrays.toString(mDetectedFieldClassifications)
+ "]";
}
}
@@ -548,15 +550,16 @@
}
final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null,
AutofillId.class);
- final Match[] detectedMatches = (detectedFieldIds != null)
- ? Match.readArrayFromParcel(parcel)
+ final FieldClassification[] detectedFieldClassifications =
+ (detectedFieldIds != null)
+ ? FieldClassification.readArrayFromParcel(parcel)
: null;
selection.addEvent(new Event(eventType, datasetId, clientState,
selectedDatasetIds, ignoredDatasets,
changedFieldIds, changedDatasetIds,
manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedFieldIds, detectedMatches));
+ detectedFieldIds, detectedFieldClassifications));
}
return selection;
}
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/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 4bf3c5a..3361824 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -52,6 +52,7 @@
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
+import android.service.autofill.FieldClassification;
import android.service.autofill.FieldClassification.Match;
import android.service.autofill.FillEventHistory;
import android.service.autofill.FillEventHistory.Event;
@@ -81,6 +82,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.List;
import java.util.Random;
/**
@@ -720,37 +722,45 @@
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
@Nullable ArrayList<AutofillId> detectedFieldIdsList,
- @Nullable ArrayList<Match> detectedMatchesList,
+ @Nullable ArrayList<FieldClassification> detectedFieldClassificationsList,
@NonNull String appPackageName) {
-
synchronized (mLock) {
if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
AutofillId[] detectedFieldsIds = null;
- Match[] detectedMatches = null;
+ FieldClassification[] detectedFieldClassifications = null;
if (detectedFieldIdsList != null) {
detectedFieldsIds = new AutofillId[detectedFieldIdsList.size()];
detectedFieldIdsList.toArray(detectedFieldsIds);
- detectedMatches = new Match[detectedMatchesList.size()];
- detectedMatchesList.toArray(detectedMatches);
+ detectedFieldClassifications =
+ new FieldClassification[detectedFieldClassificationsList.size()];
+ detectedFieldClassificationsList.toArray(detectedFieldClassifications);
- final int size = detectedMatchesList.size();
+ final int numberFields = detectedFieldsIds.length;
+ int totalSize = 0;
float totalScore = 0;
- for (int i = 0; i < size; i++) {
- totalScore += detectedMatches[i].getScore();
+ for (int i = 0; i < numberFields; i++) {
+ final FieldClassification fc = detectedFieldClassifications[i];
+ final List<Match> matches = fc.getMatches();
+ final int size = matches.size();
+ totalSize += size;
+ for (int j = 0; j < size; j++) {
+ totalScore += matches.get(j).getScore();
+ }
}
- final int averageScore = (int) ((totalScore * 100) / size);
- mMetricsLogger.write(
- Helper.newLogMaker(MetricsEvent.AUTOFILL_FIELD_CLASSIFICATION_MATCHES,
- appPackageName, getServicePackageName())
- .setCounterValue(size)
- .addTaggedData(MetricsEvent.FIELD_AUTOFILL_MATCH_SCORE, averageScore));
+ final int averageScore = (int) ((totalScore * 100) / totalSize);
+ mMetricsLogger.write(Helper
+ .newLogMaker(MetricsEvent.AUTOFILL_FIELD_CLASSIFICATION_MATCHES,
+ appPackageName, getServicePackageName())
+ .setCounterValue(numberFields)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_MATCH_SCORE,
+ averageScore));
}
mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
clientState, selectedDatasets, ignoredDatasets,
changedFieldIds, changedDatasetIds,
manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedFieldsIds, detectedMatches));
+ detectedFieldsIds, detectedFieldClassifications));
}
}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index ceae93c..7b85a6c 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -66,6 +66,7 @@
import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
import android.service.autofill.EditDistanceScorer;
+import android.service.autofill.FieldClassification;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LocalLog;
@@ -961,15 +962,15 @@
final UserData userData = mService.getUserData();
final ArrayList<AutofillId> detectedFieldIds;
- final ArrayList<Match> detectedMatches;
+ final ArrayList<FieldClassification> detectedFieldClassifications;
if (userData != null) {
final int maxFieldsSize = UserData.getMaxFieldClassificationIdsSize();
detectedFieldIds = new ArrayList<>(maxFieldsSize);
- detectedMatches = new ArrayList<>(maxFieldsSize);
+ detectedFieldClassifications = new ArrayList<>(maxFieldsSize);
} else {
detectedFieldIds = null;
- detectedMatches = null;
+ detectedFieldClassifications = null;
}
for (int i = 0; i < mViewStates.size(); i++) {
@@ -1078,8 +1079,8 @@
// Sets field classification score for field
if (userData!= null) {
- setScore(detectedFieldIds, detectedMatches, userData, viewState.id,
- currentValue);
+ setScore(detectedFieldIds, detectedFieldClassifications, userData,
+ viewState.id, currentValue);
}
} // else
} // else
@@ -1093,7 +1094,7 @@
+ ", changedDatasetIds=" + changedDatasetIds
+ ", manuallyFilledIds=" + manuallyFilledIds
+ ", detectedFieldIds=" + detectedFieldIds
- + ", detectedMatches=" + detectedMatches
+ + ", detectedFieldClassifications=" + detectedFieldClassifications
);
}
@@ -1116,16 +1117,17 @@
mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
changedFieldIds, changedDatasetIds,
manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedFieldIds, detectedMatches, mComponentName.getPackageName());
+ detectedFieldIds, detectedFieldClassifications, mComponentName.getPackageName());
}
/**
- * Adds the top score match to {@code detectedFieldsIds} and {@code detectedMatches} for
+ * Adds the matches to {@code detectedFieldsIds} and {@code detectedFieldClassifications} for
* {@code fieldId} based on its {@code currentValue} and {@code userData}.
*/
private static void setScore(@NonNull ArrayList<AutofillId> detectedFieldIds,
- @NonNull ArrayList<Match> detectedMatches, @NonNull UserData userData,
- @NonNull AutofillId fieldId, @NonNull AutofillValue currentValue) {
+ @NonNull ArrayList<FieldClassification> detectedFieldClassifications,
+ @NonNull UserData userData, @NonNull AutofillId fieldId,
+ @NonNull AutofillValue currentValue) {
final String[] userValues = userData.getValues();
final String[] remoteIds = userData.getRemoteIds();
@@ -1138,23 +1140,26 @@
+ valuesLength + ", ids.length = " + idsLength);
return;
}
- String remoteId = null;
- float topScore = 0;
+
+ ArrayList<Match> matches = null;
for (int i = 0; i < userValues.length; i++) {
+ String remoteId = remoteIds[i];
final String value = userValues[i];
final float score = userData.getScorer().getScore(currentValue, value);
- if (score > topScore) {
- topScore = score;
- remoteId = remoteIds[i];
+ if (score > 0) {
+ if (sVerbose) {
+ Slog.v(TAG, "adding score " + score + " at index " + i + " and id " + fieldId);
+ }
+ if (matches == null) {
+ matches = new ArrayList<>(userValues.length);
+ }
+ matches.add(new Match(remoteId, score));
}
+ else if (sVerbose) Slog.v(TAG, "skipping score 0 at index " + i + " and id " + fieldId);
}
-
- if (remoteId != null && topScore > 0) {
- if (sVerbose) Slog.v(TAG, "setScores(): top score for #" + fieldId + " is " + topScore);
+ if (matches != null) {
detectedFieldIds.add(fieldId);
- detectedMatches.add(new Match(remoteId, topScore));
- } else if (sVerbose) {
- Slog.v(TAG, "setScores(): no top score for #" + fieldId + ": " + topScore);
+ detectedFieldClassifications.add(new FieldClassification(matches));
}
}
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 56bc19d..7f88663 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/KeySyncUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
index e4d2b953..37aeb3a 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
@@ -16,6 +16,8 @@
package com.android.server.locksettings.recoverablekeystore;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -25,6 +27,7 @@
import java.util.HashMap;
import java.util.Map;
+import javax.crypto.AEADBadTagException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
@@ -45,9 +48,13 @@
"V1 locally_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
private static final byte[] ENCRYPTED_APPLICATION_KEY_HEADER =
"V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] RECOVERY_CLAIM_HEADER =
+ "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);
+ private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
+
/**
* Encrypts the recovery key using both the lock screen hash and the remote storage's public
* key.
@@ -121,7 +128,7 @@
*/
public static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
- keyGenerator.init(RECOVERY_KEY_SIZE_BITS, SecureRandom.getInstanceStrong());
+ keyGenerator.init(RECOVERY_KEY_SIZE_BITS, new SecureRandom());
return keyGenerator.generateKey();
}
@@ -153,13 +160,100 @@
}
/**
- * Returns a new array, the contents of which are the concatenation of {@code a} and {@code b}.
+ * Returns a random 16-byte key claimant.
+ *
+ * @hide
*/
- private static byte[] concat(byte[] a, byte[] b) {
- byte[] result = new byte[a.length + b.length];
- System.arraycopy(a, 0, result, 0, a.length);
- System.arraycopy(b, 0, result, a.length, b.length);
- return result;
+ public static byte[] generateKeyClaimant() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] key = new byte[KEY_CLAIMANT_LENGTH_BYTES];
+ secureRandom.nextBytes(key);
+ return key;
+ }
+
+ /**
+ * Encrypts a claim to recover a remote recovery key.
+ *
+ * @param publicKey The public key of the remote server.
+ * @param vaultParams Associated vault parameters.
+ * @param challenge The challenge issued by the server.
+ * @param thmKfHash The THM hash of the lock screen.
+ * @param keyClaimant The random key claimant.
+ * @return The encrypted recovery claim, to be sent to the remote server.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
+ * @throws InvalidKeyException if the {@code publicKey} could not be used to encrypt.
+ *
+ * @hide
+ */
+ public static byte[] encryptRecoveryClaim(
+ PublicKey publicKey,
+ byte[] vaultParams,
+ byte[] challenge,
+ byte[] thmKfHash,
+ byte[] keyClaimant) throws NoSuchAlgorithmException, InvalidKeyException {
+ return SecureBox.encrypt(
+ publicKey,
+ /*sharedSecret=*/ null,
+ /*header=*/ concat(RECOVERY_CLAIM_HEADER, vaultParams, challenge),
+ /*payload=*/ concat(thmKfHash, keyClaimant));
+ }
+
+ /**
+ * Decrypts a recovery key, after having retrieved it from a remote server.
+ *
+ * @param lskfHash The lock screen hash associated with the key.
+ * @param encryptedRecoveryKey The encrypted key.
+ * @return The raw key material.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+ * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+ * different key.
+ */
+ public static byte[] decryptRecoveryKey(byte[] lskfHash, byte[] encryptedRecoveryKey)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ lskfHash,
+ /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
+ /*encryptedPayload=*/ encryptedRecoveryKey);
+ }
+
+ /**
+ * Decrypts an application key, using the recovery key.
+ *
+ * @param recoveryKey The recovery key - used to wrap all application keys.
+ * @param encryptedApplicationKey The application key to unwrap.
+ * @return The raw key material of the application key.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+ * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+ * different key.
+ */
+ public static byte[] decryptApplicationKey(byte[] recoveryKey, byte[] encryptedApplicationKey)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ recoveryKey,
+ /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
+ /*encryptedPayload=*/ encryptedApplicationKey);
+ }
+
+ /**
+ * Returns the concatenation of all the given {@code arrays}.
+ */
+ @VisibleForTesting
+ static byte[] concat(byte[]... arrays) {
+ int length = 0;
+ for (byte[] array : arrays) {
+ length += array.length;
+ }
+
+ byte[] concatenated = new byte[length];
+ int pos = 0;
+ for (byte[] array : arrays) {
+ System.arraycopy(array, /*srcPos=*/ 0, concatenated, pos, array.length);
+ pos += array.length;
+ }
+
+ return concatenated;
}
// Statics only
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
index 457fdc14..742cb45 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
@@ -16,10 +16,15 @@
package com.android.server.locksettings.recoverablekeystore;
+import android.annotation.Nullable;
+
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
import java.security.PublicKey;
+import javax.crypto.AEADBadTagException;
+
/**
* TODO(b/69056040) Add implementation of SecureBox. This is a placeholder so KeySyncUtils compiles.
*
@@ -32,8 +37,25 @@
* @hide
*/
public static byte[] encrypt(
- PublicKey theirPublicKey, byte[] sharedSecret, byte[] header, byte[] payload)
+ @Nullable PublicKey theirPublicKey,
+ @Nullable byte[] sharedSecret,
+ @Nullable byte[] header,
+ @Nullable byte[] payload)
throws NoSuchAlgorithmException, InvalidKeyException {
throw new UnsupportedOperationException("Needs to be implemented.");
}
+
+ /**
+ * TODO(b/69056040) Add implementation of decrypt.
+ *
+ * @hide
+ */
+ public static byte[] decrypt(
+ @Nullable PrivateKey ourPrivateKey,
+ @Nullable byte[] sharedSecret,
+ @Nullable byte[] header,
+ byte[] encryptedPayload)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ throw new UnsupportedOperationException("Needs to be implemented.");
+ }
}
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/KeySyncUtilsTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
index c918e8c..ac3abed 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
@@ -37,6 +37,7 @@
public class KeySyncUtilsTest {
private static final int RECOVERY_KEY_LENGTH_BITS = 256;
private static final int THM_KF_HASH_SIZE = 256;
+ private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
private static final String SHA_256_ALGORITHM = "SHA-256";
@Test
@@ -70,6 +71,32 @@
assertFalse(Arrays.equals(a.getEncoded(), b.getEncoded()));
}
+ @Test
+ public void generateKeyClaimant_returns16Bytes() throws Exception {
+ byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
+
+ assertEquals(KEY_CLAIMANT_LENGTH_BYTES, keyClaimant.length);
+ }
+
+ @Test
+ public void generateKeyClaimant_generatesANewClaimantEachTime() {
+ byte[] a = KeySyncUtils.generateKeyClaimant();
+ byte[] b = KeySyncUtils.generateKeyClaimant();
+
+ assertFalse(Arrays.equals(a, b));
+ }
+
+ @Test
+ public void concat_concatenatesArrays() {
+ assertArrayEquals(
+ utf8Bytes("hello, world!"),
+ KeySyncUtils.concat(
+ utf8Bytes("hello"),
+ utf8Bytes(", "),
+ utf8Bytes("world"),
+ utf8Bytes("!")));
+ }
+
private static byte[] utf8Bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
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);
+ }
+}