diff options
9 files changed, 1080 insertions, 338 deletions
diff --git a/services/backup/java/com/android/server/backup/keyvalue/AgentException.java b/services/backup/java/com/android/server/backup/keyvalue/AgentException.java new file mode 100644 index 000000000000..e2ca35116bdc --- /dev/null +++ b/services/backup/java/com/android/server/backup/keyvalue/AgentException.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 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.keyvalue; + +/** + * This represents something wrong with a specific package. For example: + * <ul> + * <li>Package unknown. + * <li>Package is not eligible for backup anymore. + * <li>Backup agent timed out. + * <li>Backup agent wrote protected keys. + * <li>... + * </ul> + * + * @see KeyValueBackupTask + * @see TaskException + */ +class AgentException extends BackupException { + static AgentException transitory() { + return new AgentException(/* transitory */ true); + } + + static AgentException transitory(Exception cause) { + return new AgentException(/* transitory */ true, cause); + } + + static AgentException permanent() { + return new AgentException(/* transitory */ false); + } + + static AgentException permanent(Exception cause) { + return new AgentException(/* transitory */ false, cause); + } + + private final boolean mTransitory; + + private AgentException(boolean transitory) { + mTransitory = transitory; + } + + private AgentException(boolean transitory, Exception cause) { + super(cause); + mTransitory = transitory; + } + + boolean isTransitory() { + return mTransitory; + } +} diff --git a/services/backup/java/com/android/server/backup/keyvalue/BackupException.java b/services/backup/java/com/android/server/backup/keyvalue/BackupException.java new file mode 100644 index 000000000000..27b2d35b13ca --- /dev/null +++ b/services/backup/java/com/android/server/backup/keyvalue/BackupException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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.keyvalue; + +import android.util.AndroidException; + +/** + * Key-value backup task exception. + * + * @see AgentException + * @see TaskException + */ +class BackupException extends AndroidException { + BackupException() {} + + BackupException(Exception cause) { + super(cause); + } +} diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java index a92070cfc754..bb8a1d1339a7 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java @@ -153,16 +153,18 @@ public class KeyValueBackupReporter { mObserver, packageName, BackupManager.ERROR_BACKUP_NOT_ALLOWED); } - void onBindAgentError(SecurityException e) { - Slog.d(TAG, "Error in bind/backup", e); - } - void onAgentUnknown(String packageName) { Slog.d(TAG, "Package does not exist, skipping"); BackupObserverUtils.sendBackupOnPackageResult( mObserver, packageName, BackupManager.ERROR_PACKAGE_NOT_FOUND); } + void onBindAgentError(String packageName, SecurityException e) { + Slog.d(TAG, "Error in bind/backup", e); + BackupObserverUtils.sendBackupOnPackageResult( + mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE); + } + void onAgentError(String packageName) { if (MORE_DEBUG) { Slog.i(TAG, "Agent failure for " + packageName + ", re-staging"); @@ -190,6 +192,8 @@ public class KeyValueBackupReporter { void onCallAgentDoBackupError(String packageName, boolean callingAgent, Exception e) { if (callingAgent) { Slog.e(TAG, "Error invoking agent on " + packageName + ": " + e); + BackupObserverUtils.sendBackupOnPackageResult( + mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE); } else { Slog.e(TAG, "Error before invoking agent on " + packageName + ": " + e); } @@ -220,12 +224,8 @@ public class KeyValueBackupReporter { } } - void onReadAgentDataError(String packageName, IOException e) { - Slog.w(TAG, "Unable read backup data for " + packageName + ": " + e); - } - - void onWriteWidgetDataError(String packageName, IOException e) { - Slog.w(TAG, "Unable to save widget data for " + packageName + ": " + e); + void onAgentDataError(String packageName, IOException e) { + Slog.w(TAG, "Unable to read/write agent data for " + packageName + ": " + e); } void onDigestError(NoSuchAlgorithmException e) { @@ -243,16 +243,12 @@ public class KeyValueBackupReporter { } } - void onSendDataToTransport(String packageName) { + void onTransportPerformBackup(String packageName) { if (MORE_DEBUG) { Slog.v(TAG, "Sending non-empty data to transport for " + packageName); } } - void onNonIncrementalAndNonIncrementalRequired() { - Slog.e(TAG, "Transport requested non-incremental but already the case"); - } - void onEmptyData(PackageInfo packageInfo) { if (MORE_DEBUG) { Slog.i(TAG, "No backup data written, not calling transport"); @@ -302,13 +298,20 @@ public class KeyValueBackupReporter { /* extras */ null); } + void onPackageBackupNonIncrementalAndNonIncrementalRequired(String packageName) { + Slog.e(TAG, "Transport requested non-incremental but already the case"); + BackupObserverUtils.sendBackupOnPackageResult( + mObserver, packageName, BackupManager.ERROR_TRANSPORT_ABORTED); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, packageName); + } + void onPackageBackupTransportFailure(String packageName) { BackupObserverUtils.sendBackupOnPackageResult( mObserver, packageName, BackupManager.ERROR_TRANSPORT_ABORTED); EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, packageName); } - void onPackageBackupError(String packageName, Exception e) { + void onPackageBackupTransportError(String packageName, Exception e) { Slog.e(TAG, "Transport error backing up " + packageName, e); BackupObserverUtils.sendBackupOnPackageResult( mObserver, packageName, BackupManager.ERROR_TRANSPORT_ABORTED); diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index e915ce16a2ef..6904b3fc6b9c 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -16,6 +16,7 @@ package com.android.server.backup.keyvalue; +import static android.app.ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL; import static android.os.ParcelFileDescriptor.MODE_CREATE; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; import static android.os.ParcelFileDescriptor.MODE_READ_WRITE; @@ -25,8 +26,8 @@ import static com.android.server.backup.BackupManagerService.KEY_WIDGET_STATE; import static com.android.server.backup.BackupManagerService.OP_PENDING; import static com.android.server.backup.BackupManagerService.OP_TYPE_BACKUP; +import android.annotation.IntDef; import android.annotation.Nullable; -import android.app.ApplicationThreadConstants; import android.app.IBackupAgent; import android.app.backup.BackupAgent; import android.app.backup.BackupDataInput; @@ -47,7 +48,6 @@ import android.os.RemoteException; import android.os.SELinux; import android.os.UserHandle; import android.os.WorkSource; -import android.util.Pair; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -77,6 +77,8 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -173,10 +175,8 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final String BLANK_STATE_FILE_NAME = "blank_state"; private static final String PM_PACKAGE = BackupManagerService.PACKAGE_MANAGER_SENTINEL; - @VisibleForTesting - public static final String STAGING_FILE_SUFFIX = ".data"; - @VisibleForTesting - public static final String NEW_STATE_FILE_SUFFIX = ".new"; + @VisibleForTesting public static final String STAGING_FILE_SUFFIX = ".data"; + @VisibleForTesting public static final String NEW_STATE_FILE_SUFFIX = ".new"; /** * Creates a new {@link KeyValueBackupTask} for key-value backup operation, spins up a new @@ -244,13 +244,13 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { private final int mCurrentOpToken; private final File mStateDirectory; private final File mDataDirectory; + private final File mBlankStateFile; private final List<String> mOriginalQueue; private final List<String> mQueue; private final List<String> mPendingFullBackups; private final Object mQueueLock; @Nullable private final DataChangedJournal mJournal; - private int mStatus; @Nullable private PerformFullTransportBackupTask mFullBackupTask; @Nullable private IBackupAgent mAgent; @Nullable private PackageInfo mCurrentPackage; @@ -316,6 +316,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mDataDirectory = mBackupManagerService.getDataDir(); mCurrentOpToken = backupManagerService.generateRandomIntegerToken(); mQueueLock = mBackupManagerService.getQueueLock(); + mBlankStateFile = new File(mStateDirectory, BLANK_STATE_FILE_NAME); } private void registerTask() { @@ -331,45 +332,43 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { public void run() { Process.setThreadPriority(THREAD_PRIORITY); - boolean processQueue = startTask(); - while (processQueue && !mQueue.isEmpty() && !mCancelled) { - String packageName = mQueue.remove(0); - if (PM_PACKAGE.equals(packageName)) { - processQueue = backupPm(); - } else { - processQueue = backupPackage(packageName); + int status = BackupTransport.TRANSPORT_OK; + try { + startTask(); + while (!mQueue.isEmpty() && !mCancelled) { + String packageName = mQueue.remove(0); + try { + if (PM_PACKAGE.equals(packageName)) { + backupPm(); + } else { + backupPackage(packageName); + } + } catch (AgentException e) { + if (e.isTransitory()) { + // We try again this package in the next backup pass. + mBackupManagerService.dataChangedImpl(packageName); + } + } } + } catch (TaskException e) { + if (e.isStateCompromised()) { + mBackupManagerService.resetBackupState(mStateDirectory); + } + revertTask(); + status = e.getStatus(); } - finishTask(); + finishTask(status); } - /** Returns whether to consume next queue package. */ - private boolean handleAgentResult(@Nullable PackageInfo packageInfo, RemoteResult result) { - if (result == RemoteResult.FAILED_THREAD_INTERRUPTED) { - // Not an explicit cancel, we need to flag it. - mCancelled = true; - mReporter.onAgentCancelled(packageInfo); - cleanUpAgentForAgentError(); - return false; - } - if (result == RemoteResult.FAILED_CANCELLED) { - mReporter.onAgentCancelled(packageInfo); - cleanUpAgentForAgentError(); - return false; - } - if (result == RemoteResult.FAILED_TIMED_OUT) { - mReporter.onAgentTimedOut(packageInfo); - cleanUpAgentForAgentError(); - return true; - } - Preconditions.checkState(result.isPresent()); - long agentResult = result.get(); - if (agentResult == BackupAgent.RESULT_ERROR) { - mReporter.onAgentResultError(packageInfo); - cleanUpAgentForAgentError(); - return true; + /** Returns transport status. */ + private int sendDataToTransport(@Nullable PackageInfo packageInfo) + throws AgentException, TaskException { + try { + return sendDataToTransport(); + } catch (IOException e) { + mReporter.onAgentDataError(packageInfo.packageName, e); + throw TaskException.causedBy(e); } - return sendDataToTransport(); } @Override @@ -378,11 +377,10 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { @Override public void operationComplete(long unusedResult) {} - /** Returns whether to consume next queue package. */ - private boolean startTask() { + private void startTask() throws TaskException { if (mBackupManagerService.isBackupOperationInProgress()) { mReporter.onSkipBackup(); - return false; + throw TaskException.create(); } // Unfortunately full backup task constructor registers the task with BMS, so we have to @@ -390,11 +388,9 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mFullBackupTask = createFullBackupTask(mPendingFullBackups); registerTask(); - mStatus = BackupTransport.TRANSPORT_OK; - if (mQueue.isEmpty() && mPendingFullBackups.isEmpty()) { mReporter.onEmptyQueueAtStart(); - return false; + return; } // We only backup PM if it was explicitly in the queue or if it's incremental. boolean backupPm = mQueue.remove(PM_PACKAGE) || !mNonIncremental; @@ -415,20 +411,18 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { if (pmState.length() <= 0) { mReporter.onInitializeTransport(transportName); mBackupManagerService.resetBackupState(mStateDirectory); - mStatus = transport.initializeDevice(); - mReporter.onTransportInitialized(mStatus); + int status = transport.initializeDevice(); + mReporter.onTransportInitialized(status); + if (status != BackupTransport.TRANSPORT_OK) { + throw TaskException.stateCompromised(); + } } + } catch (TaskException e) { + throw e; } catch (Exception e) { mReporter.onInitializeTransportError(e); - mStatus = BackupTransport.TRANSPORT_ERROR; - } - - if (mStatus != BackupTransport.TRANSPORT_OK) { - mBackupManagerService.resetBackupState(mStateDirectory); - return false; + throw TaskException.stateCompromised(); } - - return true; } private PerformFullTransportBackupTask createFullBackupTask(List<String> packages) { @@ -446,120 +440,82 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mUserInitiated); } - /** Returns whether to consume next queue package. */ - private boolean backupPm() { - RemoteResult agentResult = null; - try { - mCurrentPackage = new PackageInfo(); - mCurrentPackage.packageName = PM_PACKAGE; - - // Since PM is running in the system process we can set up its agent directly. - BackupAgent pmAgent = mBackupManagerService.makeMetadataAgent(); - mAgent = IBackupAgent.Stub.asInterface(pmAgent.onBind()); + private void backupPm() throws TaskException { + mReporter.onStartPackageBackup(PM_PACKAGE); + mCurrentPackage = new PackageInfo(); + mCurrentPackage.packageName = PM_PACKAGE; - Pair<Integer, RemoteResult> statusAndResult = extractAgentData(PM_PACKAGE, mAgent); - mStatus = statusAndResult.first; - agentResult = statusAndResult.second; - } catch (Exception e) { + try { + extractPmAgentData(mCurrentPackage); + int status = sendDataToTransport(mCurrentPackage); + cleanUpAgentForTransportStatus(status); + } catch (AgentException | TaskException e) { mReporter.onExtractPmAgentDataError(e); - mStatus = BackupTransport.TRANSPORT_ERROR; - } - - if (mStatus != BackupTransport.TRANSPORT_OK) { - // In this case either extractAgentData() already made the agent clean-up or we haven't - // prepared the state for calling the agent, in either case we don't need to clean-up. - mBackupManagerService.resetBackupState(mStateDirectory); - return false; + cleanUpAgentForError(e); + // PM agent failure is task failure. + throw TaskException.stateCompromised(e); } - - Preconditions.checkNotNull(agentResult); - return handleAgentResult(mCurrentPackage, agentResult); } - /** Returns whether to consume next queue package. */ - private boolean backupPackage(String packageName) { + private void backupPackage(String packageName) throws AgentException, TaskException { mReporter.onStartPackageBackup(packageName); - mStatus = BackupTransport.TRANSPORT_OK; + mCurrentPackage = getPackageForBackup(packageName); - // Verify that the requested app is eligible for key-value backup. - RemoteResult agentResult = null; try { - mCurrentPackage = mPackageManager.getPackageInfo( - packageName, PackageManager.GET_SIGNING_CERTIFICATES); - ApplicationInfo applicationInfo = mCurrentPackage.applicationInfo; - if (!AppBackupUtils.appIsEligibleForBackup(applicationInfo, mPackageManager)) { - // The manifest has changed. This won't happen again because the app won't be - // requesting further backups. - mReporter.onPackageNotEligibleForBackup(packageName); - return true; - } - - if (AppBackupUtils.appGetsFullBackup(mCurrentPackage)) { - // Initially enqueued for key-value backup, but only supports full-backup now. - mReporter.onPackageEligibleForFullBackup(packageName); - return true; - } - - if (AppBackupUtils.appIsStopped(applicationInfo)) { - // Just as it won't receive broadcasts, we won't run it for backup. - mReporter.onPackageStopped(packageName); - return true; - } + extractAgentData(mCurrentPackage); + int status = sendDataToTransport(mCurrentPackage); + cleanUpAgentForTransportStatus(status); + } catch (AgentException | TaskException e) { + cleanUpAgentForError(e); + throw e; + } + } - try { - mBackupManagerService.setWorkSource(new WorkSource(applicationInfo.uid)); - IBackupAgent agent = - mBackupManagerService.bindToAgentSynchronous( - applicationInfo, - ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL); - if (agent != null) { - mAgent = agent; - Pair<Integer, RemoteResult> statusAndResult = - extractAgentData(packageName, agent); - mStatus = statusAndResult.first; - agentResult = statusAndResult.second; - } else { - // Timeout waiting for the agent to bind. - mStatus = BackupTransport.AGENT_ERROR; - } - } catch (SecurityException e) { - mReporter.onBindAgentError(e); - mStatus = BackupTransport.AGENT_ERROR; - } + private PackageInfo getPackageForBackup(String packageName) throws AgentException { + final PackageInfo packageInfo; + try { + packageInfo = + mPackageManager.getPackageInfo( + packageName, PackageManager.GET_SIGNING_CERTIFICATES); } catch (PackageManager.NameNotFoundException e) { - mStatus = BackupTransport.AGENT_UNKNOWN; - } finally { - mBackupManagerService.setWorkSource(null); + mReporter.onAgentUnknown(packageName); + throw AgentException.permanent(e); } + ApplicationInfo applicationInfo = packageInfo.applicationInfo; + if (!AppBackupUtils.appIsEligibleForBackup(applicationInfo, mPackageManager)) { + mReporter.onPackageNotEligibleForBackup(packageName); + throw AgentException.permanent(); + } + if (AppBackupUtils.appGetsFullBackup(packageInfo)) { + mReporter.onPackageEligibleForFullBackup(packageName); + throw AgentException.permanent(); + } + if (AppBackupUtils.appIsStopped(applicationInfo)) { + mReporter.onPackageStopped(packageName); + throw AgentException.permanent(); + } + return packageInfo; + } - if (mStatus != BackupTransport.TRANSPORT_OK) { - // In this case either extractAgentData() already made the agent clean-up or we haven't - // prepared the state for calling the agent, in either case we don't need to clean-up. - Preconditions.checkState(mAgent == null); - - if (mStatus == BackupTransport.AGENT_ERROR) { + private IBackupAgent bindAgent(PackageInfo packageInfo) throws AgentException { + String packageName = packageInfo.packageName; + final IBackupAgent agent; + try { + agent = + mBackupManagerService.bindToAgentSynchronous( + packageInfo.applicationInfo, BACKUP_MODE_INCREMENTAL); + if (agent == null) { mReporter.onAgentError(packageName); - mBackupManagerService.dataChangedImpl(packageName); - mStatus = BackupTransport.TRANSPORT_OK; - return true; + throw AgentException.transitory(); } - - if (mStatus == BackupTransport.AGENT_UNKNOWN) { - mReporter.onAgentUnknown(packageName); - mStatus = BackupTransport.TRANSPORT_OK; - return true; - } - - // Transport-level failure, re-enqueue everything. - revertTask(); - return false; + } catch (SecurityException e) { + mReporter.onBindAgentError(packageName, e); + throw AgentException.transitory(e); } - - Preconditions.checkNotNull(agentResult); - return handleAgentResult(mCurrentPackage, agentResult); + return agent; } - private void finishTask() { + private void finishTask(int status) { // Mark packages that we couldn't backup as pending backup. for (String packageName : mQueue) { mBackupManagerService.dataChangedImpl(packageName); @@ -576,7 +532,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { // If we succeeded and this is the first time we've done a backup, we can record the current // backup dataset token. long currentToken = mBackupManagerService.getCurrentToken(); - if ((mStatus == BackupTransport.TRANSPORT_OK) && (currentToken == 0)) { + if ((status == BackupTransport.TRANSPORT_OK) && (currentToken == 0)) { try { IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString); mBackupManagerService.setCurrentToken(transport.getCurrentRestoreSet()); @@ -589,9 +545,14 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { synchronized (mQueueLock) { mBackupManagerService.setBackupRunning(false); - if (mStatus == BackupTransport.TRANSPORT_NOT_INITIALIZED) { + if (status == BackupTransport.TRANSPORT_NOT_INITIALIZED) { mReporter.onTransportNotInitialized(); - triggerTransportInitializationLocked(); + try { + triggerTransportInitializationLocked(); + } catch (Exception e) { + mReporter.onPendingInitializeTransportError(e); + status = BackupTransport.TRANSPORT_ERROR; + } } } @@ -605,7 +566,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } if (!mCancelled - && mStatus == BackupTransport.TRANSPORT_OK + && status == BackupTransport.TRANSPORT_OK && mFullBackupTask != null && !mPendingFullBackups.isEmpty()) { mReporter.onStartFullBackup(mPendingFullBackups); @@ -621,7 +582,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mFullBackupTask.unregisterTask(); } mTaskFinishedListener.onFinished(callerLogString); - mReporter.onBackupFinished(getBackupFinishedStatus(mCancelled, mStatus)); + mReporter.onBackupFinished(getBackupFinishedStatus(mCancelled, status)); mBackupManagerService.getWakelock().release(); } @@ -642,17 +603,12 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } @GuardedBy("mQueueLock") - private void triggerTransportInitializationLocked() { - try { - IBackupTransport transport = - mTransportClient.connectOrThrow("KVBT.triggerTransportInitializationLocked"); - mBackupManagerService.getPendingInits().add(transport.name()); - deletePmStateFile(); - mBackupManagerService.backupNow(); - } catch (Exception e) { - mReporter.onPendingInitializeTransportError(e); - mStatus = BackupTransport.TRANSPORT_ERROR; - } + private void triggerTransportInitializationLocked() throws Exception { + IBackupTransport transport = + mTransportClient.connectOrThrow("KVBT.triggerTransportInitializationLocked"); + mBackupManagerService.getPendingInits().add(transport.name()); + deletePmStateFile(); + mBackupManagerService.backupNow(); } /** Removes PM state, triggering initialization in the next key-value task. */ @@ -660,35 +616,69 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { new File(mStateDirectory, PM_PACKAGE).delete(); } + /** Same as {@link #extractAgentData(PackageInfo)}, but only for PM package. */ + private void extractPmAgentData(PackageInfo packageInfo) throws AgentException, TaskException { + Preconditions.checkArgument(packageInfo.packageName.equals(PM_PACKAGE)); + BackupAgent pmAgent = mBackupManagerService.makeMetadataAgent(); + mAgent = IBackupAgent.Stub.asInterface(pmAgent.onBind()); + extractAgentData(packageInfo, mAgent); + } + /** - * Returns a {@link Pair}. The first of the pair contains the status. In case the status is - * {@link BackupTransport#TRANSPORT_OK}, the second of the pair contains the agent result, - * otherwise {@code null}. + * Binds to the agent and extracts its backup data. If this method returns, the data in {@code + * mBackupData} is ready to be sent to the transport, otherwise it will throw. + * + * <p>This method leaves agent resources (agent binder, files and file-descriptors) opened that + * need to be cleaned up after terminating, either successfully or exceptionally. This clean-up + * can be done with methods {@link #cleanUpAgentForTransportStatus(int)} and {@link + * #cleanUpAgentForError(BackupException)}, depending on whether data was successfully sent to + * the transport or not. It's the caller responsibility to do the clean-up or delegate it. */ - private Pair<Integer, RemoteResult> extractAgentData(String packageName, IBackupAgent agent) { + private void extractAgentData(PackageInfo packageInfo) throws AgentException, TaskException { + mBackupManagerService.setWorkSource(new WorkSource(packageInfo.applicationInfo.uid)); + try { + mAgent = bindAgent(packageInfo); + extractAgentData(packageInfo, mAgent); + } finally { + mBackupManagerService.setWorkSource(null); + } + } + + /** + * Calls agent {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor, + * ParcelFileDescriptor, long, IBackupCallback, int)} and waits for the result. If this method + * returns, the data in {@code mBackupData} is ready to be sent to the transport, otherwise it + * will throw. + * + * <p>This method creates files and file-descriptors for the agent that need to be deleted and + * closed after terminating, either successfully or exceptionally. This clean-up can be done + * with methods {@link #cleanUpAgentForTransportStatus(int)} and {@link + * #cleanUpAgentForError(BackupException)}, depending on whether data was successfully sent to + * the transport or not. It's the caller responsibility to do the clean-up or delegate it. + */ + private void extractAgentData(PackageInfo packageInfo, IBackupAgent agent) + throws AgentException, TaskException { + String packageName = packageInfo.packageName; mReporter.onExtractAgentData(packageName); - File blankStateFile = new File(mStateDirectory, BLANK_STATE_FILE_NAME); mSavedStateFile = new File(mStateDirectory, packageName); mBackupDataFile = new File(mDataDirectory, packageName + STAGING_FILE_SUFFIX); mNewStateFile = new File(mStateDirectory, packageName + NEW_STATE_FILE_SUFFIX); mReporter.onAgentFilesReady(mBackupDataFile); - mSavedState = null; - mBackupData = null; - mNewState = null; - boolean callingAgent = false; final RemoteResult agentResult; try { - File savedStateFileForAgent = (mNonIncremental) ? blankStateFile : mSavedStateFile; + File savedStateFileForAgent = (mNonIncremental) ? mBlankStateFile : mSavedStateFile; // MODE_CREATE to make an empty file if necessary - mSavedState = ParcelFileDescriptor.open( - savedStateFileForAgent, MODE_READ_ONLY | MODE_CREATE); - mBackupData = ParcelFileDescriptor.open( - mBackupDataFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE); - mNewState = ParcelFileDescriptor.open( - mNewStateFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE); + mSavedState = + ParcelFileDescriptor.open(savedStateFileForAgent, MODE_READ_ONLY | MODE_CREATE); + mBackupData = + ParcelFileDescriptor.open( + mBackupDataFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE); + mNewState = + ParcelFileDescriptor.open( + mNewStateFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE); if (!SELinux.restorecon(mBackupDataFile)) { mReporter.onRestoreconFailed(mBackupDataFile); @@ -713,15 +703,40 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { "doBackup()"); } catch (Exception e) { mReporter.onCallAgentDoBackupError(packageName, callingAgent, e); - cleanUpAgentForAgentError(); - // TODO: Remove the check on callingAgent when RemoteCall supports local agent calls. - int status = - callingAgent ? BackupTransport.AGENT_ERROR : BackupTransport.TRANSPORT_ERROR; - return Pair.create(status, null); + if (callingAgent) { + throw AgentException.transitory(e); + } else { + throw TaskException.create(); + } + } finally { + mBlankStateFile.delete(); } - blankStateFile.delete(); + checkAgentResult(packageInfo, agentResult); + } - return Pair.create(BackupTransport.TRANSPORT_OK, agentResult); + private void checkAgentResult(PackageInfo packageInfo, RemoteResult result) + throws AgentException, TaskException { + if (result == RemoteResult.FAILED_THREAD_INTERRUPTED) { + // Not an explicit cancel, we need to flag it. + mCancelled = true; + mReporter.onAgentCancelled(packageInfo); + throw TaskException.create(); + } + if (result == RemoteResult.FAILED_CANCELLED) { + mReporter.onAgentCancelled(packageInfo); + throw TaskException.create(); + } + if (result == RemoteResult.FAILED_TIMED_OUT) { + mReporter.onAgentTimedOut(packageInfo); + throw AgentException.transitory(); + } + Preconditions.checkState(result.isPresent()); + long resultCode = result.get(); + if (resultCode == BackupAgent.RESULT_ERROR) { + mReporter.onAgentResultError(packageInfo); + throw AgentException.transitory(); + } + Preconditions.checkState(resultCode == BackupAgent.RESULT_SUCCESS); } private void agentFail(IBackupAgent agent, String message) { @@ -801,94 +816,79 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } } - /** Returns whether to consume next queue package. */ - private boolean sendDataToTransport() { + /** Returns transport status. */ + private int sendDataToTransport() throws AgentException, TaskException, IOException { Preconditions.checkState(mBackupData != null); + checkBackupData(mCurrentPackage.applicationInfo, mBackupDataFile); String packageName = mCurrentPackage.packageName; - ApplicationInfo applicationInfo = mCurrentPackage.applicationInfo; + writeWidgetPayloadIfAppropriate(mBackupData.getFileDescriptor(), packageName); - boolean writingWidgetData = false; - try { - if (!validateBackupData(applicationInfo, mBackupDataFile)) { - cleanUpAgentForAgentError(); - return true; - } - writingWidgetData = true; - writeWidgetPayloadIfAppropriate(mBackupData.getFileDescriptor(), packageName); - } catch (IOException e) { - if (writingWidgetData) { - mReporter.onWriteWidgetDataError(packageName, e); - } else { - mReporter.onReadAgentDataError(packageName, e); - } - cleanUpAgentForAgentError(); - revertTask(); - return false; + boolean nonIncremental = mSavedStateFile.length() == 0; + int status = transportPerformBackup(mCurrentPackage, mBackupDataFile, nonIncremental); + handleTransportStatus(status, packageName, mBackupDataFile.length()); + return status; + } + + private int transportPerformBackup( + PackageInfo packageInfo, File backupDataFile, boolean nonIncremental) + throws TaskException { + String packageName = packageInfo.packageName; + long size = backupDataFile.length(); + if (size <= 0) { + mReporter.onEmptyData(packageInfo); + return BackupTransport.TRANSPORT_OK; } - boolean nonIncremental = mSavedStateFile.length() == 0; - long size = mBackupDataFile.length(); - if (size > 0) { - try (ParcelFileDescriptor backupData = - ParcelFileDescriptor.open(mBackupDataFile, MODE_READ_ONLY)) { - IBackupTransport transport = - mTransportClient.connectOrThrow("KVBT.sendDataToTransport()"); - mReporter.onSendDataToTransport(packageName); - int flags = getPerformBackupFlags(mUserInitiated, nonIncremental); + int status; + try (ParcelFileDescriptor backupData = + ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) { + IBackupTransport transport = + mTransportClient.connectOrThrow("KVBT.transportPerformBackup()"); + mReporter.onTransportPerformBackup(packageName); + int flags = getPerformBackupFlags(mUserInitiated, nonIncremental); - mStatus = transport.performBackup(mCurrentPackage, backupData, flags); - if (mStatus == BackupTransport.TRANSPORT_OK) { - mStatus = transport.finishBackup(); - } - } catch (Exception e) { - mReporter.onPackageBackupError(packageName, e); - mStatus = BackupTransport.TRANSPORT_ERROR; + status = transport.performBackup(packageInfo, backupData, flags); + if (status == BackupTransport.TRANSPORT_OK) { + status = transport.finishBackup(); } - } else { - mReporter.onEmptyData(mCurrentPackage); - mStatus = BackupTransport.TRANSPORT_OK; + } catch (Exception e) { + mReporter.onPackageBackupTransportError(packageName, e); + throw TaskException.causedBy(e); } - if (nonIncremental - && mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { - mReporter.onNonIncrementalAndNonIncrementalRequired(); - mStatus = BackupTransport.TRANSPORT_ERROR; + if (nonIncremental && status == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { + mReporter.onPackageBackupNonIncrementalAndNonIncrementalRequired(packageName); + throw TaskException.create(); } - - boolean processQueue = handleTransportStatus(mStatus, packageName, size); - // We might report quota exceeded to the agent in handleTransportStatus() above, so we - // only clean-up after it. - cleanUpAgentForTransportStatus(mStatus); - return processQueue; + return status; } - /** Returns whether to consume next queue package. */ - private boolean handleTransportStatus(int status, String packageName, long size) { + private void handleTransportStatus(int status, String packageName, long size) + throws TaskException, AgentException { if (status == BackupTransport.TRANSPORT_OK) { mReporter.onPackageBackupComplete(packageName, size); - return true; - } - if (status == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { - mReporter.onPackageBackupRejected(packageName); - return true; + return; } if (status == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { mReporter.onPackageBackupNonIncrementalRequired(mCurrentPackage); // Immediately retry the current package. mQueue.add(0, packageName); - return true; + return; + } + if (status == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { + mReporter.onPackageBackupRejected(packageName); + throw AgentException.permanent(); } if (status == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { mReporter.onPackageBackupQuotaExceeded(packageName); agentDoQuotaExceeded(mAgent, packageName, size); - return true; + throw AgentException.permanent(); } // Any other error here indicates a transport-level failure. mReporter.onPackageBackupTransportFailure(packageName); - revertTask(); - return false; + throw TaskException.forStatus(status); } private void agentDoQuotaExceeded(@Nullable IBackupAgent agent, String packageName, long size) { @@ -908,19 +908,17 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } /** - * For system apps and pseudo-apps always return {@code true}. For regular apps returns whether - * {@code backupDataFile} doesn't have any protected keys. - * - * <p>If the app has attempted to write any protected keys we also crash them. + * For system apps and pseudo-apps never throws. For regular apps throws {@link AgentException} + * if {@code backupDataFile} has any protected keys, also crashing the app. */ - private boolean validateBackupData( - @Nullable ApplicationInfo applicationInfo, File backupDataFile) throws IOException { + private void checkBackupData(@Nullable ApplicationInfo applicationInfo, File backupDataFile) + throws IOException, AgentException { if (applicationInfo == null || (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { // System apps and pseudo-apps can write what they want. - return true; + return; } try (ParcelFileDescriptor backupData = - ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) { + ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) { BackupDataInput backupDataInput = new BackupDataInput(backupData.getFileDescriptor()); while (backupDataInput.readNextHeader()) { String key = backupDataInput.getKey(); @@ -928,12 +926,11 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mReporter.onAgentIllegalKey(mCurrentPackage, key); // Crash them if they wrote any protected keys. agentFail(mAgent, "Illegal backup key: " + key); - return false; + throw AgentException.permanent(); } backupDataInput.skipEntityData(); } } - return true; } private int getPerformBackupFlags(boolean userInitiated, boolean nonIncremental) { @@ -1009,44 +1006,39 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } } - /** Cleans-up after having called the agent. */ - private void cleanUpAgentForTransportStatus(int status) { - updateFiles(status); - cleanUpAgent(); - } - - /** Cleans-up if we failed to call the agent. */ - private void cleanUpAgentForAgentError() { - mBackupDataFile.delete(); - mNewStateFile.delete(); - cleanUpAgent(); + /** + * Cleans up agent resources opened by {@link #extractAgentData(PackageInfo)} for exceptional + * case. + * + * <p>Note: Declaring exception parameter so that the caller only calls this when an exception + * is thrown. + */ + private void cleanUpAgentForError(BackupException exception) { + cleanUpAgent(StateTransaction.DISCARD_NEW); } - private void updateFiles(int status) { + /** + * Cleans up agent resources opened by {@link #extractAgentData(PackageInfo)} according to + * transport status returned in {@link #sendDataToTransport(PackageInfo)}. + */ + private void cleanUpAgentForTransportStatus(int status) { switch (status) { case BackupTransport.TRANSPORT_OK: - mBackupDataFile.delete(); - mNewStateFile.renameTo(mSavedStateFile); + cleanUpAgent(StateTransaction.COMMIT_NEW); break; case BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED: - mSavedStateFile.delete(); - mBackupDataFile.delete(); - mNewStateFile.delete(); + cleanUpAgent(StateTransaction.DISCARD_ALL); break; default: - // Includes: - // * BackupTransport.TRANSPORT_PACKAGE_REJECTED - // * BackupTransport.TRANSPORT_QUOTA_EXCEEDED - // * BackupTransport.TRANSPORT_ERROR - mBackupDataFile.delete(); - mNewStateFile.delete(); - break; + // All other transport statuses are properly converted to agent or task exceptions. + throw new AssertionError(); } } - /** Cleans-up file-descriptors and unbinds agent. */ - private void cleanUpAgent() { - mAgent = null; + private void cleanUpAgent(@StateTransaction int stateTransaction) { + applyStateTransaction(stateTransaction); + mBackupDataFile.delete(); + mBlankStateFile.delete(); tryCloseFileDescriptor(mSavedState, "old state"); tryCloseFileDescriptor(mBackupData, "backup data"); tryCloseFileDescriptor(mNewState, "new state"); @@ -1058,6 +1050,24 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { if (mCurrentPackage.applicationInfo != null) { mBackupManagerService.unbindAgent(mCurrentPackage.applicationInfo); } + mAgent = null; + } + + private void applyStateTransaction(@StateTransaction int stateTransaction) { + switch (stateTransaction) { + case StateTransaction.COMMIT_NEW: + mNewStateFile.renameTo(mSavedStateFile); + break; + case StateTransaction.DISCARD_NEW: + mNewStateFile.delete(); + break; + case StateTransaction.DISCARD_ALL: + mSavedStateFile.delete(); + mNewStateFile.delete(); + break; + default: + throw new IllegalArgumentException("Unknown state transaction " + stateTransaction); + } } private void tryCloseFileDescriptor(@Nullable Closeable closeable, String logName) { @@ -1079,4 +1089,16 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mPendingCall = null; return result; } + + @IntDef({ + StateTransaction.COMMIT_NEW, + StateTransaction.DISCARD_NEW, + StateTransaction.DISCARD_ALL, + }) + @Retention(RetentionPolicy.SOURCE) + private @interface StateTransaction { + int COMMIT_NEW = 0; + int DISCARD_NEW = 1; + int DISCARD_ALL = 2; + } } diff --git a/services/backup/java/com/android/server/backup/keyvalue/TaskException.java b/services/backup/java/com/android/server/backup/keyvalue/TaskException.java new file mode 100644 index 000000000000..08d289556ca3 --- /dev/null +++ b/services/backup/java/com/android/server/backup/keyvalue/TaskException.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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.keyvalue; + +import android.app.backup.BackupTransport; + +import com.android.internal.util.Preconditions; + +/** + * The key-value backup task has failed, no more packages will be processed and we shouldn't attempt + * any more backups now. These can be caused by transport failures (as opposed to agent failures). + * + * @see KeyValueBackupTask + * @see AgentException + */ +class TaskException extends BackupException { + private static final int DEFAULT_STATUS = BackupTransport.TRANSPORT_ERROR; + + static TaskException stateCompromised() { + return new TaskException(/* stateCompromised */ true, DEFAULT_STATUS); + } + + static TaskException stateCompromised(Exception cause) { + if (cause instanceof TaskException) { + TaskException exception = (TaskException) cause; + return new TaskException(cause, /* stateCompromised */ true, exception.getStatus()); + } + return new TaskException(cause, /* stateCompromised */ true, DEFAULT_STATUS); + } + + static TaskException forStatus(int status) { + Preconditions.checkArgument( + status != BackupTransport.TRANSPORT_OK, "Exception based on TRANSPORT_OK"); + return new TaskException(/* stateCompromised */ false, status); + } + + static TaskException causedBy(Exception cause) { + if (cause instanceof TaskException) { + return (TaskException) cause; + } + return new TaskException(cause, /* stateCompromised */ false, DEFAULT_STATUS); + } + + static TaskException create() { + return new TaskException(/* stateCompromised */ false, DEFAULT_STATUS); + } + + private final boolean mStateCompromised; + private final int mStatus; + + private TaskException(Exception cause, boolean stateCompromised, int status) { + super(cause); + mStateCompromised = stateCompromised; + mStatus = status; + } + + private TaskException(boolean stateCompromised, int status) { + mStateCompromised = stateCompromised; + mStatus = status; + } + + boolean isStateCompromised() { + return mStateCompromised; + } + + int getStatus() { + return mStatus; + } +} diff --git a/services/robotests/src/com/android/server/backup/keyvalue/AgentExceptionTest.java b/services/robotests/src/com/android/server/backup/keyvalue/AgentExceptionTest.java new file mode 100644 index 000000000000..373033500cde --- /dev/null +++ b/services/robotests/src/com/android/server/backup/keyvalue/AgentExceptionTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 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.keyvalue; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import com.android.server.testing.FrameworkRobolectricTestRunner; +import com.android.server.testing.SystemLoaderPackages; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import java.io.IOException; + +@RunWith(FrameworkRobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 26) +@SystemLoaderPackages({"com.android.server.backup"}) +@Presubmit +public class AgentExceptionTest { + @Test + public void testTransitory_isTransitory() throws Exception { + AgentException exception = AgentException.transitory(); + + assertThat(exception.isTransitory()).isTrue(); + } + + @Test + public void testTransitory_withCause() throws Exception { + Exception cause = new IOException(); + + AgentException exception = AgentException.transitory(cause); + + assertThat(exception.isTransitory()).isTrue(); + assertThat(exception.getCause()).isEqualTo(cause); + } + + @Test + public void testPermanent_isNotTransitory() throws Exception { + AgentException exception = AgentException.permanent(); + + assertThat(exception.isTransitory()).isFalse(); + } + + @Test + public void testPermanent_withCause() throws Exception { + Exception cause = new IOException(); + + AgentException exception = AgentException.permanent(cause); + + assertThat(exception.isTransitory()).isFalse(); + assertThat(exception.getCause()).isEqualTo(cause); + } +} diff --git a/services/robotests/src/com/android/server/backup/keyvalue/BackupExceptionTest.java b/services/robotests/src/com/android/server/backup/keyvalue/BackupExceptionTest.java new file mode 100644 index 000000000000..5ea74f163bd6 --- /dev/null +++ b/services/robotests/src/com/android/server/backup/keyvalue/BackupExceptionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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.keyvalue; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import com.android.server.testing.FrameworkRobolectricTestRunner; +import com.android.server.testing.SystemLoaderPackages; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import java.io.IOException; + +@RunWith(FrameworkRobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 26) +@SystemLoaderPackages({"com.android.server.backup"}) +@Presubmit +public class BackupExceptionTest { + @Test + public void testConstructor_passesCause() { + Exception cause = new IOException(); + + Exception exception = new BackupException(cause); + + assertThat(exception.getCause()).isEqualTo(cause); + } +} diff --git a/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index b4bc9d199cb0..fb57d68082a2 100644 --- a/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -155,9 +155,7 @@ import java.util.List; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; -// TODO: When returning to RUNNING_QUEUE vs FINAL, RUNNING_QUEUE sets status = OK. Why? Verify? -// TODO: Check queue in general, behavior w/ multiple packages -// TODO: Test PM invocation +// TODO: Test agents timing out @RunWith(FrameworkRobolectricTestRunner.class) @Config( manifest = Config.NONE, @@ -370,6 +368,47 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenOnePackage_cleansUpPmFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + assertCleansUpFiles(mTransport, PM_PACKAGE); + } + + @Test + public void testRunTask_whenTransportReturnsTransportErrorForPm_cleansUpPmFiles() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PM_PACKAGE)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + assertCleansUpFiles(mTransport, PM_PACKAGE); + } + + @Test + public void testRunTask_whenTransportReturnsTransportErrorForPm_resetsBackupState() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PM_PACKAGE)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile()); + } + + @Test public void testRunTask_whenOnePackage_updatesBookkeeping() throws Exception { // Transport has to be initialized to not reset current token TransportMock transportMock = setUpInitializedTransport(mTransport); @@ -418,7 +457,7 @@ public class KeyValueBackupTaskTest { public void testRunTask_whenNonPmPackageAndNonIncremental_doesNotBackUpPm() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgentWithData(PACKAGE_1); - PackageManagerBackupAgent pmAgent = spy(createPmAgent()); + BackupAgent pmAgent = spy(createPmAgent()); when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1); @@ -431,7 +470,7 @@ public class KeyValueBackupTaskTest { public void testRunTask_whenNonPmPackageAndPmAndNonIncremental_backsUpPm() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgentWithData(PACKAGE_1); - PackageManagerBackupAgent pmAgent = spy(createPmAgent()); + BackupAgent pmAgent = spy(createPmAgent()); when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1, PM_PACKAGE); @@ -445,7 +484,7 @@ public class KeyValueBackupTaskTest { public void testRunTask_whenNonPmPackageAndIncremental_backsUpPm() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgentWithData(PACKAGE_1); - PackageManagerBackupAgent pmAgent = spy(createPmAgent()); + BackupAgent pmAgent = spy(createPmAgent()); when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, false, PACKAGE_1); @@ -529,6 +568,35 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenPackageUnknown() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + // Not calling setUpAgent() for PACKAGE_1 + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport, never()) + .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_PACKAGE_NOT_FOUND); + verify(mObserver).backupFinished(SUCCESS); + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenFirstPackageUnknown_callsTransportForSecondPackage() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + // Not calling setUpAgent() for PACKAGE_1 + setUpAgentWithData(PACKAGE_2); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2); + + runTask(task); + + verify(transportMock.transport) + .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt()); + } + + @Test public void testRunTask_whenPackageNotEligibleForBackup() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); AgentMock agentMock = setUpAgentWithData(PACKAGE_1.backupNotAllowed()); @@ -545,6 +613,19 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenFirstPackageNotEligibleForBackup_callsTransportForSecondPackage() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentsWithData(PACKAGE_1.backupNotAllowed(), PACKAGE_2); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2); + + runTask(task); + + verify(transportMock.transport) + .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt()); + } + + @Test public void testRunTask_whenPackageDoesFullBackup() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); PackageData packageData = fullBackupPackage(1); @@ -561,6 +642,20 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenFirstPackageDoesFullBackup_callsTransportForSecondPackage() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + PackageData packageData = fullBackupPackage(1); + setUpAgentsWithData(packageData, PACKAGE_2); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, packageData, PACKAGE_2); + + runTask(task); + + verify(transportMock.transport) + .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt()); + } + + @Test public void testRunTask_whenPackageIsStopped() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); AgentMock agentMock = setUpAgentWithData(PACKAGE_1.stopped()); @@ -575,18 +670,16 @@ public class KeyValueBackupTaskTest { } @Test - public void testRunTask_whenPackageUnknown() throws Exception { + public void testRunTask_whenFirstPackageIsStopped_callsTransportForSecondPackage() + throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); - // Not calling setUpAgent() - KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + setUpAgentsWithData(PACKAGE_1.stopped(), PACKAGE_2); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2); runTask(task); - verify(transportMock.transport, never()) - .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_PACKAGE_NOT_FOUND); - verify(mObserver).backupFinished(SUCCESS); - assertBackupNotPendingFor(PACKAGE_1); + verify(transportMock.transport) + .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt()); } @Test @@ -629,6 +722,7 @@ public class KeyValueBackupTaskTest { verify(mBackupManagerService).setWorkSource(null); verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); verify(mObserver).backupFinished(BackupManager.SUCCESS); + assertBackupPendingFor(PACKAGE_1); } @Test @@ -645,6 +739,7 @@ public class KeyValueBackupTaskTest { verify(mBackupManagerService).setWorkSource(null); verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); verify(mObserver).backupFinished(BackupManager.SUCCESS); + assertBackupPendingFor(PACKAGE_1); } @Test @@ -798,7 +893,7 @@ public class KeyValueBackupTaskTest { runTask(task); - assertBackupNotPendingFor(PACKAGE_1); + assertBackupPendingFor(PACKAGE_1); } @Test @@ -1140,6 +1235,38 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenPmAgentWritesData_callsTransportPerformBackupWithAgentData() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + Path backupDataPath = createTemporaryFile(); + when(transportMock.transport.performBackup( + argThat(packageInfo(PM_PACKAGE)), any(), anyInt())) + .then(copyBackupDataTo(backupDataPath)); + BackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + agentOnBackupDo( + pmAgent, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key1", "data1".getBytes()); + writeData(dataOutput, "key2", "data2".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport) + .performBackup(argThat(packageInfo(PM_PACKAGE)), any(), anyInt()); + try (FileInputStream inputStream = new FileInputStream(backupDataPath.toFile())) { + BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); + assertDataHasKeyValue(backupData, "key1", "data1".getBytes()); + assertDataHasKeyValue(backupData, "key2", "data2".getBytes()); + assertThat(backupData.readNextHeader()).isFalse(); + } + } + + @Test public void testRunTask_whenPerformBackupSucceeds_callsTransportFinishBackup() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); @@ -1176,6 +1303,50 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenFinishBackupSucceedsForPm_cleansUp() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + when(transportMock.transport.finishBackup()).thenReturn(BackupTransport.TRANSPORT_OK); + BackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + agentOnBackupDo( + pmAgent, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PM_PACKAGE))) + .isEqualTo("newState".getBytes()); + assertCleansUpFiles(mTransport, PM_PACKAGE); + // We don't unbind PM + verify(mBackupManagerService, never()).unbindAgent(argThat(applicationInfo(PM_PACKAGE))); + } + + @Test + public void testRunTask_whenFinishBackupSucceedsForPm_doesNotUnbindPm() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + when(transportMock.transport.finishBackup()).thenReturn(BackupTransport.TRANSPORT_OK); + BackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + agentOnBackupDo( + pmAgent, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService, never()).unbindAgent(argThat(applicationInfo(PM_PACKAGE))); + } + + @Test public void testRunTask_whenFinishBackupSucceeds_logsBackupPackageEvent() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgentWithData(PACKAGE_1); @@ -1354,6 +1525,7 @@ public class KeyValueBackupTaskTest { public void testRunTask_whenTransportReturnsQuotaExceeded_updatesBookkeeping() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); when(transportMock.transport.performBackup( argThat(packageInfo(PACKAGE_1)), any(), anyInt())) .thenReturn(BackupTransport.TRANSPORT_QUOTA_EXCEEDED); @@ -1701,9 +1873,9 @@ public class KeyValueBackupTaskTest { } @Test - public void testRunTask_whenPmAgentFails() throws Exception { + public void testRunTask_whenPmAgentFails_reportsCorrectly() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); - PackageManagerBackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); + BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); @@ -1718,6 +1890,75 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenPmAgentFails_revertsTask() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + assertTaskReverted(transportMock, PACKAGE_1); + } + + @Test + public void testRunTask_whenPmAgentFails_cleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + assertCleansUpFiles(mTransport, PM_PACKAGE); + } + + @Test + public void testRunTask_whenPmAgentFails_resetsBackupState() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile()); + } + + @Test + public void testRunTask_whenMarkCancelDuringPmOnBackup_resetsBackupState() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + BackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + agentOnBackupDo( + pmAgent, (oldState, dataOutput, newState) -> runInWorkerThread(task::markCancel)); + + runTask(task); + + verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile()); + } + + @Test + public void testRunTask_whenMarkCancelDuringPmOnBackup_cleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + BackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + agentOnBackupDo( + pmAgent, (oldState, dataOutput, newState) -> runInWorkerThread(task::markCancel)); + + runTask(task); + + assertCleansUpFiles(mTransport, PM_PACKAGE); + } + + @Test public void testRunTask_whenBackupRunning_doesNotThrow() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); when(mBackupManagerService.isBackupOperationInProgress()).thenReturn(true); @@ -1736,7 +1977,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mReporter).onReadAgentDataError(eq(PACKAGE_1.packageName), any()); + verify(mReporter).onAgentDataError(eq(PACKAGE_1.packageName), any()); } @Test @@ -1779,6 +2020,24 @@ public class KeyValueBackupTaskTest { } @Test + public void testRunTask_whenMarkCancelDuringAgentOnBackup_cleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + runInWorkerThread(task::markCancel); + }); + + runTask(task); + + assertCleansUpFiles(mTransport, PACKAGE_1); + } + + @Test public void testRunTask_whenMarkCancelDuringFirstAgentOnBackup_doesNotCallTransportAfterWaitCancel() throws Exception { @@ -2293,20 +2552,28 @@ public class KeyValueBackupTaskTest { */ private static void agentOnBackupDo(AgentMock agentMock, BackupAgentOnBackup function) throws Exception { - doAnswer( - (BackupAgentOnBackup) - (oldState, dataOutput, newState) -> { - ByteArrayOutputStream outputStream = - new ByteArrayOutputStream(); - transferStreamedData( - new FileInputStream(oldState.getFileDescriptor()), - outputStream); - agentMock.oldState = outputStream.toByteArray(); - agentMock.oldStateHistory.add(agentMock.oldState); - function.onBackup(oldState, dataOutput, newState); - }) - .when(agentMock.agent) - .onBackup(any(), any(), any()); + agentOnBackupDo( + agentMock.agent, + (oldState, dataOutput, newState) -> { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + transferStreamedData( + new FileInputStream(oldState.getFileDescriptor()), outputStream); + agentMock.oldState = outputStream.toByteArray(); + agentMock.oldStateHistory.add(agentMock.oldState); + function.onBackup(oldState, dataOutput, newState); + }); + } + + /** + * Implements {@code function} for {@link BackupAgent#onBackup(ParcelFileDescriptor, + * BackupDataOutput, ParcelFileDescriptor)} of {@code agentMock}. + * + * @see #agentOnBackupDo(AgentMock, BackupAgentOnBackup) + * @see #remoteAgentOnBackupThrows(AgentMock, BackupAgentOnBackup) + */ + private static void agentOnBackupDo(BackupAgent backupAgent, BackupAgentOnBackup function) + throws IOException { + doAnswer(function).when(backupAgent).onBackup(any(), any(), any()); } /** @@ -2400,6 +2667,10 @@ public class KeyValueBackupTaskTest { // constructor assertJournalDoesNotContain(mBackupManagerService.getJournal(), packageName); assertThat(mBackupManagerService.getPendingBackups()).doesNotContainKey(packageName); + // Also verifying BMS is never called since for some cases the package wouldn't be + // pending for other reasons (for example it's not eligible for backup). Regardless of + // these reasons, we shouldn't mark them as pending backup (call dataChangedImpl()). + verify(mBackupManagerService, never()).dataChangedImpl(packageName); } } diff --git a/services/robotests/src/com/android/server/backup/keyvalue/TaskExceptionTest.java b/services/robotests/src/com/android/server/backup/keyvalue/TaskExceptionTest.java new file mode 100644 index 000000000000..4b79657b1dae --- /dev/null +++ b/services/robotests/src/com/android/server/backup/keyvalue/TaskExceptionTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2018 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.keyvalue; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.expectThrows; + +import android.app.backup.BackupTransport; +import android.platform.test.annotations.Presubmit; + +import com.android.server.testing.FrameworkRobolectricTestRunner; +import com.android.server.testing.SystemLoaderPackages; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import java.io.IOException; + +@RunWith(FrameworkRobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 26) +@SystemLoaderPackages({"com.android.server.backup"}) +@Presubmit +public class TaskExceptionTest { + @Test + public void testStateCompromised() { + TaskException exception = TaskException.stateCompromised(); + + assertThat(exception.isStateCompromised()).isTrue(); + assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR); + } + + @Test + public void testStateCompromised_whenCauseInstanceOfTaskException() { + Exception cause = TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED); + + TaskException exception = TaskException.stateCompromised(cause); + + assertThat(exception.isStateCompromised()).isTrue(); + assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_NOT_INITIALIZED); + assertThat(exception.getCause()).isEqualTo(cause); + } + + @Test + public void testStateCompromised_whenCauseNotInstanceOfTaskException() { + Exception cause = new IOException(); + + TaskException exception = TaskException.stateCompromised(cause); + + assertThat(exception.isStateCompromised()).isTrue(); + assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR); + assertThat(exception.getCause()).isEqualTo(cause); + } + + @Test + public void testForStatus_whenTransportOk_throws() { + expectThrows( + IllegalArgumentException.class, + () -> TaskException.forStatus(BackupTransport.TRANSPORT_OK)); + } + + @Test + public void testForStatus_whenTransportNotInitialized() { + TaskException exception = + TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED); + + assertThat(exception.isStateCompromised()).isFalse(); + assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_NOT_INITIALIZED); + } + + @Test + public void testCausedBy_whenCauseInstanceOfTaskException_returnsCause() { + Exception cause = TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED); + + TaskException exception = TaskException.causedBy(cause); + + assertThat(exception).isEqualTo(cause); + } + + @Test + public void testCausedBy_whenCauseNotInstanceOfTaskException() { + Exception cause = new IOException(); + + TaskException exception = TaskException.causedBy(cause); + + assertThat(exception).isNotEqualTo(cause); + assertThat(exception.isStateCompromised()).isFalse(); + assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR); + assertThat(exception.getCause()).isEqualTo(cause); + } + + @Test + public void testCreate() { + TaskException exception = TaskException.create(); + + assertThat(exception.isStateCompromised()).isFalse(); + assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR); + } + + @Test + public void testIsStateCompromised_whenStateCompromised_returnsTrue() { + TaskException taskException = TaskException.stateCompromised(); + + boolean stateCompromised = taskException.isStateCompromised(); + + assertThat(stateCompromised).isTrue(); + } + + @Test + public void testIsStateCompromised_whenCreatedWithCreate_returnsFalse() { + TaskException taskException = TaskException.create(); + + boolean stateCompromised = taskException.isStateCompromised(); + + assertThat(stateCompromised).isFalse(); + } + + @Test + public void testGetStatus_whenStatusIsTransportPackageRejected() { + TaskException taskException = + TaskException.forStatus(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + + int status = taskException.getStatus(); + + assertThat(status).isEqualTo(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + } + + @Test + public void testGetStatus_whenStatusIsTransportNotInitialized() { + TaskException taskException = + TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED); + + int status = taskException.getStatus(); + + assertThat(status).isEqualTo(BackupTransport.TRANSPORT_NOT_INITIALIZED); + } +} |