Merge "Move logic for backup journal into its own class"
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 143d147..d6e3691 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -15,11 +15,6 @@
*/
package android.app;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlSerializer;
-
import android.annotation.SystemApi;
import android.app.NotificationManager.Importance;
import android.content.Intent;
@@ -31,6 +26,11 @@
import android.service.notification.NotificationListenerService;
import android.text.TextUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
import java.io.IOException;
import java.util.Arrays;
@@ -743,7 +743,7 @@
private static String longArrayToString(long[] values) {
StringBuffer sb = new StringBuffer();
- if (values != null) {
+ if (values != null && values.length > 0) {
for (int i = 0; i < values.length - 1; i++) {
sb.append(values[i]).append(DELIMITER);
}
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index f21545f..04a8265 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -1387,7 +1387,7 @@
if (mTextActionMode != null) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
- hideFloatingToolbar();
+ hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
break;
case MotionEvent.ACTION_UP: // fall through
case MotionEvent.ACTION_CANCEL:
@@ -1396,10 +1396,10 @@
}
}
- private void hideFloatingToolbar() {
+ void hideFloatingToolbar(int duration) {
if (mTextActionMode != null) {
mTextView.removeCallbacks(mShowFloatingToolbar);
- mTextActionMode.hide(ActionMode.DEFAULT_HIDE_DURATION);
+ mTextActionMode.hide(duration);
}
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 9a92489..d277616 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -374,6 +374,8 @@
private static final int KEY_DOWN_HANDLED_BY_KEY_LISTENER = 1;
private static final int KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD = 2;
+ private static final int FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY = 500;
+
// System wide time for last cut, copy or text changed action.
static long sLastCutCopyOrTextChangedTime;
@@ -11138,6 +11140,10 @@
}
boolean selectAllText() {
+ if (mEditor != null) {
+ // Hide the toolbar before changing the selection to avoid flickering.
+ mEditor.hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY);
+ }
final int length = mText.length();
Selection.setSelection((Spannable) mText, 0, length);
return length > 0;
diff --git a/core/java/com/android/internal/view/FloatingActionMode.java b/core/java/com/android/internal/view/FloatingActionMode.java
index ff211b6..497e7b0 100644
--- a/core/java/com/android/internal/view/FloatingActionMode.java
+++ b/core/java/com/android/internal/view/FloatingActionMode.java
@@ -295,6 +295,8 @@
*/
private static final class FloatingToolbarVisibilityHelper {
+ private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500;
+
private final FloatingToolbar mToolbar;
private boolean mHideRequested;
@@ -304,6 +306,8 @@
private boolean mActive;
+ private long mLastShowTime;
+
public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
mToolbar = Preconditions.checkNotNull(toolbar);
}
@@ -327,7 +331,13 @@
}
public void setMoving(boolean moving) {
- mMoving = moving;
+ // Avoid unintended flickering by allowing the toolbar to show long enough before
+ // triggering the 'moving' flag - which signals a hide.
+ final boolean showingLongEnough =
+ System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE;
+ if (!moving || showingLongEnough) {
+ mMoving = moving;
+ }
}
public void setOutOfBounds(boolean outOfBounds) {
@@ -347,6 +357,7 @@
mToolbar.hide();
} else {
mToolbar.show();
+ mLastShowTime = System.currentTimeMillis();
}
}
}
diff --git a/legacy-test/jarjar-rules.txt b/legacy-test/jarjar-rules.txt
index 9077e6f..fd8555c 100644
--- a/legacy-test/jarjar-rules.txt
+++ b/legacy-test/jarjar-rules.txt
@@ -1,2 +1,3 @@
rule junit.** repackaged.junit.@1
rule android.test.** repackaged.android.test.@1
+rule com.android.internal.util.** repackaged.com.android.internal.util.@1
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index a7e1490..be87ed2 100644
--- a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -31,6 +31,7 @@
import android.net.Proxy;
import android.net.Uri;
import android.net.http.SslError;
+import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.ArrayMap;
@@ -57,6 +58,7 @@
import java.lang.InterruptedException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.util.Objects;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -286,6 +288,18 @@
return null;
}
+ private static String host(URL url) {
+ if (url == null) {
+ return null;
+ }
+ return url.getHost();
+ }
+
+ private static String sanitizeURL(URL url) {
+ // In non-Debug build, only show host to avoid leaking private info.
+ return Build.IS_DEBUGGABLE ? Objects.toString(url) : host(url);
+ }
+
private void testForCaptivePortal() {
// TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
new Thread(new Runnable() {
@@ -339,6 +353,8 @@
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
getResources().getDisplayMetrics());
private int mPagesLoaded;
+ // the host of the page that this webview is currently loading. Can be null when undefined.
+ private String mHostname;
// If we haven't finished cleaning up the history, don't allow going back.
public boolean allowBack() {
@@ -346,8 +362,8 @@
}
@Override
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
- if (url.contains(mBrowserBailOutToken)) {
+ public void onPageStarted(WebView view, String urlString, Bitmap favicon) {
+ if (urlString.contains(mBrowserBailOutToken)) {
mLaunchBrowser = true;
done(Result.WANTED_AS_IS);
return;
@@ -355,11 +371,17 @@
// The first page load is used only to cause the WebView to
// fetch the proxy settings. Don't update the URL bar, and
// don't check if the captive portal is still there.
- if (mPagesLoaded == 0) return;
+ if (mPagesLoaded == 0) {
+ return;
+ }
+ final URL url = makeURL(urlString);
+ Log.d(TAG, "onPageSarted: " + sanitizeURL(url));
+ mHostname = host(url);
// For internally generated pages, leave URL bar listing prior URL as this is the URL
// the page refers to.
- if (!url.startsWith(INTERNAL_ASSETS)) {
- getActionBar().setSubtitle(getHeaderSubtitle(url));
+ if (!urlString.startsWith(INTERNAL_ASSETS)) {
+ String subtitle = (url != null) ? getHeaderSubtitle(url) : urlString;
+ getActionBar().setSubtitle(subtitle);
}
getProgressBar().setVisibility(View.VISIBLE);
testForCaptivePortal();
@@ -401,15 +423,18 @@
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
- logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR);
- Log.w(TAG, "SSL error (error: " + error.getPrimaryError() + " host: " +
- // Only show host to avoid leaking private info.
- Uri.parse(error.getUrl()).getHost() + " certificate: " +
- error.getCertificate() + "); displaying SSL warning.");
- final String sslErrorPage = makeSslErrorPage();
- if (VDBG) {
- Log.d(TAG, sslErrorPage);
+ final URL url = makeURL(error.getUrl());
+ final String host = host(url);
+ Log.d(TAG, String.format("SSL error: %s, url: %s, certificate: %s",
+ error.getPrimaryError(), sanitizeURL(url), error.getCertificate()));
+ if (url == null || !Objects.equals(host, mHostname)) {
+ // Ignore ssl errors for resources coming from a different hostname than the page
+ // that we are currently loading, and only cancel the request.
+ handler.cancel();
+ return;
}
+ logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR);
+ final String sslErrorPage = makeSslErrorPage();
view.loadDataWithBaseURL(INTERNAL_ASSETS, sslErrorPage, "text/HTML", "UTF-8", null);
}
@@ -502,16 +527,13 @@
return getString(R.string.action_bar_title, info.getExtraInfo().replaceAll("^\"|\"$", ""));
}
- private String getHeaderSubtitle(String urlString) {
- URL url = makeURL(urlString);
- if (url == null) {
- return urlString;
- }
+ private String getHeaderSubtitle(URL url) {
+ String host = host(url);
final String https = "https";
if (https.equals(url.getProtocol())) {
- return https + "://" + url.getHost();
+ return https + "://" + host;
}
- return url.getHost();
+ return host;
}
private void logMetricsEvent(int event) {
diff --git a/proto/src/ipconnectivity.proto b/proto/src/ipconnectivity.proto
index 76c5418..885896f 100644
--- a/proto/src/ipconnectivity.proto
+++ b/proto/src/ipconnectivity.proto
@@ -23,7 +23,7 @@
// It is not intended to map one to one to the TRANSPORT_* constants defined in
// android.net.NetworkCapabilities. Instead it is intended to be used as
// a dimension field for metrics events and aggregated metrics.
-// Next tag: 7
+// Next tag: 10
enum LinkLayer {
// An unknown link layer technology.
UNKNOWN = 0;
@@ -32,6 +32,9 @@
CELLULAR = 2;
ETHERNET = 3;
WIFI = 4;
+ WIFI_P2P = 7;
+ WIFI_NAN = 8; // Also known as WiFi Aware
+ LOWPAN = 9;
// Indicates that the link layer dimension is not relevant for the metrics or
// event considered.
@@ -47,16 +50,18 @@
optional int32 value = 2;
};
-// Logs changes in the system default network. Changes can be 1) acquiring a
-// default network with no previous default, 2) a switch of the system default
-// network to a new default network, 3) a loss of the system default network.
-// This message is associated to android.net.metrics.DefaultNetworkEvent.
+// An event record when the system default network disconnects or the system
+// switches to a new default network.
+// Next tag: 10.
message DefaultNetworkEvent {
- // A value of 0 means this is a loss of the system default network.
- optional NetworkId network_id = 1;
- // A value of 0 means there was no previous default network.
- optional NetworkId previous_network_id = 2;
+ // Reason why this network stopped being the default.
+ enum LostReason {
+ UNKNOWN = 0;
+ OUTSCORED = 1;
+ INVALIDATION = 2;
+ DISCONNECT = 3;
+ };
// Whether the network supports IPv4, IPv6, or both.
enum IPSupport {
@@ -66,17 +71,52 @@
DUAL = 3;
};
+ // Duration in milliseconds when this network was the default.
+ // Since version 4
+ optional int64 default_network_duration_ms = 5;
+
+ // Duration in milliseconds without a default network before this network
+ // became the default.
+ // Since version 4
+ optional int64 no_default_network_duration_ms = 6;
+
+ // Network score of this network when it became the default network.
+ // Since version 4
+ optional int64 initial_score = 7;
+
+ // Network score of this network when it stopped being the default network.
+ // Since version 4
+ optional int64 final_score = 8;
+
+ // Best available information about IP support of this default network.
+ // Since version 4
+ optional IPSupport ip_support = 9;
+
+
+ // Deprecated fields
+
+ // A value of 0 means this is a loss of the system default network.
+ // Deprecated since version 3. Replaced by top level network_id.
+ optional NetworkId network_id = 1 [deprecated = true];
+
+ // A value of 0 means there was no previous default network.
+ // Deprecated since version 3. Replaced by previous_default_network_id.
+ optional NetworkId previous_network_id = 2 [deprecated = true];
+
// Best available information about IP support of the previous network when
// disconnecting or switching to a new default network.
- optional IPSupport previous_network_ip_support = 3;
+ // Deprecated since version 3. Replaced by ip_support field.
+ optional IPSupport previous_network_ip_support = 3 [deprecated = true];
// The transport types of the new default network, represented by
// TRANSPORT_* constants as defined in NetworkCapabilities.
- repeated int32 transport_types = 4;
+ // Deprecated since version 3. Replaced by top-level transports field.
+ repeated int32 transport_types = 4 [deprecated = true];
};
// Logs IpReachabilityMonitor probe events and NUD_FAILED events.
// This message is associated to android.net.metrics.IpReachabilityEvent.
+// Next tag: 3.
message IpReachabilityEvent {
// The interface name (wlan, rmnet, lo, ...) on which the probe was sent.
// Deprecated since version 2, to be replaced by link_layer field.
@@ -91,6 +131,7 @@
// Logs NetworkMonitor and ConnectivityService events related to the state of
// a network: connection, evaluation, validation, lingering, and disconnection.
// This message is associated to android.net.metrics.NetworkEvent.
+// Next tag: 4.
message NetworkEvent {
// The id of the network on which this event happened.
// Deprecated since version 3.
@@ -108,6 +149,7 @@
// Logs individual captive portal probing events that are performed when
// evaluating or reevaluating networks for Internet connectivity.
// This message is associated to android.net.metrics.ValidationProbeEvent.
+// Next tag: 5.
message ValidationProbeEvent {
// The id of the network for which the probe was sent.
// Deprecated since version 3.
@@ -124,26 +166,64 @@
optional int32 probe_result = 4;
}
-// Logs DNS lookup latencies. Repeated fields must have the same length.
+// Logs DNS lookup latencies.
// This message is associated to android.net.metrics.DnsEvent.
-// Deprecated since version 2.
+// Next tag: 11
message DNSLookupBatch {
+
+ // The time it took for successful DNS lookups to complete.
+ // The number of repeated values can be less than getaddrinfo_query_count
+ // + gethostbyname_query_count in case of event rate-limiting.
+ repeated int32 latencies_ms = 4;
+
+ // The total number of getaddrinfo queries.
+ // Since version 4.
+ optional int64 getaddrinfo_query_count = 5;
+
+ // The total number of gethostbyname queries.
+ // Since version 4.
+ optional int64 gethostbyname_query_count = 6;
+
+ // The total number of getaddrinfo errors.
+ // Since version 4.
+ optional int64 getaddrinfo_error_count = 7;
+
+ // The total number of gethostbyname errors.
+ // Since version 4.
+ optional int64 gethostbyname_error_count = 8;
+
+ // Counts of all errors returned by getaddrinfo.
+ // The Pair key field is the getaddrinfo error value.
+ // The value field is the count for that return value.
+ // Since version 4
+ repeated Pair getaddrinfo_errors = 9;
+
+ // Counts of all errors returned by gethostbyname.
+ // The Pair key field is the gethostbyname errno value.
+ // the Pair value field is the count for that errno code.
+ // Since version 4
+ repeated Pair gethostbyname_errors = 10;
+
+
+ // Deprecated fields
+
// The id of the network on which the DNS lookups took place.
- optional NetworkId network_id = 1;
+ // Deprecated since version 3.
+ optional NetworkId network_id = 1 [deprecated = true];
// The types of the DNS lookups, as defined in android.net.metrics.DnsEvent.
- repeated int32 event_types = 2;
+ // Deprecated since version 3.
+ repeated int32 event_types = 2 [deprecated = true];
// The return values of the DNS resolver for each DNS lookups.
- repeated int32 return_codes = 3;
-
- // The time it took for each DNS lookups to complete.
- repeated int32 latencies_ms = 4;
+ // Deprecated since version 3.
+ repeated int32 return_codes = 3 [deprecated = true];
};
// Represents a collections of DNS lookup latencies and counters for a
// particular combination of DNS query type and return code.
// Since version 2.
+// Next tag: 7.
message DNSLatencies {
// The type of the DNS lookups, as defined in android.net.metrics.DnsEvent.
// Acts as a key for a set of DNS query results.
@@ -203,6 +283,7 @@
// state transition or a response packet parsing error.
// This message is associated to android.net.metrics.DhcpClientEvent and
// android.net.metrics.DhcpErrorEvent.
+// Next tag: 5
message DHCPEvent {
// The interface name (wlan, rmnet, lo, ...) on which the event happened.
// Deprecated since version 2, to be replaced by link_layer field.
@@ -255,7 +336,7 @@
// Represents Router Advertisement listening statistics for an interface with
// Android Packet Filter enabled.
// Since version 1.
-// Next tag: 12
+// Next tag: 15
message ApfStatistics {
// The time interval in milliseconds these stastistics cover.
optional int64 duration_ms = 1;
@@ -288,12 +369,28 @@
// The total number of APF program updates triggered when disabling the
// multicast filter. Since version 3.
+ // Since version 4.
optional int32 program_updates_allowing_multicast = 11;
+
+ // The total number of packets processed by the APF interpreter.
+ // Since version 4.
+ optional int32 total_packet_processed = 12;
+
+ // The total number of packets dropped by the APF interpreter.
+ // Since version 4.
+ optional int32 total_packet_dropped = 13;
+
+ // List of hardware counters collected by the APF interpreter.
+ // The Pair key is the counter id, defined in android.net.metrics.ApfStats.
+ // The Pair value is the counter value.
+ // Since version 4.
+ repeated Pair hardware_counters = 14;
}
// Represents the reception of a Router Advertisement packet for an interface
// with Android Packet Filter enabled.
// Since version 1.
+// Next tag: 7.
message RaEvent {
// All lifetime values are expressed in seconds. The default value for an
// option lifetime that was not present in the RA option list is -1.
@@ -322,6 +419,7 @@
// Represents an IP provisioning event in IpManager and how long the
// provisioning action took.
// This message is associated to android.net.metrics.IpManagerEvent.
+// Next tag: 4.
message IpProvisioningEvent {
// The interface name (wlan, rmnet, lo, ...) on which the probe was sent.
// Deprecated since version 2, to be replaced by link_layer field.
@@ -335,8 +433,48 @@
optional int32 latency_ms = 3;
}
+// Represents statistics from a single android Network.
+// Since version 4. Replace NetworkEvent.
+// Next tag: 9.
+message NetworkStats {
+
+ // Duration of this Network lifecycle in milliseconds.
+ optional int64 duration_ms = 1;
+
+ // Information about IP support of this network.
+ optional DefaultNetworkEvent.IPSupport ip_support = 2;
+
+ // True if the network was validated at least once.
+ optional bool ever_validated = 3;
+
+ // True if a captive portal was found at least once on this network.
+ optional bool portal_found = 4;
+
+ // Total number of times no connectivity was reported for this network.
+ optional int32 no_connectivity_reports = 5;
+
+ // Total number of validation attempts.
+ optional int32 validation_attempts = 6;
+
+ // Results from all validation attempts.
+ // The Pair key is the result:
+ // 0 -> unvalidated
+ // 1 -> validated
+ // 2 -> captive portal
+ // The Pair value is the duration of the validation attempts in milliseconds.
+ repeated Pair validation_events = 7;
+
+ // Time series of validation states in time order.
+ // The Pair key is the state:
+ // 0 -> unvalidated
+ // 1 -> validated
+ // 2 -> captive portal,
+ // The Pair value is the duration of that state in milliseconds.
+ repeated Pair validation_states = 8;
+}
+
// Represents one of the IP connectivity event defined in this file.
-// Next tag: 19
+// Next tag: 20
message IpConnectivityEvent {
// Time in ms when the event was recorded.
optional int64 time_ms = 1;
@@ -370,14 +508,13 @@
oneof event {
// An event about the system default network.
- // The link_layer field is not relevant for this event and set to NONE.
DefaultNetworkEvent default_network_event = 2;
// An IP reachability probe event.
IpReachabilityEvent ip_reachability_event = 3;
// A network lifecycle event.
- NetworkEvent network_event = 4;
+ NetworkEvent network_event = 4 [deprecated = true];
// A batch of DNS lookups.
// Deprecated in the nyc-mr2 release since version 2,and replaced by
@@ -407,10 +544,14 @@
// An RA packet reception event.
RaEvent ra_event = 11;
+
+ // Network statistics.
+ NetworkStats network_stats = 19;
};
};
// The information about IP connectivity events.
+// Next tag: 4.
message IpConnectivityLog {
// An array of IP connectivity events.
repeated IpConnectivityEvent events = 1;
@@ -424,5 +565,6 @@
// nyc-mr1: not populated, implicitly 1.
// nyc-mr2: 2.
// oc: 3.
+ // oc-dr1: 4. (sailfish, marlin, walleye, taimen)
optional int32 version = 3;
};
diff --git a/services/backup/java/com/android/server/backup/BackupPasswordManager.java b/services/backup/java/com/android/server/backup/BackupPasswordManager.java
new file mode 100644
index 0000000..ee7651b
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/BackupPasswordManager.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.backup;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.server.backup.utils.DataStreamFileCodec;
+import com.android.server.backup.utils.DataStreamCodec;
+import com.android.server.backup.utils.PasswordUtils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.security.SecureRandom;
+
+/**
+ * Manages persisting and verifying backup passwords.
+ *
+ * <p>Does not persist the password itself, but persists a PBKDF2 hash with a randomly chosen (also
+ * persisted) salt. Validation is performed by running the challenge text through the same
+ * PBKDF2 cycle with the persisted salt, and checking the hashes match.
+ *
+ * @see PasswordUtils for the hashing algorithm.
+ */
+public final class BackupPasswordManager {
+ private static final String TAG = "BackupPasswordManager";
+ private static final boolean DEBUG = false;
+
+ private static final int BACKUP_PW_FILE_VERSION = 2;
+ private static final int DEFAULT_PW_FILE_VERSION = 1;
+
+ private static final String PASSWORD_VERSION_FILE_NAME = "pwversion";
+ private static final String PASSWORD_HASH_FILE_NAME = "pwhash";
+
+ // See https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html
+ public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
+ public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";
+
+ private final SecureRandom mRng;
+ private final Context mContext;
+ private final File mBaseStateDir;
+
+ private String mPasswordHash;
+ private int mPasswordVersion;
+ private byte[] mPasswordSalt;
+
+ /**
+ * Creates an instance enforcing permissions using the {@code context} and persisting password
+ * data within the {@code baseStateDir}.
+ *
+ * @param context The context, for enforcing permissions around setting the password.
+ * @param baseStateDir A directory within which to persist password data.
+ * @param secureRandom Random number generator with which to generate password salts.
+ */
+ BackupPasswordManager(Context context, File baseStateDir, SecureRandom secureRandom) {
+ mContext = context;
+ mRng = secureRandom;
+ mBaseStateDir = baseStateDir;
+ loadStateFromFilesystem();
+ }
+
+ /**
+ * Returns {@code true} if a password for backup is set.
+ *
+ * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
+ * permission.
+ */
+ boolean hasBackupPassword() {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+ "hasBackupPassword");
+ return mPasswordHash != null && mPasswordHash.length() > 0;
+ }
+
+ /**
+ * Returns {@code true} if {@code password} matches the persisted password.
+ *
+ * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
+ * permission.
+ */
+ boolean backupPasswordMatches(String password) {
+ if (hasBackupPassword() && !passwordMatchesSaved(password)) {
+ if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Sets the new password, given a correct current password.
+ *
+ * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
+ * permission.
+ * @return {@code true} if has permission to set the password, {@code currentPassword}
+ * matches the currently persisted password, and is able to persist {@code newPassword}.
+ */
+ boolean setBackupPassword(String currentPassword, String newPassword) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+ "setBackupPassword");
+
+ if (!passwordMatchesSaved(currentPassword)) {
+ return false;
+ }
+
+ // Snap up to latest password file version.
+ try {
+ getPasswordVersionFileCodec().serialize(BACKUP_PW_FILE_VERSION);
+ mPasswordVersion = BACKUP_PW_FILE_VERSION;
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to write backup pw version; password not changed");
+ return false;
+ }
+
+ if (newPassword == null || newPassword.isEmpty()) {
+ return clearPassword();
+ }
+
+ try {
+ byte[] salt = randomSalt();
+ String newPwHash = PasswordUtils.buildPasswordHash(
+ PBKDF_CURRENT, newPassword, salt, PasswordUtils.PBKDF2_HASH_ROUNDS);
+
+ getPasswordHashFileCodec().serialize(new BackupPasswordHash(newPwHash, salt));
+ mPasswordHash = newPwHash;
+ mPasswordSalt = salt;
+ return true;
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to set backup password");
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if should try salting using the older PBKDF algorithm.
+ *
+ * <p>This is {@code true} for v1 files.
+ */
+ private boolean usePbkdf2Fallback() {
+ return mPasswordVersion < BACKUP_PW_FILE_VERSION;
+ }
+
+ /**
+ * Deletes the current backup password.
+ *
+ * @return {@code true} if successful.
+ */
+ private boolean clearPassword() {
+ File passwordHashFile = getPasswordHashFile();
+ if (passwordHashFile.exists() && !passwordHashFile.delete()) {
+ Slog.e(TAG, "Unable to clear backup password");
+ return false;
+ }
+
+ mPasswordHash = null;
+ mPasswordSalt = null;
+ return true;
+ }
+
+ /**
+ * Sets the password hash, salt, and version in the object from what has been persisted to the
+ * filesystem.
+ */
+ private void loadStateFromFilesystem() {
+ try {
+ mPasswordVersion = getPasswordVersionFileCodec().deserialize();
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to read backup pw version");
+ mPasswordVersion = DEFAULT_PW_FILE_VERSION;
+ }
+
+ try {
+ BackupPasswordHash hash = getPasswordHashFileCodec().deserialize();
+ mPasswordHash = hash.hash;
+ mPasswordSalt = hash.salt;
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to read saved backup pw hash");
+ }
+ }
+
+ /**
+ * Whether the candidate password matches the current password. If the persisted password is an
+ * older version, attempts hashing using the older algorithm.
+ *
+ * @param candidatePassword The password to try.
+ * @return {@code true} if the passwords match.
+ */
+ private boolean passwordMatchesSaved(String candidatePassword) {
+ return passwordMatchesSaved(PBKDF_CURRENT, candidatePassword)
+ || (usePbkdf2Fallback() && passwordMatchesSaved(PBKDF_FALLBACK, candidatePassword));
+ }
+
+ /**
+ * Returns {@code true} if the candidate password is correct.
+ *
+ * @param algorithm The algorithm used to hash passwords.
+ * @param candidatePassword The candidate password to compare to the current password.
+ * @return {@code true} if the candidate password matched the saved password.
+ */
+ private boolean passwordMatchesSaved(String algorithm, String candidatePassword) {
+ if (mPasswordHash == null) {
+ return candidatePassword == null || candidatePassword.equals("");
+ } else if (candidatePassword == null || candidatePassword.length() == 0) {
+ // The current password is not zero-length, but the candidate password is.
+ return false;
+ } else {
+ String candidatePasswordHash = PasswordUtils.buildPasswordHash(
+ algorithm, candidatePassword, mPasswordSalt, PasswordUtils.PBKDF2_HASH_ROUNDS);
+ return mPasswordHash.equalsIgnoreCase(candidatePasswordHash);
+ }
+ }
+
+ private byte[] randomSalt() {
+ int bitsPerByte = 8;
+ byte[] array = new byte[PasswordUtils.PBKDF2_SALT_SIZE / bitsPerByte];
+ mRng.nextBytes(array);
+ return array;
+ }
+
+ private DataStreamFileCodec<Integer> getPasswordVersionFileCodec() {
+ return new DataStreamFileCodec<>(
+ new File(mBaseStateDir, PASSWORD_VERSION_FILE_NAME),
+ new PasswordVersionFileCodec());
+ }
+
+ private DataStreamFileCodec<BackupPasswordHash> getPasswordHashFileCodec() {
+ return new DataStreamFileCodec<>(getPasswordHashFile(), new PasswordHashFileCodec());
+ }
+
+ private File getPasswordHashFile() {
+ return new File(mBaseStateDir, PASSWORD_HASH_FILE_NAME);
+ }
+
+ /**
+ * Container class for a PBKDF hash and the salt used to create the hash.
+ */
+ private static final class BackupPasswordHash {
+ public String hash;
+ public byte[] salt;
+
+ BackupPasswordHash(String hash, byte[] salt) {
+ this.hash = hash;
+ this.salt = salt;
+ }
+ }
+
+ /**
+ * The password version file contains a single 32-bit integer.
+ */
+ private static final class PasswordVersionFileCodec implements
+ DataStreamCodec<Integer> {
+ @Override
+ public void serialize(Integer integer, DataOutputStream dataOutputStream)
+ throws IOException {
+ dataOutputStream.write(integer);
+ }
+
+ @Override
+ public Integer deserialize(DataInputStream dataInputStream) throws IOException {
+ return dataInputStream.readInt();
+ }
+ }
+
+ /**
+ * The passwords hash file contains
+ *
+ * <ul>
+ * <li>A 32-bit integer representing the number of bytes in the salt;
+ * <li>The salt bytes;
+ * <li>A UTF-8 string of the hash.
+ * </ul>
+ */
+ private static final class PasswordHashFileCodec implements
+ DataStreamCodec<BackupPasswordHash> {
+ @Override
+ public void serialize(BackupPasswordHash backupPasswordHash,
+ DataOutputStream dataOutputStream) throws IOException {
+ dataOutputStream.writeInt(backupPasswordHash.salt.length);
+ dataOutputStream.write(backupPasswordHash.salt);
+ dataOutputStream.writeUTF(backupPasswordHash.hash);
+ }
+
+ @Override
+ public BackupPasswordHash deserialize(
+ DataInputStream dataInputStream) throws IOException {
+ int saltLen = dataInputStream.readInt();
+ byte[] salt = new byte[saltLen];
+ dataInputStream.readFully(salt);
+ String hash = dataInputStream.readUTF();
+ return new BackupPasswordHash(hash, salt);
+ }
+ }
+}
diff --git a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
index 9aa6229..bc5bebb 100644
--- a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
@@ -118,13 +118,11 @@
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.BackupManagerMonitorUtils;
import com.android.server.backup.utils.BackupObserverUtils;
-import com.android.server.backup.utils.PasswordUtils;
import com.android.server.power.BatterySaverPolicy.ServiceType;
import libcore.io.IoUtils;
import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -136,7 +134,6 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
@@ -170,10 +167,6 @@
// with U+FF00 or higher for system use).
public static final String KEY_WIDGET_STATE = "\uffed\uffedwidget";
- // Historical and current algorithm names
- public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
- public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";
-
// Name and current contents version of the full-backup manifest file
//
// Manifest version history:
@@ -191,7 +184,6 @@
// 5 : added support for key-value packages
public static final int BACKUP_FILE_VERSION = 5;
public static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
- private static final int BACKUP_PW_FILE_VERSION = 2;
public static final String BACKUP_METADATA_FILENAME = "_meta";
public static final int BACKUP_METADATA_VERSION = 1;
public static final int BACKUP_WIDGET_METADATA_TOKEN = 0x01FFED01;
@@ -284,6 +276,8 @@
private final Object mClearDataLock = new Object();
private volatile boolean mClearingData;
+ private final BackupPasswordManager mBackupPasswordManager;
+
@GuardedBy("mPendingRestores")
private boolean mIsRestoreInProgress;
@GuardedBy("mPendingRestores")
@@ -633,18 +627,7 @@
private File mJournalDir;
@Nullable private DataChangedJournal mJournal;
- // Backup password, if any, and the file where it's saved. What is stored is not the
- // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but
- // persisted) salt. Validation is performed by running the challenge text through the
- // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches
- // the saved hash string, then the challenge text matches the originally supplied
- // password text.
private final SecureRandom mRng = new SecureRandom();
- private String mPasswordHash;
- private File mPasswordHashFile;
- private int mPasswordVersion;
- private File mPasswordVersionFile;
- private byte[] mPasswordSalt;
// Keep a log of all the apps we've ever backed up, and what the
// dataset tokens are for both the current backup dataset and
@@ -746,52 +729,7 @@
// This dir on /cache is managed directly in init.rc
mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup_stage");
- mPasswordVersion = 1; // unless we hear otherwise
- mPasswordVersionFile = new File(mBaseStateDir, "pwversion");
- if (mPasswordVersionFile.exists()) {
- FileInputStream fin = null;
- DataInputStream in = null;
- try {
- fin = new FileInputStream(mPasswordVersionFile);
- in = new DataInputStream(fin);
- mPasswordVersion = in.readInt();
- } catch (IOException e) {
- Slog.e(TAG, "Unable to read backup pw version");
- } finally {
- try {
- if (in != null) in.close();
- if (fin != null) fin.close();
- } catch (IOException e) {
- Slog.w(TAG, "Error closing pw version files");
- }
- }
- }
-
- mPasswordHashFile = new File(mBaseStateDir, "pwhash");
- if (mPasswordHashFile.exists()) {
- FileInputStream fin = null;
- DataInputStream in = null;
- try {
- fin = new FileInputStream(mPasswordHashFile);
- in = new DataInputStream(new BufferedInputStream(fin));
- // integer length of the salt array, followed by the salt,
- // then the hex pw hash string
- int saltLen = in.readInt();
- byte[] salt = new byte[saltLen];
- in.readFully(salt);
- mPasswordHash = in.readUTF();
- mPasswordSalt = salt;
- } catch (IOException e) {
- Slog.e(TAG, "Unable to read saved backup pw hash");
- } finally {
- try {
- if (in != null) in.close();
- if (fin != null) fin.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to close streams");
- }
- }
- }
+ mBackupPasswordManager = new BackupPasswordManager(mContext, mBaseStateDir, mRng);
// Alarm receivers for scheduled backups & initialization operations
mRunBackupReceiver = new RunBackupReceiver(this);
@@ -1129,128 +1067,18 @@
return array;
}
- private boolean passwordMatchesSaved(String algorithm, String candidatePw, int rounds) {
- if (mPasswordHash == null) {
- // no current password case -- require that 'currentPw' be null or empty
- if (candidatePw == null || "".equals(candidatePw)) {
- return true;
- } // else the non-empty candidate does not match the empty stored pw
- } else {
- // hash the stated current pw and compare to the stored one
- if (candidatePw != null && candidatePw.length() > 0) {
- String currentPwHash = PasswordUtils.buildPasswordHash(algorithm, candidatePw,
- mPasswordSalt,
- rounds);
- if (mPasswordHash.equalsIgnoreCase(currentPwHash)) {
- // candidate hash matches the stored hash -- the password matches
- return true;
- }
- } // else the stored pw is nonempty but the candidate is empty; no match
- }
- return false;
- }
-
@Override
public boolean setBackupPassword(String currentPw, String newPw) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "setBackupPassword");
-
- // When processing v1 passwords we may need to try two different PBKDF2 checksum regimes
- final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
-
- // If the supplied pw doesn't hash to the the saved one, fail. The password
- // might be caught in the legacy crypto mismatch; verify that too.
- if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS)
- && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
- currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS))) {
- return false;
- }
-
- // Snap up to current on the pw file version
- mPasswordVersion = BACKUP_PW_FILE_VERSION;
- FileOutputStream pwFout = null;
- DataOutputStream pwOut = null;
- try {
- pwFout = new FileOutputStream(mPasswordVersionFile);
- pwOut = new DataOutputStream(pwFout);
- pwOut.writeInt(mPasswordVersion);
- } catch (IOException e) {
- Slog.e(TAG, "Unable to write backup pw version; password not changed");
- return false;
- } finally {
- try {
- if (pwOut != null) pwOut.close();
- if (pwFout != null) pwFout.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to close pw version record");
- }
- }
-
- // Clearing the password is okay
- if (newPw == null || newPw.isEmpty()) {
- if (mPasswordHashFile.exists()) {
- if (!mPasswordHashFile.delete()) {
- // Unable to delete the old pw file, so fail
- Slog.e(TAG, "Unable to clear backup password");
- return false;
- }
- }
- mPasswordHash = null;
- mPasswordSalt = null;
- return true;
- }
-
- try {
- // Okay, build the hash of the new backup password
- byte[] salt = randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);
- String newPwHash = PasswordUtils.buildPasswordHash(PBKDF_CURRENT, newPw, salt,
- PasswordUtils.PBKDF2_HASH_ROUNDS);
-
- OutputStream pwf = null, buffer = null;
- DataOutputStream out = null;
- try {
- pwf = new FileOutputStream(mPasswordHashFile);
- buffer = new BufferedOutputStream(pwf);
- out = new DataOutputStream(buffer);
- // integer length of the salt array, followed by the salt,
- // then the hex pw hash string
- out.writeInt(salt.length);
- out.write(salt);
- out.writeUTF(newPwHash);
- out.flush();
- mPasswordHash = newPwHash;
- mPasswordSalt = salt;
- return true;
- } finally {
- if (out != null) out.close();
- if (buffer != null) buffer.close();
- if (pwf != null) pwf.close();
- }
- } catch (IOException e) {
- Slog.e(TAG, "Unable to set backup password");
- }
- return false;
+ return mBackupPasswordManager.setBackupPassword(currentPw, newPw);
}
@Override
public boolean hasBackupPassword() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "hasBackupPassword");
-
- return mPasswordHash != null && mPasswordHash.length() > 0;
+ return mBackupPasswordManager.hasBackupPassword();
}
public boolean backupPasswordMatches(String currentPw) {
- if (hasBackupPassword()) {
- final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
- if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS)
- && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
- currentPw, PasswordUtils.PBKDF2_HASH_ROUNDS))) {
- if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
- return false;
- }
- }
- return true;
+ return mBackupPasswordManager.backupPasswordMatches(currentPw);
}
// Maintain persistent state around whether need to do an initialize operation.
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
index 007d930..804e92c 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
@@ -16,11 +16,11 @@
package com.android.server.backup.fullbackup;
+import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_VERSION;
import static com.android.server.backup.RefactoredBackupManagerService.DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG;
-import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_CURRENT;
import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;
diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
index b1d6afc..62ae065 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -16,6 +16,8 @@
package com.android.server.backup.restore;
+import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
+import static com.android.server.backup.BackupPasswordManager.PBKDF_FALLBACK;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_VERSION;
import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_MANIFEST_FILENAME;
@@ -23,8 +25,6 @@
import static com.android.server.backup.RefactoredBackupManagerService.DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT;
-import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_CURRENT;
-import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_FALLBACK;
import static com.android.server.backup.RefactoredBackupManagerService.SETTINGS_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;
diff --git a/services/backup/java/com/android/server/backup/utils/DataStreamCodec.java b/services/backup/java/com/android/server/backup/utils/DataStreamCodec.java
new file mode 100644
index 0000000..b1e226d
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/utils/DataStreamCodec.java
@@ -0,0 +1,40 @@
+/*
+ * 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.backup.utils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Implements how to serialize a {@code T} to a {@link DataOutputStream} and how to deserialize a
+ * {@code T} from a {@link DataInputStream}.
+ *
+ * @param <T> Type of object to be serialized / deserialized.
+ */
+public interface DataStreamCodec<T> {
+ /**
+ * Serializes {@code t} to {@code dataOutputStream}.
+ */
+ void serialize(T t, DataOutputStream dataOutputStream) throws IOException;
+
+ /**
+ * Deserializes {@code t} from {@code dataInputStream}.
+ */
+ T deserialize(DataInputStream dataInputStream) throws IOException;
+}
+
diff --git a/services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java b/services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java
new file mode 100644
index 0000000..7753b03
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java
@@ -0,0 +1,78 @@
+/*
+ * 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.backup.utils;
+
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Provides an interface for serializing an object to a file and deserializing it back again.
+ *
+ * <p>Serialization logic is implemented as a {@link DataStreamCodec}.
+ *
+ * @param <T> The type of object to serialize / deserialize.
+ */
+public final class DataStreamFileCodec<T> {
+ private final File mFile;
+ private final DataStreamCodec<T> mCodec;
+
+ /**
+ * Constructs an instance to serialize to or deserialize from the given file, with the given
+ * serialization / deserialization strategy.
+ */
+ public DataStreamFileCodec(File file, DataStreamCodec<T> codec) {
+ mFile = file;
+ mCodec = codec;
+ }
+
+ /**
+ * Deserializes a {@code T} from the file, automatically closing input streams.
+ *
+ * @return The deserialized object.
+ * @throws IOException if an IO error occurred.
+ */
+ public T deserialize() throws IOException {
+ try (
+ FileInputStream fileInputStream = new FileInputStream(mFile);
+ DataInputStream dataInputStream = new DataInputStream(fileInputStream)
+ ) {
+ return mCodec.deserialize(dataInputStream);
+ }
+ }
+
+ /**
+ * Serializes {@code t} to the file, automatically flushing and closing output streams.
+ *
+ * @param t The object to serialize.
+ * @throws IOException if an IO error occurs.
+ */
+ public void serialize(T t) throws IOException {
+ try (
+ FileOutputStream fileOutputStream = new FileOutputStream(mFile);
+ BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
+ DataOutputStream dataOutputStream = new DataOutputStream(bufferedOutputStream)
+ ) {
+ mCodec.serialize(t, dataOutputStream);
+ dataOutputStream.flush();
+ }
+ }
+}
diff --git a/services/backup/java/com/android/server/backup/utils/PasswordUtils.java b/services/backup/java/com/android/server/backup/utils/PasswordUtils.java
index 12fc927..9c5e283 100644
--- a/services/backup/java/com/android/server/backup/utils/PasswordUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/PasswordUtils.java
@@ -123,8 +123,7 @@
int rounds) {
try {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
- KeySpec
- ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE);
+ KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE);
return keyFactory.generateSecret(ks);
} catch (InvalidKeySpecException e) {
Slog.e(TAG, "Invalid key spec for PBKDF2!");
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index e82724d..d4abc08 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import android.app.ActivityManager;
import com.android.internal.app.IBatteryStats;
import com.android.server.LocalServices;
import com.android.server.am.BatteryStatsService;
@@ -161,6 +162,9 @@
// True if should use light sensor to automatically determine doze screen brightness.
private final boolean mAllowAutoBrightnessWhileDozingConfig;
+ // Whether or not the color fade on screen on / off is enabled.
+ private final boolean mColorFadeEnabled;
+
// True if we should fade the screen while turning it off, false if we should play
// a stylish color fade animation instead.
private boolean mColorFadeFadesConfig;
@@ -407,6 +411,8 @@
mScreenBrightnessRangeMinimum = screenBrightnessRangeMinimum;
+
+ mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic();
mColorFadeFadesConfig = resources.getBoolean(
com.android.internal.R.bool.config_animateScreenLights);
@@ -497,17 +503,19 @@
// Initialize the power state object for the default display.
// In the future, we might manage multiple displays independently.
mPowerState = new DisplayPowerState(mBlanker,
- new ColorFade(Display.DEFAULT_DISPLAY));
+ mColorFadeEnabled ? new ColorFade(Display.DEFAULT_DISPLAY) : null);
- mColorFadeOnAnimator = ObjectAnimator.ofFloat(
- mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 0.0f, 1.0f);
- mColorFadeOnAnimator.setDuration(COLOR_FADE_ON_ANIMATION_DURATION_MILLIS);
- mColorFadeOnAnimator.addListener(mAnimatorListener);
+ if (mColorFadeEnabled) {
+ mColorFadeOnAnimator = ObjectAnimator.ofFloat(
+ mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 0.0f, 1.0f);
+ mColorFadeOnAnimator.setDuration(COLOR_FADE_ON_ANIMATION_DURATION_MILLIS);
+ mColorFadeOnAnimator.addListener(mAnimatorListener);
- mColorFadeOffAnimator = ObjectAnimator.ofFloat(
- mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 1.0f, 0.0f);
- mColorFadeOffAnimator.setDuration(COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS);
- mColorFadeOffAnimator.addListener(mAnimatorListener);
+ mColorFadeOffAnimator = ObjectAnimator.ofFloat(
+ mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 1.0f, 0.0f);
+ mColorFadeOffAnimator.setDuration(COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS);
+ mColorFadeOffAnimator.addListener(mAnimatorListener);
+ }
mScreenBrightnessRampAnimator = new RampAnimator<DisplayPowerState>(
mPowerState, DisplayPowerState.SCREEN_BRIGHTNESS);
@@ -784,9 +792,9 @@
// Note that we do not wait for the brightness ramp animation to complete before
// reporting the display is ready because we only need to ensure the screen is in the
// right power state even as it continues to converge on the desired brightness.
- final boolean ready = mPendingScreenOnUnblocker == null
- && !mColorFadeOnAnimator.isStarted()
- && !mColorFadeOffAnimator.isStarted()
+ final boolean ready = mPendingScreenOnUnblocker == null &&
+ (!mColorFadeEnabled ||
+ (!mColorFadeOnAnimator.isStarted() && !mColorFadeOffAnimator.isStarted()))
&& mPowerState.waitUntilClean(mCleanListener);
final boolean finished = ready
&& !mScreenBrightnessRampAnimator.isAnimating();
@@ -959,8 +967,8 @@
private void animateScreenStateChange(int target, boolean performScreenOffTransition) {
// If there is already an animation in progress, don't interfere with it.
- if (mColorFadeOnAnimator.isStarted()
- || mColorFadeOffAnimator.isStarted()) {
+ if (mColorFadeEnabled &&
+ (mColorFadeOnAnimator.isStarted() || mColorFadeOffAnimator.isStarted())) {
if (target != Display.STATE_ON) {
return;
}
@@ -984,7 +992,7 @@
if (!setScreenState(Display.STATE_ON)) {
return; // screen on blocked
}
- if (USE_COLOR_FADE_ON_ANIMATION && mPowerRequest.isBrightOrDim()) {
+ if (USE_COLOR_FADE_ON_ANIMATION && mColorFadeEnabled && mPowerRequest.isBrightOrDim()) {
// Perform screen on animation.
if (mPowerState.getColorFadeLevel() == 1.0f) {
mPowerState.dismissColorFade();
@@ -1060,6 +1068,10 @@
} else {
// Want screen off.
mPendingScreenOff = true;
+ if (!mColorFadeEnabled) {
+ mPowerState.setColorFadeLevel(0.0f);
+ }
+
if (mPowerState.getColorFadeLevel() == 0.0f) {
// Turn the screen off.
// A black surface is already hiding the contents of the screen.
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index e2fd0ac..d0c1580 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -174,7 +174,7 @@
* @return True if the electron beam was prepared.
*/
public boolean prepareColorFade(Context context, int mode) {
- if (!mColorFade.prepare(context, mode)) {
+ if (mColorFade == null || !mColorFade.prepare(context, mode)) {
mColorFadePrepared = false;
mColorFadeReady = true;
return false;
@@ -190,7 +190,7 @@
* Dismisses the color fade surface.
*/
public void dismissColorFade() {
- mColorFade.dismiss();
+ if (mColorFade != null) mColorFade.dismiss();
mColorFadePrepared = false;
mColorFadeReady = true;
}
@@ -199,7 +199,7 @@
* Dismisses the color fade resources.
*/
public void dismissColorFadeResources() {
- mColorFade.dismissResources();
+ if (mColorFade != null) mColorFade.dismissResources();
}
/**
@@ -269,7 +269,7 @@
pw.println(" mColorFadeDrawPending=" + mColorFadeDrawPending);
mPhotonicModulator.dump(pw);
- mColorFade.dump(pw);
+ if (mColorFade != null) mColorFade.dump(pw);
}
private void scheduleScreenUpdate() {
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index cbaad46..a7a2743 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -104,9 +104,6 @@
}
public boolean shouldIntercept(int zen, ZenModeConfig config, NotificationRecord record) {
- if (isSystem(record)) {
- return false;
- }
switch (zen) {
case Global.ZEN_MODE_NO_INTERRUPTIONS:
// #notevenalarms
@@ -177,10 +174,6 @@
return false;
}
- private static boolean isSystem(NotificationRecord record) {
- return record.isCategory(Notification.CATEGORY_SYSTEM);
- }
-
private static boolean isAlarm(NotificationRecord record) {
return record.isCategory(Notification.CATEGORY_ALARM)
|| record.isAudioStream(AudioManager.STREAM_ALARM)
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java b/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java
index 3007cb1..f457f6a 100644
--- a/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java
+++ b/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java
@@ -25,8 +25,14 @@
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
+import com.android.internal.util.FastXmlSerializer;
+
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -50,4 +56,15 @@
channel.setBlockableSystem(true);
assertEquals(true, channel.isBlockableSystem());
}
+
+ @Test
+ public void testEmptyVibration_noException() throws Exception {
+ NotificationChannel channel = new NotificationChannel("a", "ab", IMPORTANCE_DEFAULT);
+ channel.setVibrationPattern(new long[0]);
+
+ XmlSerializer serializer = new FastXmlSerializer();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+ channel.writeXml(serializer);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java b/services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java
new file mode 100644
index 0000000..04c0251
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.backup;
+
+import static com.android.server.testutis.TestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.backup.utils.PasswordUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.security.SecureRandom;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BackupPasswordManagerTest {
+ private static final String PASSWORD_VERSION_FILE_NAME = "pwversion";
+ private static final String PASSWORD_HASH_FILE_NAME = "pwhash";
+ private static final String V1_HASH_ALGORITHM = "PBKDF2WithHmacSHA1And8bit";
+
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Mock private Context mContext;
+
+ private File mStateFolder;
+ private BackupPasswordManager mPasswordManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mStateFolder = mTemporaryFolder.newFolder();
+ mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom());
+ }
+
+ @Test
+ public void hasBackupPassword_isFalseIfFileDoesNotExist() {
+ assertThat(mPasswordManager.hasBackupPassword()).isFalse();
+ }
+
+ @Test
+ public void hasBackupPassword_isTrueIfFileExists() throws Exception {
+ mPasswordManager.setBackupPassword(null, "password1234");
+ assertThat(mPasswordManager.hasBackupPassword()).isTrue();
+ }
+
+ @Test
+ public void hasBackupPassword_throwsSecurityExceptionIfLacksPermission() {
+ setDoesNotHavePermission();
+
+ assertExpectException(
+ SecurityException.class,
+ /* expectedExceptionMessageRegex */ null,
+ () -> mPasswordManager.hasBackupPassword());
+ }
+
+ @Test
+ public void backupPasswordMatches_isTrueIfNoPassword() {
+ assertThat(mPasswordManager.backupPasswordMatches("anything")).isTrue();
+ }
+
+ @Test
+ public void backupPasswordMatches_isTrueForSamePassword() {
+ String password = "password1234";
+ mPasswordManager.setBackupPassword(null, password);
+ assertThat(mPasswordManager.backupPasswordMatches(password)).isTrue();
+ }
+
+ @Test
+ public void backupPasswordMatches_isFalseForDifferentPassword() {
+ mPasswordManager.setBackupPassword(null, "shiba");
+ assertThat(mPasswordManager.backupPasswordMatches("corgi")).isFalse();
+ }
+
+ @Test
+ public void backupPasswordMatches_worksForV1HashIfVersionIsV1() throws Exception {
+ String password = "corgi\uFFFF";
+ writePasswordVersionToFile(1);
+ writeV1HashToFile(password, saltFixture());
+
+ // Reconstruct so it reloads from filesystem
+ mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom());
+
+ assertThat(mPasswordManager.backupPasswordMatches(password)).isTrue();
+ }
+
+ @Test
+ public void backupPasswordMatches_failsForV1HashIfVersionIsV2() throws Exception {
+ // The algorithms produce identical hashes except if the password contains higher-order
+ // unicode. See
+ // https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html
+ String password = "corgi\uFFFF";
+ writePasswordVersionToFile(2);
+ writeV1HashToFile(password, saltFixture());
+
+ // Reconstruct so it reloads from filesystem
+ mPasswordManager = new BackupPasswordManager(mContext, mStateFolder, new SecureRandom());
+
+ assertThat(mPasswordManager.backupPasswordMatches(password)).isFalse();
+ }
+
+ @Test
+ public void backupPasswordMatches_throwsSecurityExceptionIfLacksPermission() {
+ setDoesNotHavePermission();
+
+ assertExpectException(
+ SecurityException.class,
+ /* expectedExceptionMessageRegex */ null,
+ () -> mPasswordManager.backupPasswordMatches("password123"));
+ }
+
+ @Test
+ public void setBackupPassword_persistsPasswordToFile() {
+ String password = "shiba";
+
+ mPasswordManager.setBackupPassword(null, password);
+
+ BackupPasswordManager newManager = new BackupPasswordManager(
+ mContext, mStateFolder, new SecureRandom());
+ assertThat(newManager.backupPasswordMatches(password)).isTrue();
+ }
+
+ @Test
+ public void setBackupPassword_failsIfCurrentPasswordIsWrong() {
+ String secondPassword = "second password";
+ mPasswordManager.setBackupPassword(null, "first password");
+
+ boolean result = mPasswordManager.setBackupPassword(
+ "incorrect pass", secondPassword);
+
+ BackupPasswordManager newManager = new BackupPasswordManager(
+ mContext, mStateFolder, new SecureRandom());
+ assertThat(result).isFalse();
+ assertThat(newManager.backupPasswordMatches(secondPassword)).isFalse();
+ }
+
+ @Test
+ public void setBackupPassword_throwsSecurityExceptionIfLacksPermission() {
+ setDoesNotHavePermission();
+
+ assertExpectException(
+ SecurityException.class,
+ /* expectedExceptionMessageRegex */ null,
+ () -> mPasswordManager.setBackupPassword(
+ "password123", "password111"));
+ }
+
+ private byte[] saltFixture() {
+ byte[] bytes = new byte[64];
+ for (int i = 0; i < 64; i++) {
+ bytes[i] = (byte) i;
+ }
+ return bytes;
+ }
+
+ private void setDoesNotHavePermission() {
+ doThrow(new SecurityException()).when(mContext)
+ .enforceCallingOrSelfPermission(anyString(), anyString());
+ }
+
+ private void writeV1HashToFile(String password, byte[] salt) throws Exception {
+ String hash = PasswordUtils.buildPasswordHash(
+ V1_HASH_ALGORITHM, password, salt, PasswordUtils.PBKDF2_HASH_ROUNDS);
+ writeHashAndSaltToFile(hash, salt);
+ }
+
+ private void writeHashAndSaltToFile(String hash, byte[] salt) throws Exception {
+ FileOutputStream fos = null;
+ DataOutputStream dos = null;
+
+ try {
+ File passwordHash = new File(mStateFolder, PASSWORD_HASH_FILE_NAME);
+ fos = new FileOutputStream(passwordHash);
+ dos = new DataOutputStream(fos);
+ dos.writeInt(salt.length);
+ dos.write(salt);
+ dos.writeUTF(hash);
+ dos.flush();
+ } finally {
+ if (dos != null) dos.close();
+ if (fos != null) fos.close();
+ }
+ }
+
+ private void writePasswordVersionToFile(int version) throws Exception {
+ FileOutputStream fos = null;
+ DataOutputStream dos = null;
+
+ try {
+ File passwordVersion = new File(mStateFolder, PASSWORD_VERSION_FILE_NAME);
+ fos = new FileOutputStream(passwordVersion);
+ dos = new DataOutputStream(fos);
+ dos.writeInt(version);
+ dos.flush();
+ } finally {
+ if (dos != null) dos.close();
+ if (fos != null) fos.close();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java b/services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java
new file mode 100644
index 0000000..bfb95c1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.backup.utils;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public final class DataStreamFileCodecTest {
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void serialize_writesToTheFile() throws Exception {
+ File unicornFile = mTemporaryFolder.newFile();
+
+ DataStreamFileCodec<MythicalCreature> mythicalCreatureCodec = new DataStreamFileCodec<>(
+ unicornFile, new MythicalCreatureDataStreamCodec());
+ MythicalCreature unicorn = new MythicalCreature(
+ 10000, "Unicorn");
+ mythicalCreatureCodec.serialize(unicorn);
+
+ DataStreamFileCodec<MythicalCreature> newCodecWithSameFile = new DataStreamFileCodec<>(
+ unicornFile, new MythicalCreatureDataStreamCodec());
+ MythicalCreature deserializedUnicorn = newCodecWithSameFile.deserialize();
+
+ assertThat(deserializedUnicorn.averageLifespanInYears)
+ .isEqualTo(unicorn.averageLifespanInYears);
+ assertThat(deserializedUnicorn.name).isEqualTo(unicorn.name);
+ }
+
+ private static class MythicalCreature {
+ int averageLifespanInYears;
+ String name;
+
+ MythicalCreature(int averageLifespanInYears, String name) {
+ this.averageLifespanInYears = averageLifespanInYears;
+ this.name = name;
+ }
+ }
+
+ private static class MythicalCreatureDataStreamCodec implements
+ DataStreamCodec<MythicalCreature> {
+ @Override
+ public void serialize(MythicalCreature mythicalCreature,
+ DataOutputStream dataOutputStream) throws IOException {
+ dataOutputStream.writeInt(mythicalCreature.averageLifespanInYears);
+ dataOutputStream.writeUTF(mythicalCreature.name);
+ }
+
+ @Override
+ public MythicalCreature deserialize(DataInputStream dataInputStream)
+ throws IOException {
+ int years = dataInputStream.readInt();
+ String name = dataInputStream.readUTF();
+ return new MythicalCreature(years, name);
+ }
+ }
+}