diff options
28 files changed, 1191 insertions, 303 deletions
diff --git a/api/current.txt b/api/current.txt index 9bdcdadc7841..5df52b98f4f5 100644 --- a/api/current.txt +++ b/api/current.txt @@ -37692,11 +37692,8 @@ package android.service.autofill { 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 89df421efcc1..4bb4c50190b0 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2676,6 +2676,7 @@ public class DevicePolicyManager { * @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 e2fd82d1f3e1..21e203bc50d7 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -87,7 +87,6 @@ import android.util.SparseArray; 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 @@ public class PackageParser { 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()); + } + } else { + if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) { + throw new PackageParserException( + INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + apkPath + " has mismatched certificates"); } - throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, - "No APK Signature Scheme v2 signature in package " + apkPath, e); } } diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index 61b0eb0b51f8..8623524350db 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -36,15 +36,26 @@ import com.android.internal.annotations.VisibleForTesting; 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 @@ import java.util.List; * <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 @@ import java.util.List; * <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 @@ import java.util.List; * * <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 @@ import java.util.List; * * <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 @@ import java.util.List; * 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 @@ import java.util.List; * <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 b28c6f81e782..001b2917aadf 100644 --- a/core/java/android/service/autofill/FieldClassification.java +++ b/core/java/android/service/autofill/FieldClassification.java @@ -20,31 +20,39 @@ import static android.view.autofill.Helper.sDebug; 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 @@ public final class FieldClassification implements Parcelable { */ @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); - } + 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)); + } - public static final Parcelable.Creator<FieldClassification> CREATOR = - new Parcelable.Creator<FieldClassification>() { + return new FieldClassification(matches); + } - @Override - public FieldClassification createFromParcel(Parcel parcel) { - return new FieldClassification(Match.readFromParcel(parcel)); + 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; + } - @Override - public FieldClassification[] newArray(int size) { - return new FieldClassification[size]; + 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 @@ public final class FieldClassification implements Parcelable { 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 07fab611e6be..df62446427d3 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -25,7 +25,6 @@ import android.content.IntentSender; 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 @@ public final class FillEventHistory implements Parcelable { 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 @@ public final class FillEventHistory implements Parcelable { @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 @@ public final class FillEventHistory implements Parcelable { 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 @@ public final class FillEventHistory implements Parcelable { * 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 @@ public final class FillEventHistory implements Parcelable { @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 @@ public final class FillEventHistory implements Parcelable { mManuallyFilledDatasetIds = manuallyFilledDatasetIds; mDetectedFieldIds = detectedFieldIds; - mDetectedMatches = detectedMatches; + mDetectedFieldClassifications = detectedFieldClassifications; } @Override @@ -510,7 +511,8 @@ public final class FillEventHistory implements Parcelable { + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds) - + ", detectedMaches =" + Arrays.toString(mDetectedMatches) + + ", detectedFieldClassifications =" + + Arrays.toString(mDetectedFieldClassifications) + "]"; } } @@ -548,15 +550,16 @@ public final class FillEventHistory implements Parcelable { } 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 73a9478509e6..75c1000ba41d 100644 --- a/core/java/android/util/apk/ApkSignatureVerifier.java +++ b/core/java/android/util/apk/ApkSignatureVerifier.java @@ -65,18 +65,14 @@ public class ApkSignatureVerifier { * 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 @@ public class ApkSignatureVerifier { } 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 @@ public class ApkSignatureVerifier { } } } - 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 class ApkSignatureVerifier { 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 3f8da093487b..2c82ac49c84e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -327,6 +327,8 @@ public final class ViewRootImpl implements ViewParent, // 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 @@ public final class ViewRootImpl implements ViewParent, 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 final class ViewRootImpl implements ViewParent, 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 000000000000..620424fb4112 --- /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 000000000000..60c315c0c4d4 --- /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 000000000000..4961c1c72984 --- /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 fa2499f6c144..5c73d5485e2a 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.AppGlobals; 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 android.util.SparseArray; 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 @@ public class ApplicationsState { .replaceAll("").toLowerCase(); } - public class Session implements LifecycleObserver, OnPause, OnResume, OnDestroy { + public class Session implements LifecycleObserver { final Callbacks mCallbacks; boolean mResumed; @@ -621,6 +619,7 @@ public class ApplicationsState { } } + @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 @@ public class ApplicationsState { 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 @@ public class ApplicationsState { 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 000000000000..2b6d09f9b72e --- /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 273802754bbb..02a49738e6a1 100644 --- a/packages/SettingsLib/tests/robotests/Android.mk +++ b/packages/SettingsLib/tests/robotests/Android.mk @@ -49,7 +49,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ 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_TEST_PACKAGE := SettingsLibShell 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 698e44247e73..df850bee13df 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 @@ public class SettingsLibRobolectricTestRunner extends RobolectricTestRunner { 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 000000000000..fc0019d09c41 --- /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 4bf3c5ab9268..33618249f3c1 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.os.UserManager; 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 com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; import java.util.Random; /** @@ -720,37 +722,45 @@ final class AutofillManagerServiceImpl { @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 ceae93cefe95..7b85a6c935da 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.SaveRequest; 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 class Session implements RemoteFillService.FillServiceCallbacks, ViewState 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + ", changedDatasetIds=" + changedDatasetIds + ", manuallyFilledIds=" + manuallyFilledIds + ", detectedFieldIds=" + detectedFieldIds - + ", detectedMatches=" + detectedMatches + + ", detectedFieldClassifications=" + detectedFieldClassifications ); } @@ -1116,16 +1117,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + 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 efa0bf82f209..2d7a6adeba3f 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 @@ public class AudioService extends IAudioService.Stub 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 56bc19d9eb7c..7f88663d4e3b 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 @@ public class ContextHubService extends IContextHubService.Stub { 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 @@ public class ContextHubService extends IContextHubService.Stub { 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 e4d2b953b61a..37aeb3af051e 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.security.SecureRandom; import java.util.HashMap; import java.util.Map; +import javax.crypto.AEADBadTagException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -45,9 +48,13 @@ public class KeySyncUtils { "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 class KeySyncUtils { */ 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 @@ public class KeySyncUtils { } /** - * 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 + */ + 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 */ - 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[] 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 457fdc14c7f6..742cb4591864 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 @@ public class SecureBox { * @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 000000000000..79bf5aad3fbf --- /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 000000000000..c54d0a6f4f77 --- /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 000000000000..e3783c496ee2 --- /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 0b089fbc6129..384efdda88d7 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 com.android.server.Watchdog; 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 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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 @@ public final class MediaRouterService extends IMediaRouterService.Stub @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; - } - 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; + mAudioRouteMainType = newRoutes.mainType; } - // 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 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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 c918e8c7899d..ac3abedba7f3 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 @@ import javax.crypto.SecretKey; 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 @@ public class KeySyncUtilsTest { 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 000000000000..5cb88dd073ec --- /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); + } +} |