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);
+        }
+    }
+}