summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/NotificationChannel.java12
-rw-r--r--core/java/android/widget/Editor.java6
-rw-r--r--core/java/android/widget/TextView.java6
-rw-r--r--core/java/com/android/internal/view/FloatingActionMode.java13
-rw-r--r--legacy-test/jarjar-rules.txt1
-rw-r--r--services/backup/java/com/android/server/backup/BackupPasswordManager.java307
-rw-r--r--services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java184
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java2
-rw-r--r--services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java4
-rw-r--r--services/backup/java/com/android/server/backup/utils/DataStreamCodec.java40
-rw-r--r--services/backup/java/com/android/server/backup/utils/DataStreamFileCodec.java78
-rw-r--r--services/backup/java/com/android/server/backup/utils/PasswordUtils.java3
-rw-r--r--services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java17
-rw-r--r--services/tests/servicestests/src/com/android/server/backup/BackupPasswordManagerTest.java232
-rw-r--r--services/tests/servicestests/src/com/android/server/backup/utils/DataStreamFileCodecTest.java86
15 files changed, 798 insertions, 193 deletions
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 143d147ab4e7..d6e36914ac6c 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.provider.Settings;
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 @@ public final class NotificationChannel implements Parcelable {
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 f21545fe8636..04a826514a83 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -1387,7 +1387,7 @@ public class Editor {
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 @@ public class Editor {
}
}
- 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 9a924890fcd7..d277616b0f4f 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -374,6 +374,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
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 ff211b6c2d62..497e7b08d881 100644
--- a/core/java/com/android/internal/view/FloatingActionMode.java
+++ b/core/java/com/android/internal/view/FloatingActionMode.java
@@ -295,6 +295,8 @@ public final class FloatingActionMode extends ActionMode {
*/
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 @@ public final class FloatingActionMode extends ActionMode {
private boolean mActive;
+ private long mLastShowTime;
+
public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
mToolbar = Preconditions.checkNotNull(toolbar);
}
@@ -327,7 +331,13 @@ public final class FloatingActionMode extends ActionMode {
}
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 @@ public final class FloatingActionMode extends ActionMode {
mToolbar.hide();
} else {
mToolbar.show();
+ mLastShowTime = System.currentTimeMillis();
}
}
}
diff --git a/legacy-test/jarjar-rules.txt b/legacy-test/jarjar-rules.txt
index 9077e6f8ba26..fd8555c8931c 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/services/backup/java/com/android/server/backup/BackupPasswordManager.java b/services/backup/java/com/android/server/backup/BackupPasswordManager.java
new file mode 100644
index 000000000000..ee7651b0a087
--- /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 7e28f610e565..674e9725cc8a 100644
--- a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
@@ -117,13 +117,11 @@ import com.android.server.backup.restore.PerformUnifiedRestoreTask;
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;
@@ -135,7 +133,6 @@ import java.io.FileNotFoundException;
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;
@@ -169,10 +166,6 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
// 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:
@@ -190,7 +183,6 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
// 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;
@@ -283,6 +275,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
private final Object mClearDataLock = new Object();
private volatile boolean mClearingData;
+ private final BackupPasswordManager mBackupPasswordManager;
+
@GuardedBy("mPendingRestores")
private boolean mIsRestoreInProgress;
@GuardedBy("mPendingRestores")
@@ -632,18 +626,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
private File mJournalDir;
private File 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
@@ -745,52 +728,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
// 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);
@@ -1146,128 +1084,18 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
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 007d9309c188..804e92c88eb7 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 b1d6afcbd63c..62ae065be1ac 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.BACKUP_ME
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 000000000000..b1e226d5999c
--- /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 000000000000..7753b0370279
--- /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 12fc927315c2..9c5e28393a53 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 @@ public class PasswordUtils {
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/tests/notification/src/com/android/server/notification/NotificationChannelTest.java b/services/tests/notification/src/com/android/server/notification/NotificationChannelTest.java
index 3007cb1755e2..f457f6a550c1 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.os.Parcel;
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 @@ public class NotificationChannelTest extends NotificationTestCase {
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 000000000000..04c02510cb3d
--- /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 000000000000..bfb95c1a3eda
--- /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);
+ }
+ }
+}