| /* |
| * 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 android.content.pm.ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND; |
| |
| import static com.android.internal.util.Preconditions.checkNotNull; |
| import static com.android.server.backup.BackupManagerService.DEBUG; |
| import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING; |
| import static com.android.server.backup.BackupManagerService.MORE_DEBUG; |
| import static com.android.server.backup.BackupManagerService.TAG; |
| import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_OPERATION_TIMEOUT; |
| import static com.android.server.backup.internal.BackupHandler.MSG_FULL_CONFIRMATION_TIMEOUT; |
| import static com.android.server.backup.internal.BackupHandler.MSG_OP_COMPLETE; |
| import static com.android.server.backup.internal.BackupHandler.MSG_REQUEST_BACKUP; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_CLEAR; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_BACKUP; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_RESTORE; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RUN_CLEAR; |
| import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE; |
| import static com.android.server.backup.internal.BackupHandler.MSG_SCHEDULE_BACKUP_PACKAGE; |
| |
| import android.annotation.Nullable; |
| import android.annotation.UserIdInt; |
| import android.app.ActivityManager; |
| import android.app.ActivityManagerInternal; |
| import android.app.AlarmManager; |
| import android.app.AppGlobals; |
| import android.app.IActivityManager; |
| import android.app.IBackupAgent; |
| import android.app.PendingIntent; |
| import android.app.backup.BackupAgent; |
| import android.app.backup.BackupManager; |
| import android.app.backup.BackupManagerMonitor; |
| import android.app.backup.FullBackup; |
| import android.app.backup.IBackupManager; |
| import android.app.backup.IBackupManagerMonitor; |
| import android.app.backup.IBackupObserver; |
| import android.app.backup.IFullBackupRestoreObserver; |
| import android.app.backup.IRestoreSession; |
| import android.app.backup.ISelectBackupTransportCallback; |
| import android.content.ActivityNotFoundException; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.database.ContentObserver; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.ParcelFileDescriptor; |
| import android.os.PowerManager; |
| import android.os.PowerManager.ServiceType; |
| import android.os.PowerSaveState; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.SELinux; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.os.WorkSource; |
| import android.os.storage.IStorageManager; |
| import android.os.storage.StorageManager; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.AtomicFile; |
| import android.util.EventLog; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.backup.IBackupTransport; |
| import com.android.internal.util.DumpUtils; |
| import com.android.internal.util.Preconditions; |
| import com.android.server.AppWidgetBackupBridge; |
| import com.android.server.EventLogTags; |
| import com.android.server.LocalServices; |
| import com.android.server.backup.fullbackup.FullBackupEntry; |
| import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; |
| import com.android.server.backup.internal.BackupHandler; |
| import com.android.server.backup.internal.ClearDataObserver; |
| import com.android.server.backup.internal.OnTaskFinishedListener; |
| import com.android.server.backup.internal.Operation; |
| import com.android.server.backup.internal.PerformInitializeTask; |
| import com.android.server.backup.internal.RunBackupReceiver; |
| import com.android.server.backup.internal.RunInitializeReceiver; |
| import com.android.server.backup.internal.SetupObserver; |
| import com.android.server.backup.keyvalue.BackupRequest; |
| import com.android.server.backup.params.AdbBackupParams; |
| import com.android.server.backup.params.AdbParams; |
| import com.android.server.backup.params.AdbRestoreParams; |
| import com.android.server.backup.params.BackupParams; |
| import com.android.server.backup.params.ClearParams; |
| import com.android.server.backup.params.ClearRetryParams; |
| import com.android.server.backup.params.RestoreParams; |
| import com.android.server.backup.restore.ActiveRestoreSession; |
| import com.android.server.backup.restore.PerformUnifiedRestoreTask; |
| import com.android.server.backup.transport.TransportClient; |
| import com.android.server.backup.transport.TransportNotRegisteredException; |
| 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.FileUtils; |
| import com.android.server.backup.utils.SparseArrayUtils; |
| |
| import com.google.android.collect.Sets; |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.RandomAccessFile; |
| import java.security.SecureRandom; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.Random; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** System service that performs backup/restore operations. */ |
| public class UserBackupManagerService { |
| // Persistently track the need to do a full init. |
| private static final String INIT_SENTINEL_FILE_NAME = "_need_init_"; |
| |
| // System-private key used for backing up an app's widget state. Must |
| // begin with U+FFxx by convention (we reserve all keys starting |
| // with U+FF00 or higher for system use). |
| public static final String KEY_WIDGET_STATE = "\uffed\uffedwidget"; |
| |
| // Name and current contents version of the full-backup manifest file |
| // |
| // Manifest version history: |
| // |
| // 1 : initial release |
| public static final String BACKUP_MANIFEST_FILENAME = "_manifest"; |
| public static final int BACKUP_MANIFEST_VERSION = 1; |
| |
| // External archive format version history: |
| // |
| // 1 : initial release |
| // 2 : no format change per se; version bump to facilitate PBKDF2 version skew detection |
| // 3 : introduced "_meta" metadata file; no other format change per se |
| // 4 : added support for new device-encrypted storage locations |
| // 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"; |
| 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; |
| |
| private static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1; |
| |
| // Round-robin queue for scheduling full backup passes. |
| private static final int SCHEDULE_FILE_VERSION = 1; |
| |
| public static final String SETTINGS_PACKAGE = "com.android.providers.settings"; |
| public static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; |
| |
| // Pseudoname that we use for the Package Manager metadata "package". |
| public static final String PACKAGE_MANAGER_SENTINEL = "@pm@"; |
| |
| // Retry interval for clear/init when the transport is unavailable |
| private static final long TRANSPORT_RETRY_INTERVAL = 1 * AlarmManager.INTERVAL_HOUR; |
| |
| public static final String RUN_BACKUP_ACTION = "android.app.backup.intent.RUN"; |
| public static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT"; |
| private static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED"; |
| private static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName"; |
| |
| // Bookkeeping of in-flight operations. The operation token is the index of the entry in the |
| // pending operations list. |
| public static final int OP_PENDING = 0; |
| private static final int OP_ACKNOWLEDGED = 1; |
| private static final int OP_TIMEOUT = -1; |
| |
| // Waiting for backup agent to respond during backup operation. |
| public static final int OP_TYPE_BACKUP_WAIT = 0; |
| |
| // Waiting for backup agent to respond during restore operation. |
| public static final int OP_TYPE_RESTORE_WAIT = 1; |
| |
| // An entire backup operation spanning multiple packages. |
| public static final int OP_TYPE_BACKUP = 2; |
| |
| // Time delay for initialization operations that can be delayed so as not to consume too much |
| // CPU on bring-up and increase time-to-UI. |
| private static final long INITIALIZATION_DELAY_MILLIS = 3000; |
| |
| // Timeout interval for deciding that a bind or clear-data has taken too long |
| private static final long TIMEOUT_INTERVAL = 10 * 1000; |
| |
| // User confirmation timeout for a full backup/restore operation. It's this long in |
| // order to give them time to enter the backup password. |
| private static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000; |
| |
| // If an app is busy when we want to do a full-data backup, how long to defer the retry. |
| // This is fuzzed, so there are two parameters; backoff_min + Rand[0, backoff_fuzz) |
| private static final long BUSY_BACKOFF_MIN_MILLIS = 1000 * 60 * 60; // one hour |
| private static final int BUSY_BACKOFF_FUZZ = 1000 * 60 * 60 * 2; // two hours |
| |
| private static final String SERIAL_ID_FILE = "serial_id"; |
| |
| private final @UserIdInt int mUserId; |
| private final BackupAgentTimeoutParameters mAgentTimeoutParameters; |
| private final TransportManager mTransportManager; |
| private final HandlerThread mUserBackupThread; |
| |
| private final Context mContext; |
| private final PackageManager mPackageManager; |
| private final IPackageManager mPackageManagerBinder; |
| private final IActivityManager mActivityManager; |
| private final ActivityManagerInternal mActivityManagerInternal; |
| private PowerManager mPowerManager; |
| private final AlarmManager mAlarmManager; |
| private final IStorageManager mStorageManager; |
| private final BackupManagerConstants mConstants; |
| private final PowerManager.WakeLock mWakelock; |
| private final BackupHandler mBackupHandler; |
| |
| private final IBackupManager mBackupManagerBinder; |
| |
| private boolean mEnabled; // access to this is synchronized on 'this' |
| private boolean mSetupComplete; |
| private boolean mAutoRestore; |
| |
| private final PendingIntent mRunBackupIntent; |
| private final PendingIntent mRunInitIntent; |
| |
| private final ArraySet<String> mPendingInits = new ArraySet<>(); // transport names |
| |
| // map UIDs to the set of participating packages under that UID |
| private final SparseArray<HashSet<String>> mBackupParticipants = new SparseArray<>(); |
| |
| // Backups that we haven't started yet. Keys are package names. |
| private final HashMap<String, BackupRequest> mPendingBackups = new HashMap<>(); |
| |
| // locking around the pending-backup management |
| private final Object mQueueLock = new Object(); |
| |
| // The thread performing the sequence of queued backups binds to each app's agent |
| // in succession. Bind notifications are asynchronously delivered through the |
| // Activity Manager; use this lock object to signal when a requested binding has |
| // completed. |
| private final Object mAgentConnectLock = new Object(); |
| private IBackupAgent mConnectedAgent; |
| private volatile boolean mConnecting; |
| |
| private volatile boolean mBackupRunning; |
| private volatile long mLastBackupPass; |
| |
| // A similar synchronization mechanism around clearing apps' data for restore |
| private final Object mClearDataLock = new Object(); |
| private volatile boolean mClearingData; |
| |
| // Used by ADB. |
| private final BackupPasswordManager mBackupPasswordManager; |
| private final SparseArray<AdbParams> mAdbBackupRestoreConfirmations = new SparseArray<>(); |
| private final SecureRandom mRng = new SecureRandom(); |
| |
| // Time when we post the transport registration operation |
| private final long mRegisterTransportsRequestedTime; |
| |
| @GuardedBy("mQueueLock") |
| private PerformFullTransportBackupTask mRunningFullBackupTask; |
| |
| @GuardedBy("mQueueLock") |
| private ArrayList<FullBackupEntry> mFullBackupQueue; |
| |
| @GuardedBy("mPendingRestores") |
| private boolean mIsRestoreInProgress; |
| |
| @GuardedBy("mPendingRestores") |
| private final Queue<PerformUnifiedRestoreTask> mPendingRestores = new ArrayDeque<>(); |
| |
| private ActiveRestoreSession mActiveRestoreSession; |
| |
| /** |
| * mCurrentOperations contains the list of currently active operations. |
| * |
| * If type of operation is OP_TYPE_WAIT, it are waiting for an ack or timeout. |
| * An operation wraps a BackupRestoreTask within it. |
| * It's the responsibility of this task to remove the operation from this array. |
| * |
| * A BackupRestore task gets notified of ack/timeout for the operation via |
| * BackupRestoreTask#handleCancel, BackupRestoreTask#operationComplete and notifyAll called |
| * on the mCurrentOpLock. |
| * {@link UserBackupManagerService#waitUntilOperationComplete(int)} is |
| * used in various places to 'wait' for notifyAll and detect change of pending state of an |
| * operation. So typically, an operation will be removed from this array by: |
| * - BackupRestoreTask#handleCancel and |
| * - BackupRestoreTask#operationComplete OR waitUntilOperationComplete. Do not remove at both |
| * these places because waitUntilOperationComplete relies on the operation being present to |
| * determine its completion status. |
| * |
| * If type of operation is OP_BACKUP, it is a task running backups. It provides a handle to |
| * cancel backup tasks. |
| */ |
| @GuardedBy("mCurrentOpLock") |
| private final SparseArray<Operation> mCurrentOperations = new SparseArray<>(); |
| private final Object mCurrentOpLock = new Object(); |
| private final Random mTokenGenerator = new Random(); |
| private final AtomicInteger mNextToken = new AtomicInteger(); |
| |
| // Where we keep our journal files and other bookkeeping. |
| private final File mBaseStateDir; |
| private final File mDataDir; |
| private final File mJournalDir; |
| @Nullable |
| private DataChangedJournal mJournal; |
| private final File mFullBackupScheduleFile; |
| |
| // Keep a log of all the apps we've ever backed up. |
| private ProcessedPackagesJournal mProcessedPackagesJournal; |
| |
| private File mTokenFile; |
| private Set<String> mAncestralPackages = null; |
| private long mAncestralToken = 0; |
| private long mCurrentToken = 0; |
| @Nullable private File mAncestralSerialNumberFile; |
| |
| |
| /** |
| * Creates an instance of {@link UserBackupManagerService} and initializes state for it. This |
| * includes setting up the directories where we keep our bookkeeping and transport management. |
| * |
| * @see #createAndInitializeService(int, Context, Trampoline, HandlerThread, File, File, |
| * TransportManager) |
| */ |
| static UserBackupManagerService createAndInitializeService( |
| @UserIdInt int userId, |
| Context context, |
| Trampoline trampoline, |
| Set<ComponentName> transportWhitelist) { |
| String currentTransport = |
| Settings.Secure.getStringForUser( |
| context.getContentResolver(), Settings.Secure.BACKUP_TRANSPORT, userId); |
| if (TextUtils.isEmpty(currentTransport)) { |
| currentTransport = null; |
| } |
| |
| if (DEBUG) { |
| Slog.v(TAG, "Starting with transport " + currentTransport); |
| } |
| TransportManager transportManager = |
| new TransportManager(userId, context, transportWhitelist, currentTransport); |
| |
| File baseStateDir = UserBackupManagerFiles.getBaseStateDir(userId); |
| File dataDir = UserBackupManagerFiles.getDataDir(userId); |
| |
| HandlerThread userBackupThread = |
| new HandlerThread("backup-" + userId, Process.THREAD_PRIORITY_BACKGROUND); |
| userBackupThread.start(); |
| if (DEBUG) { |
| Slog.d(TAG, "Started thread " + userBackupThread.getName() + " for user " + userId); |
| } |
| |
| return createAndInitializeService( |
| userId, |
| context, |
| trampoline, |
| userBackupThread, |
| baseStateDir, |
| dataDir, |
| transportManager); |
| } |
| |
| /** |
| * Creates an instance of {@link UserBackupManagerService}. |
| * |
| * @param userId The user which this service is for. |
| * @param context The system server context. |
| * @param trampoline A reference to the proxy to {@link BackupManagerService}. |
| * @param userBackupThread The thread running backup/restore operations for the user. |
| * @param baseStateDir The directory we store the user's persistent bookkeeping data. |
| * @param dataDir The directory we store the user's temporary staging data. |
| * @param transportManager The {@link TransportManager} responsible for handling the user's |
| * transports. |
| */ |
| @VisibleForTesting |
| public static UserBackupManagerService createAndInitializeService( |
| @UserIdInt int userId, |
| Context context, |
| Trampoline trampoline, |
| HandlerThread userBackupThread, |
| File baseStateDir, |
| File dataDir, |
| TransportManager transportManager) { |
| return new UserBackupManagerService( |
| userId, |
| context, |
| trampoline, |
| userBackupThread, |
| baseStateDir, |
| dataDir, |
| transportManager); |
| } |
| |
| /** |
| * Returns the value of {@link Settings.Secure#USER_SETUP_COMPLETE} for the specified user |
| * {@code userId} as a {@code boolean}. |
| */ |
| public static boolean getSetupCompleteSettingForUser(Context context, int userId) { |
| return Settings.Secure.getIntForUser( |
| context.getContentResolver(), |
| Settings.Secure.USER_SETUP_COMPLETE, |
| 0, |
| userId) |
| != 0; |
| } |
| |
| private UserBackupManagerService( |
| @UserIdInt int userId, |
| Context context, |
| Trampoline parent, |
| HandlerThread userBackupThread, |
| File baseStateDir, |
| File dataDir, |
| TransportManager transportManager) { |
| mUserId = userId; |
| mContext = checkNotNull(context, "context cannot be null"); |
| mPackageManager = context.getPackageManager(); |
| mPackageManagerBinder = AppGlobals.getPackageManager(); |
| mActivityManager = ActivityManager.getService(); |
| mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); |
| |
| mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); |
| mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); |
| mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); |
| |
| checkNotNull(parent, "trampoline cannot be null"); |
| mBackupManagerBinder = Trampoline.asInterface(parent.asBinder()); |
| |
| mAgentTimeoutParameters = new |
| BackupAgentTimeoutParameters(Handler.getMain(), mContext.getContentResolver()); |
| mAgentTimeoutParameters.start(); |
| |
| checkNotNull(userBackupThread, "userBackupThread cannot be null"); |
| mUserBackupThread = userBackupThread; |
| mBackupHandler = new BackupHandler(this, userBackupThread.getLooper()); |
| |
| // Set up our bookkeeping |
| final ContentResolver resolver = context.getContentResolver(); |
| mSetupComplete = getSetupCompleteSettingForUser(context, userId); |
| mAutoRestore = Settings.Secure.getIntForUser(resolver, |
| Settings.Secure.BACKUP_AUTO_RESTORE, 1, userId) != 0; |
| |
| ContentObserver setupObserver = new SetupObserver(this, mBackupHandler); |
| resolver.registerContentObserver( |
| Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), |
| /* notifyForDescendents */ false, |
| setupObserver, |
| mUserId); |
| |
| mBaseStateDir = checkNotNull(baseStateDir, "baseStateDir cannot be null"); |
| // TODO (b/120424138): Remove once the system user is migrated to use the per-user CE |
| // directory. Per-user CE directories are managed by vold. |
| if (userId == UserHandle.USER_SYSTEM) { |
| mBaseStateDir.mkdirs(); |
| if (!SELinux.restorecon(mBaseStateDir)) { |
| Slog.w(TAG, "SELinux restorecon failed on " + mBaseStateDir); |
| } |
| } |
| |
| // TODO (b/120424138): The system user currently uses the cache which is managed by init.rc |
| // Initialization and restorecon is managed by vold for per-user CE directories. |
| mDataDir = checkNotNull(dataDir, "dataDir cannot be null"); |
| mBackupPasswordManager = new BackupPasswordManager(mContext, mBaseStateDir, mRng); |
| |
| // Receivers for scheduled backups and transport initialization operations. |
| BroadcastReceiver runBackupReceiver = new RunBackupReceiver(this); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(RUN_BACKUP_ACTION); |
| context.registerReceiverAsUser( |
| runBackupReceiver, |
| UserHandle.of(userId), |
| filter, |
| android.Manifest.permission.BACKUP, |
| /* scheduler */ null); |
| |
| BroadcastReceiver runInitReceiver = new RunInitializeReceiver(this); |
| filter = new IntentFilter(); |
| filter.addAction(RUN_INITIALIZE_ACTION); |
| context.registerReceiverAsUser( |
| runInitReceiver, |
| UserHandle.of(userId), |
| filter, |
| android.Manifest.permission.BACKUP, |
| /* scheduler */ null); |
| |
| Intent backupIntent = new Intent(RUN_BACKUP_ACTION); |
| backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); |
| mRunBackupIntent = |
| PendingIntent.getBroadcastAsUser( |
| context, |
| /* requestCode */ 0, |
| backupIntent, |
| /* flags */ 0, |
| UserHandle.of(userId)); |
| |
| Intent initIntent = new Intent(RUN_INITIALIZE_ACTION); |
| initIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); |
| mRunInitIntent = |
| PendingIntent.getBroadcastAsUser( |
| context, |
| /* requestCode */ 0, |
| initIntent, |
| /* flags */ 0, |
| UserHandle.of(userId)); |
| |
| // Set up the backup-request journaling |
| mJournalDir = new File(mBaseStateDir, "pending"); |
| mJournalDir.mkdirs(); // creates mBaseStateDir along the way |
| mJournal = null; // will be created on first use |
| |
| mConstants = new BackupManagerConstants(mBackupHandler, mContext.getContentResolver()); |
| // We are observing changes to the constants throughout the lifecycle of BMS. This is |
| // because we reference the constants in multiple areas of BMS, which otherwise would |
| // require frequent starting and stopping. |
| mConstants.start(); |
| |
| // Set up the various sorts of package tracking we do |
| mFullBackupScheduleFile = new File(mBaseStateDir, "fb-schedule"); |
| initPackageTracking(); |
| |
| // Build our mapping of uid to backup client services. This implicitly |
| // schedules a backup pass on the Package Manager metadata the first |
| // time anything needs to be backed up. |
| synchronized (mBackupParticipants) { |
| addPackageParticipantsLocked(null); |
| } |
| |
| mTransportManager = checkNotNull(transportManager, "transportManager cannot be null"); |
| mTransportManager.setOnTransportRegisteredListener(this::onTransportRegistered); |
| mRegisterTransportsRequestedTime = SystemClock.elapsedRealtime(); |
| mBackupHandler.postDelayed( |
| mTransportManager::registerTransports, INITIALIZATION_DELAY_MILLIS); |
| |
| // Now that we know about valid backup participants, parse any leftover journal files into |
| // the pending backup set |
| mBackupHandler.postDelayed(this::parseLeftoverJournals, INITIALIZATION_DELAY_MILLIS); |
| |
| // Power management |
| mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*-" + userId); |
| } |
| |
| void initializeBackupEnableState() { |
| boolean isEnabled = UserBackupManagerFilePersistedSettings.readBackupEnableState(mUserId); |
| setBackupEnabled(isEnabled); |
| } |
| |
| /** Cleans up state when the user of this service is stopped. */ |
| void tearDownService() { |
| mUserBackupThread.quit(); |
| } |
| |
| public @UserIdInt int getUserId() { |
| return mUserId; |
| } |
| |
| public BackupManagerConstants getConstants() { |
| return mConstants; |
| } |
| |
| public BackupAgentTimeoutParameters getAgentTimeoutParameters() { |
| return mAgentTimeoutParameters; |
| } |
| |
| public Context getContext() { |
| return mContext; |
| } |
| |
| public PackageManager getPackageManager() { |
| return mPackageManager; |
| } |
| |
| public IPackageManager getPackageManagerBinder() { |
| return mPackageManagerBinder; |
| } |
| |
| public IActivityManager getActivityManager() { |
| return mActivityManager; |
| } |
| |
| public AlarmManager getAlarmManager() { |
| return mAlarmManager; |
| } |
| |
| @VisibleForTesting |
| void setPowerManager(PowerManager powerManager) { |
| mPowerManager = powerManager; |
| } |
| |
| public TransportManager getTransportManager() { |
| return mTransportManager; |
| } |
| |
| public boolean isEnabled() { |
| return mEnabled; |
| } |
| |
| public void setEnabled(boolean enabled) { |
| mEnabled = enabled; |
| } |
| |
| public boolean isSetupComplete() { |
| return mSetupComplete; |
| } |
| |
| public void setSetupComplete(boolean setupComplete) { |
| mSetupComplete = setupComplete; |
| } |
| |
| public PowerManager.WakeLock getWakelock() { |
| return mWakelock; |
| } |
| |
| /** |
| * Sets the {@link WorkSource} of the {@link PowerManager.WakeLock} returned by {@link |
| * #getWakelock()}. |
| */ |
| @VisibleForTesting |
| public void setWorkSource(@Nullable WorkSource workSource) { |
| // TODO: This is for testing, unfortunately WakeLock is final and WorkSource is not exposed |
| mWakelock.setWorkSource(workSource); |
| } |
| |
| public Handler getBackupHandler() { |
| return mBackupHandler; |
| } |
| |
| public PendingIntent getRunInitIntent() { |
| return mRunInitIntent; |
| } |
| |
| public HashMap<String, BackupRequest> getPendingBackups() { |
| return mPendingBackups; |
| } |
| |
| public Object getQueueLock() { |
| return mQueueLock; |
| } |
| |
| public boolean isBackupRunning() { |
| return mBackupRunning; |
| } |
| |
| public void setBackupRunning(boolean backupRunning) { |
| mBackupRunning = backupRunning; |
| } |
| |
| public void setLastBackupPass(long lastBackupPass) { |
| mLastBackupPass = lastBackupPass; |
| } |
| |
| public Object getClearDataLock() { |
| return mClearDataLock; |
| } |
| |
| public void setClearingData(boolean clearingData) { |
| mClearingData = clearingData; |
| } |
| |
| public boolean isRestoreInProgress() { |
| return mIsRestoreInProgress; |
| } |
| |
| public void setRestoreInProgress(boolean restoreInProgress) { |
| mIsRestoreInProgress = restoreInProgress; |
| } |
| |
| public Queue<PerformUnifiedRestoreTask> getPendingRestores() { |
| return mPendingRestores; |
| } |
| |
| public ActiveRestoreSession getActiveRestoreSession() { |
| return mActiveRestoreSession; |
| } |
| |
| public SparseArray<Operation> getCurrentOperations() { |
| return mCurrentOperations; |
| } |
| |
| public Object getCurrentOpLock() { |
| return mCurrentOpLock; |
| } |
| |
| public SparseArray<AdbParams> getAdbBackupRestoreConfirmations() { |
| return mAdbBackupRestoreConfirmations; |
| } |
| |
| public File getBaseStateDir() { |
| return mBaseStateDir; |
| } |
| |
| public File getDataDir() { |
| return mDataDir; |
| } |
| |
| @Nullable |
| public DataChangedJournal getJournal() { |
| return mJournal; |
| } |
| |
| public void setJournal(@Nullable DataChangedJournal journal) { |
| mJournal = journal; |
| } |
| |
| public SecureRandom getRng() { |
| return mRng; |
| } |
| |
| public void setAncestralPackages(Set<String> ancestralPackages) { |
| mAncestralPackages = ancestralPackages; |
| } |
| |
| public void setAncestralToken(long ancestralToken) { |
| mAncestralToken = ancestralToken; |
| } |
| |
| public long getCurrentToken() { |
| return mCurrentToken; |
| } |
| |
| public void setCurrentToken(long currentToken) { |
| mCurrentToken = currentToken; |
| } |
| |
| public ArraySet<String> getPendingInits() { |
| return mPendingInits; |
| } |
| |
| /** Clear all pending transport initializations. */ |
| public void clearPendingInits() { |
| mPendingInits.clear(); |
| } |
| |
| public PerformFullTransportBackupTask getRunningFullBackupTask() { |
| return mRunningFullBackupTask; |
| } |
| |
| public void setRunningFullBackupTask( |
| PerformFullTransportBackupTask runningFullBackupTask) { |
| mRunningFullBackupTask = runningFullBackupTask; |
| } |
| |
| /** |
| * Utility: build a new random integer token. The low bits are the ordinal of the operation for |
| * near-time uniqueness, and the upper bits are random for app-side unpredictability. |
| */ |
| public int generateRandomIntegerToken() { |
| int token = mTokenGenerator.nextInt(); |
| if (token < 0) token = -token; |
| token &= ~0xFF; |
| token |= (mNextToken.incrementAndGet() & 0xFF); |
| return token; |
| } |
| |
| /** |
| * Construct a backup agent instance for the metadata pseudopackage. This is a process-local |
| * non-lifecycle agent instance, so we manually set up the context topology for it. |
| */ |
| public BackupAgent makeMetadataAgent() { |
| PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent(mPackageManager, mUserId); |
| pmAgent.attach(mContext); |
| pmAgent.onCreate(UserHandle.of(mUserId)); |
| return pmAgent; |
| } |
| |
| /** |
| * Same as {@link #makeMetadataAgent()} but with explicit package-set configuration. |
| */ |
| public PackageManagerBackupAgent makeMetadataAgent(List<PackageInfo> packages) { |
| PackageManagerBackupAgent pmAgent = |
| new PackageManagerBackupAgent(mPackageManager, packages, mUserId); |
| pmAgent.attach(mContext); |
| pmAgent.onCreate(UserHandle.of(mUserId)); |
| return pmAgent; |
| } |
| |
| private void initPackageTracking() { |
| if (MORE_DEBUG) Slog.v(TAG, "` tracking"); |
| |
| // Remember our ancestral dataset |
| mTokenFile = new File(mBaseStateDir, "ancestral"); |
| try (DataInputStream tokenStream = new DataInputStream(new BufferedInputStream( |
| new FileInputStream(mTokenFile)))) { |
| int version = tokenStream.readInt(); |
| if (version == CURRENT_ANCESTRAL_RECORD_VERSION) { |
| mAncestralToken = tokenStream.readLong(); |
| mCurrentToken = tokenStream.readLong(); |
| |
| int numPackages = tokenStream.readInt(); |
| if (numPackages >= 0) { |
| mAncestralPackages = new HashSet<>(); |
| for (int i = 0; i < numPackages; i++) { |
| String pkgName = tokenStream.readUTF(); |
| mAncestralPackages.add(pkgName); |
| } |
| } |
| } |
| } catch (FileNotFoundException fnf) { |
| // Probably innocuous |
| Slog.v(TAG, "No ancestral data"); |
| } catch (IOException e) { |
| Slog.w(TAG, "Unable to read token file", e); |
| } |
| |
| mProcessedPackagesJournal = new ProcessedPackagesJournal(mBaseStateDir); |
| mProcessedPackagesJournal.init(); |
| |
| synchronized (mQueueLock) { |
| // Resume the full-data backup queue |
| mFullBackupQueue = readFullBackupSchedule(); |
| } |
| |
| // Register for broadcasts about package changes. |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_PACKAGE_ADDED); |
| filter.addAction(Intent.ACTION_PACKAGE_REMOVED); |
| filter.addAction(Intent.ACTION_PACKAGE_CHANGED); |
| filter.addDataScheme("package"); |
| mContext.registerReceiverAsUser( |
| mBroadcastReceiver, |
| UserHandle.of(mUserId), |
| filter, |
| /* broadcastPermission */ null, |
| /* scheduler */ null); |
| |
| // Register for events related to sdcard installation. |
| IntentFilter sdFilter = new IntentFilter(); |
| sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); |
| sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); |
| mContext.registerReceiverAsUser( |
| mBroadcastReceiver, |
| UserHandle.of(mUserId), |
| sdFilter, |
| /* broadcastPermission */ null, |
| /* scheduler */ null); |
| } |
| |
| private ArrayList<FullBackupEntry> readFullBackupSchedule() { |
| boolean changed = false; |
| ArrayList<FullBackupEntry> schedule = null; |
| List<PackageInfo> apps = |
| PackageManagerBackupAgent.getStorableApplications(mPackageManager, mUserId); |
| |
| if (mFullBackupScheduleFile.exists()) { |
| try (FileInputStream fstream = new FileInputStream(mFullBackupScheduleFile); |
| BufferedInputStream bufStream = new BufferedInputStream(fstream); |
| DataInputStream in = new DataInputStream(bufStream)) { |
| int version = in.readInt(); |
| if (version != SCHEDULE_FILE_VERSION) { |
| Slog.e(TAG, "Unknown backup schedule version " + version); |
| return null; |
| } |
| |
| final int numPackages = in.readInt(); |
| schedule = new ArrayList<>(numPackages); |
| |
| // HashSet instead of ArraySet specifically because we want the eventual |
| // lookups against O(hundreds) of entries to be as fast as possible, and |
| // we discard the set immediately after the scan so the extra memory |
| // overhead is transient. |
| HashSet<String> foundApps = new HashSet<>(numPackages); |
| |
| for (int i = 0; i < numPackages; i++) { |
| String pkgName = in.readUTF(); |
| long lastBackup = in.readLong(); |
| foundApps.add(pkgName); // all apps that we've addressed already |
| try { |
| PackageInfo pkg = mPackageManager.getPackageInfoAsUser(pkgName, 0, mUserId); |
| if (AppBackupUtils.appGetsFullBackup(pkg) |
| && AppBackupUtils.appIsEligibleForBackup(pkg.applicationInfo, |
| mUserId)) { |
| schedule.add(new FullBackupEntry(pkgName, lastBackup)); |
| } else { |
| if (DEBUG) { |
| Slog.i(TAG, "Package " + pkgName |
| + " no longer eligible for full backup"); |
| } |
| } |
| } catch (NameNotFoundException e) { |
| if (DEBUG) { |
| Slog.i(TAG, "Package " + pkgName |
| + " not installed; dropping from full backup"); |
| } |
| } |
| } |
| |
| // New apps can arrive "out of band" via OTA and similar, so we also need to |
| // scan to make sure that we're tracking all full-backup candidates properly |
| for (PackageInfo app : apps) { |
| if (AppBackupUtils.appGetsFullBackup(app) |
| && AppBackupUtils.appIsEligibleForBackup(app.applicationInfo, |
| mUserId)) { |
| if (!foundApps.contains(app.packageName)) { |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "New full backup app " + app.packageName + " found"); |
| } |
| schedule.add(new FullBackupEntry(app.packageName, 0)); |
| changed = true; |
| } |
| } |
| } |
| |
| Collections.sort(schedule); |
| } catch (Exception e) { |
| Slog.e(TAG, "Unable to read backup schedule", e); |
| mFullBackupScheduleFile.delete(); |
| schedule = null; |
| } |
| } |
| |
| if (schedule == null) { |
| // no prior queue record, or unable to read it. Set up the queue |
| // from scratch. |
| changed = true; |
| schedule = new ArrayList<>(apps.size()); |
| for (PackageInfo info : apps) { |
| if (AppBackupUtils.appGetsFullBackup(info) && AppBackupUtils.appIsEligibleForBackup( |
| info.applicationInfo, mUserId)) { |
| schedule.add(new FullBackupEntry(info.packageName, 0)); |
| } |
| } |
| } |
| |
| if (changed) { |
| writeFullBackupScheduleAsync(); |
| } |
| return schedule; |
| } |
| |
| private Runnable mFullBackupScheduleWriter = new Runnable() { |
| @Override |
| public void run() { |
| synchronized (mQueueLock) { |
| try { |
| ByteArrayOutputStream bufStream = new ByteArrayOutputStream(4096); |
| DataOutputStream bufOut = new DataOutputStream(bufStream); |
| bufOut.writeInt(SCHEDULE_FILE_VERSION); |
| |
| // version 1: |
| // |
| // [int] # of packages in the queue = N |
| // N * { |
| // [utf8] package name |
| // [long] last backup time for this package |
| // } |
| int numPackages = mFullBackupQueue.size(); |
| bufOut.writeInt(numPackages); |
| |
| for (int i = 0; i < numPackages; i++) { |
| FullBackupEntry entry = mFullBackupQueue.get(i); |
| bufOut.writeUTF(entry.packageName); |
| bufOut.writeLong(entry.lastBackup); |
| } |
| bufOut.flush(); |
| |
| AtomicFile af = new AtomicFile(mFullBackupScheduleFile); |
| FileOutputStream out = af.startWrite(); |
| out.write(bufStream.toByteArray()); |
| af.finishWrite(out); |
| } catch (Exception e) { |
| Slog.e(TAG, "Unable to write backup schedule!", e); |
| } |
| } |
| } |
| }; |
| |
| private void writeFullBackupScheduleAsync() { |
| mBackupHandler.removeCallbacks(mFullBackupScheduleWriter); |
| mBackupHandler.post(mFullBackupScheduleWriter); |
| } |
| |
| private void parseLeftoverJournals() { |
| ArrayList<DataChangedJournal> journals = DataChangedJournal.listJournals(mJournalDir); |
| for (DataChangedJournal journal : journals) { |
| if (!journal.equals(mJournal)) { |
| try { |
| journal.forEach(packageName -> { |
| Slog.i(TAG, "Found stale backup journal, scheduling"); |
| if (MORE_DEBUG) Slog.i(TAG, " " + packageName); |
| dataChangedImpl(packageName); |
| }); |
| } catch (IOException e) { |
| Slog.e(TAG, "Can't read " + journal, e); |
| } |
| } |
| } |
| } |
| |
| /** Used for generating random salts or passwords. */ |
| public byte[] randomBytes(int bits) { |
| byte[] array = new byte[bits / 8]; |
| mRng.nextBytes(array); |
| return array; |
| } |
| |
| /** For adb backup/restore. */ |
| public boolean setBackupPassword(String currentPw, String newPw) { |
| return mBackupPasswordManager.setBackupPassword(currentPw, newPw); |
| } |
| |
| /** For adb backup/restore. */ |
| public boolean hasBackupPassword() { |
| return mBackupPasswordManager.hasBackupPassword(); |
| } |
| |
| /** For adb backup/restore. */ |
| public boolean backupPasswordMatches(String currentPw) { |
| return mBackupPasswordManager.backupPasswordMatches(currentPw); |
| } |
| |
| /** |
| * Maintain persistent state around whether need to do an initialize operation. This will lock |
| * on {@link #getQueueLock()}. |
| */ |
| public void recordInitPending( |
| boolean isPending, String transportName, String transportDirName) { |
| synchronized (mQueueLock) { |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "recordInitPending(" + isPending + ") on transport " + transportName); |
| } |
| |
| File stateDir = new File(mBaseStateDir, transportDirName); |
| File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME); |
| |
| if (isPending) { |
| // We need an init before we can proceed with sending backup data. |
| // Record that with an entry in our set of pending inits, as well as |
| // journaling it via creation of a sentinel file. |
| mPendingInits.add(transportName); |
| try { |
| (new FileOutputStream(initPendingFile)).close(); |
| } catch (IOException ioe) { |
| // Something is badly wrong with our permissions; just try to move on |
| } |
| } else { |
| // No more initialization needed; wipe the journal and reset our state. |
| initPendingFile.delete(); |
| mPendingInits.remove(transportName); |
| } |
| } |
| } |
| |
| /** |
| * Reset all of our bookkeeping because the backend data has been wiped (for example due to idle |
| * expiry), so we must re-upload all saved settings. |
| */ |
| public void resetBackupState(File stateFileDir) { |
| synchronized (mQueueLock) { |
| mProcessedPackagesJournal.reset(); |
| |
| mCurrentToken = 0; |
| writeRestoreTokens(); |
| |
| // Remove all the state files |
| for (File sf : stateFileDir.listFiles()) { |
| // ... but don't touch the needs-init sentinel |
| if (!sf.getName().equals(INIT_SENTINEL_FILE_NAME)) { |
| sf.delete(); |
| } |
| } |
| } |
| |
| // Enqueue a new backup of every participant |
| synchronized (mBackupParticipants) { |
| final int numParticipants = mBackupParticipants.size(); |
| for (int i = 0; i < numParticipants; i++) { |
| HashSet<String> participants = mBackupParticipants.valueAt(i); |
| if (participants != null) { |
| for (String packageName : participants) { |
| dataChangedImpl(packageName); |
| } |
| } |
| } |
| } |
| } |
| |
| private void onTransportRegistered(String transportName, String transportDirName) { |
| if (DEBUG) { |
| long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime; |
| Slog.d(TAG, "Transport " + transportName + " registered " + timeMs |
| + "ms after first request (delay = " + INITIALIZATION_DELAY_MILLIS + "ms)"); |
| } |
| |
| File stateDir = new File(mBaseStateDir, transportDirName); |
| stateDir.mkdirs(); |
| |
| File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME); |
| if (initSentinel.exists()) { |
| synchronized (mQueueLock) { |
| mPendingInits.add(transportName); |
| |
| // TODO: pick a better starting time than now + 1 minute |
| long delay = 1000 * 60; // one minute, in milliseconds |
| mAlarmManager.set(AlarmManager.RTC_WAKEUP, |
| System.currentTimeMillis() + delay, mRunInitIntent); |
| } |
| } |
| } |
| |
| /** |
| * A {@link BroadcastReceiver} tracking changes to packages and sd cards in order to update our |
| * internal bookkeeping. |
| */ |
| private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| public void onReceive(Context context, Intent intent) { |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "Received broadcast " + intent); |
| } |
| |
| String action = intent.getAction(); |
| boolean replacing = false; |
| boolean added = false; |
| boolean changed = false; |
| Bundle extras = intent.getExtras(); |
| String[] packageList = null; |
| |
| if (Intent.ACTION_PACKAGE_ADDED.equals(action) |
| || Intent.ACTION_PACKAGE_REMOVED.equals(action) |
| || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { |
| Uri uri = intent.getData(); |
| if (uri == null) { |
| return; |
| } |
| |
| String packageName = uri.getSchemeSpecificPart(); |
| if (packageName != null) { |
| packageList = new String[]{packageName}; |
| } |
| |
| changed = Intent.ACTION_PACKAGE_CHANGED.equals(action); |
| if (changed) { |
| // Look at new transport states for package changed events. |
| String[] components = |
| intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); |
| |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Package " + packageName + " changed"); |
| for (int i = 0; i < components.length; i++) { |
| Slog.i(TAG, " * " + components[i]); |
| } |
| } |
| |
| mBackupHandler.post( |
| () -> mTransportManager.onPackageChanged(packageName, components)); |
| return; |
| } |
| |
| added = Intent.ACTION_PACKAGE_ADDED.equals(action); |
| replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false); |
| } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { |
| added = true; |
| packageList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); |
| } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { |
| added = false; |
| packageList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); |
| } |
| |
| if (packageList == null || packageList.length == 0) { |
| return; |
| } |
| |
| int uid = extras.getInt(Intent.EXTRA_UID); |
| if (added) { |
| synchronized (mBackupParticipants) { |
| if (replacing) { |
| // Remove the entry under the old uid and fall through to re-add. If an app |
| // just opted into key/value backup, add it as a known participant. |
| removePackageParticipantsLocked(packageList, uid); |
| } |
| addPackageParticipantsLocked(packageList); |
| } |
| |
| long now = System.currentTimeMillis(); |
| for (String packageName : packageList) { |
| try { |
| PackageInfo app = |
| mPackageManager.getPackageInfoAsUser( |
| packageName, /* flags */ 0, mUserId); |
| if (AppBackupUtils.appGetsFullBackup(app) |
| && AppBackupUtils.appIsEligibleForBackup(app.applicationInfo, |
| mUserId)) { |
| enqueueFullBackup(packageName, now); |
| scheduleNextFullBackupJob(0); |
| } else { |
| // The app might have just transitioned out of full-data into doing |
| // key/value backups, or might have just disabled backups entirely. Make |
| // sure it is no longer in the full-data queue. |
| synchronized (mQueueLock) { |
| dequeueFullBackupLocked(packageName); |
| } |
| writeFullBackupScheduleAsync(); |
| } |
| |
| mBackupHandler.post( |
| () -> mTransportManager.onPackageAdded(packageName)); |
| } catch (NameNotFoundException e) { |
| if (DEBUG) { |
| Slog.w(TAG, "Can't resolve new app " + packageName); |
| } |
| } |
| } |
| |
| // Whenever a package is added or updated we need to update the package metadata |
| // bookkeeping. |
| dataChangedImpl(PACKAGE_MANAGER_SENTINEL); |
| } else { |
| if (!replacing) { |
| // Outright removal. In the full-data case, the app will be dropped from the |
| // queue when its (now obsolete) name comes up again for backup. |
| synchronized (mBackupParticipants) { |
| removePackageParticipantsLocked(packageList, uid); |
| } |
| } |
| |
| for (String packageName : packageList) { |
| mBackupHandler.post( |
| () -> mTransportManager.onPackageRemoved(packageName)); |
| } |
| } |
| } |
| }; |
| |
| // Add the backup agents in the given packages to our set of known backup participants. |
| // If 'packageNames' is null, adds all backup agents in the whole system. |
| private void addPackageParticipantsLocked(String[] packageNames) { |
| // Look for apps that define the android:backupAgent attribute |
| List<PackageInfo> targetApps = allAgentPackages(); |
| if (packageNames != null) { |
| if (MORE_DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: #" + packageNames.length); |
| for (String packageName : packageNames) { |
| addPackageParticipantsLockedInner(packageName, targetApps); |
| } |
| } else { |
| if (MORE_DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: all"); |
| addPackageParticipantsLockedInner(null, targetApps); |
| } |
| } |
| |
| private void addPackageParticipantsLockedInner(String packageName, |
| List<PackageInfo> targetPkgs) { |
| if (MORE_DEBUG) { |
| Slog.v(TAG, "Examining " + packageName + " for backup agent"); |
| } |
| |
| for (PackageInfo pkg : targetPkgs) { |
| if (packageName == null || pkg.packageName.equals(packageName)) { |
| int uid = pkg.applicationInfo.uid; |
| HashSet<String> set = mBackupParticipants.get(uid); |
| if (set == null) { |
| set = new HashSet<>(); |
| mBackupParticipants.put(uid, set); |
| } |
| set.add(pkg.packageName); |
| if (MORE_DEBUG) Slog.v(TAG, "Agent found; added"); |
| |
| // Schedule a backup for it on general principles |
| if (MORE_DEBUG) Slog.i(TAG, "Scheduling backup for new app " + pkg.packageName); |
| Message msg = mBackupHandler |
| .obtainMessage(MSG_SCHEDULE_BACKUP_PACKAGE, pkg.packageName); |
| mBackupHandler.sendMessage(msg); |
| } |
| } |
| } |
| |
| // Remove the given packages' entries from our known active set. |
| private void removePackageParticipantsLocked(String[] packageNames, int oldUid) { |
| if (packageNames == null) { |
| Slog.w(TAG, "removePackageParticipants with null list"); |
| return; |
| } |
| |
| if (MORE_DEBUG) { |
| Slog.v(TAG, "removePackageParticipantsLocked: uid=" + oldUid |
| + " #" + packageNames.length); |
| } |
| for (String pkg : packageNames) { |
| // Known previous UID, so we know which package set to check |
| HashSet<String> set = mBackupParticipants.get(oldUid); |
| if (set != null && set.contains(pkg)) { |
| removePackageFromSetLocked(set, pkg); |
| if (set.isEmpty()) { |
| if (MORE_DEBUG) Slog.v(TAG, " last one of this uid; purging set"); |
| mBackupParticipants.remove(oldUid); |
| } |
| } |
| } |
| } |
| |
| private void removePackageFromSetLocked(final HashSet<String> set, |
| final String packageName) { |
| if (set.contains(packageName)) { |
| // Found it. Remove this one package from the bookkeeping, and |
| // if it's the last participating app under this uid we drop the |
| // (now-empty) set as well. |
| // Note that we deliberately leave it 'known' in the "ever backed up" |
| // bookkeeping so that its current-dataset data will be retrieved |
| // if the app is subsequently reinstalled |
| if (MORE_DEBUG) Slog.v(TAG, " removing participant " + packageName); |
| set.remove(packageName); |
| mPendingBackups.remove(packageName); |
| } |
| } |
| |
| // Returns the set of all applications that define an android:backupAgent attribute |
| private List<PackageInfo> allAgentPackages() { |
| // !!! TODO: cache this and regenerate only when necessary |
| int flags = PackageManager.GET_SIGNING_CERTIFICATES; |
| List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(flags, mUserId); |
| int numPackages = packages.size(); |
| for (int a = numPackages - 1; a >= 0; a--) { |
| PackageInfo pkg = packages.get(a); |
| try { |
| ApplicationInfo app = pkg.applicationInfo; |
| if (((app.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) |
| || app.backupAgentName == null |
| || (app.flags & ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0) { |
| packages.remove(a); |
| } else { |
| // we will need the shared library path, so look that up and store it here. |
| // This is used implicitly when we pass the PackageInfo object off to |
| // the Activity Manager to launch the app for backup/restore purposes. |
| app = mPackageManager.getApplicationInfoAsUser(pkg.packageName, |
| PackageManager.GET_SHARED_LIBRARY_FILES, mUserId); |
| pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles; |
| pkg.applicationInfo.sharedLibraryInfos = app.sharedLibraryInfos; |
| } |
| } catch (NameNotFoundException e) { |
| packages.remove(a); |
| } |
| } |
| return packages; |
| } |
| |
| /** |
| * Called from the backup tasks: record that the given app has been successfully backed up at |
| * least once. This includes both key/value and full-data backups through the transport. |
| */ |
| public void logBackupComplete(String packageName) { |
| if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return; |
| |
| for (String receiver : mConstants.getBackupFinishedNotificationReceivers()) { |
| final Intent notification = new Intent(); |
| notification.setAction(BACKUP_FINISHED_ACTION); |
| notification.setPackage(receiver); |
| notification.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES |
| | Intent.FLAG_RECEIVER_FOREGROUND); |
| notification.putExtra(BACKUP_FINISHED_PACKAGE_EXTRA, packageName); |
| mContext.sendBroadcastAsUser(notification, UserHandle.of(mUserId)); |
| } |
| |
| mProcessedPackagesJournal.addPackage(packageName); |
| } |
| |
| /** |
| * Persistently record the current and ancestral backup tokens, as well as the set of packages |
| * with data available in the ancestral dataset. |
| */ |
| public void writeRestoreTokens() { |
| try (RandomAccessFile af = new RandomAccessFile(mTokenFile, "rwd")) { |
| // First, the version number of this record, for futureproofing |
| af.writeInt(CURRENT_ANCESTRAL_RECORD_VERSION); |
| |
| // Write the ancestral and current tokens |
| af.writeLong(mAncestralToken); |
| af.writeLong(mCurrentToken); |
| |
| // Now write the set of ancestral packages |
| if (mAncestralPackages == null) { |
| af.writeInt(-1); |
| } else { |
| af.writeInt(mAncestralPackages.size()); |
| if (DEBUG) Slog.v(TAG, "Ancestral packages: " + mAncestralPackages.size()); |
| for (String pkgName : mAncestralPackages) { |
| af.writeUTF(pkgName); |
| if (MORE_DEBUG) Slog.v(TAG, " " + pkgName); |
| } |
| } |
| } catch (IOException e) { |
| Slog.w(TAG, "Unable to write token file:", e); |
| } |
| } |
| |
| /** Fires off a backup agent, blocking until it attaches or times out. */ |
| @Nullable |
| public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) { |
| IBackupAgent agent = null; |
| synchronized (mAgentConnectLock) { |
| mConnecting = true; |
| mConnectedAgent = null; |
| try { |
| if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId)) { |
| Slog.d(TAG, "awaiting agent for " + app); |
| |
| // success; wait for the agent to arrive |
| // only wait 10 seconds for the bind to happen |
| long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; |
| while (mConnecting && mConnectedAgent == null |
| && (System.currentTimeMillis() < timeoutMark)) { |
| try { |
| mAgentConnectLock.wait(5000); |
| } catch (InterruptedException e) { |
| // just bail |
| Slog.w(TAG, "Interrupted: " + e); |
| mConnecting = false; |
| mConnectedAgent = null; |
| } |
| } |
| |
| // if we timed out with no connect, abort and move on |
| if (mConnecting) { |
| Slog.w(TAG, "Timeout waiting for agent " + app); |
| mConnectedAgent = null; |
| } |
| if (DEBUG) Slog.i(TAG, "got agent " + mConnectedAgent); |
| agent = mConnectedAgent; |
| } |
| } catch (RemoteException e) { |
| // can't happen - ActivityManager is local |
| } |
| } |
| if (agent == null) { |
| mActivityManagerInternal.clearPendingBackup(mUserId); |
| } |
| return agent; |
| } |
| |
| /** Unbind from a backup agent. */ |
| public void unbindAgent(ApplicationInfo app) { |
| try { |
| mActivityManager.unbindBackupAgent(app); |
| } catch (RemoteException e) { |
| // Can't happen - activity manager is local |
| } |
| } |
| |
| /** |
| * Clear an application's data after a failed restore, blocking until the operation completes or |
| * times out. |
| */ |
| public void clearApplicationDataAfterRestoreFailure(String packageName) { |
| clearApplicationDataSynchronous(packageName, true, false); |
| } |
| |
| /** |
| * Clear an application's data before restore, blocking until the operation completes or times |
| * out. |
| */ |
| public void clearApplicationDataBeforeRestore(String packageName) { |
| clearApplicationDataSynchronous(packageName, false, true); |
| } |
| |
| /** |
| * Clear an application's data, blocking until the operation completes or times out. |
| * |
| * @param checkFlagAllowClearUserDataOnFailedRestore if {@code true} uses |
| * {@link ApplicationInfo#PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE} to decide if |
| * clearing data is allowed after a failed restore. |
| * |
| * @param keepSystemState if {@code true}, we don't clear system state such as already restored |
| * notification settings, permission grants, etc. |
| */ |
| private void clearApplicationDataSynchronous(String packageName, |
| boolean checkFlagAllowClearUserDataOnFailedRestore, boolean keepSystemState) { |
| try { |
| ApplicationInfo applicationInfo = mPackageManager.getPackageInfoAsUser( |
| packageName, 0, mUserId).applicationInfo; |
| |
| boolean shouldClearData; |
| if (checkFlagAllowClearUserDataOnFailedRestore |
| && applicationInfo.targetSdkVersion >= Build.VERSION_CODES.Q) { |
| shouldClearData = (applicationInfo.privateFlags |
| & ApplicationInfo.PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE) != 0; |
| } else { |
| shouldClearData = |
| (applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) != 0; |
| } |
| |
| if (!shouldClearData) { |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Clearing app data is not allowed so not wiping " |
| + packageName); |
| } |
| return; |
| } |
| } catch (NameNotFoundException e) { |
| Slog.w(TAG, "Tried to clear data for " + packageName + " but not found"); |
| return; |
| } |
| |
| ClearDataObserver observer = new ClearDataObserver(this); |
| |
| synchronized (mClearDataLock) { |
| mClearingData = true; |
| try { |
| mActivityManager.clearApplicationUserData(packageName, keepSystemState, observer, |
| mUserId); |
| } catch (RemoteException e) { |
| // can't happen because the activity manager is in this process |
| } |
| |
| // only wait 10 seconds for the clear data to happen |
| long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; |
| while (mClearingData && (System.currentTimeMillis() < timeoutMark)) { |
| try { |
| mClearDataLock.wait(5000); |
| } catch (InterruptedException e) { |
| // won't happen, but still. |
| mClearingData = false; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Get the restore-set token for the best-available restore set for this {@code packageName}: |
| * the active set if possible, else the ancestral one. Returns zero if none available. |
| */ |
| public long getAvailableRestoreToken(String packageName) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "getAvailableRestoreToken"); |
| |
| long token = mAncestralToken; |
| synchronized (mQueueLock) { |
| if (mCurrentToken != 0 && mProcessedPackagesJournal.hasBeenProcessed(packageName)) { |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "App in ever-stored, so using current token"); |
| } |
| token = mCurrentToken; |
| } |
| } |
| if (MORE_DEBUG) Slog.i(TAG, "getAvailableRestoreToken() == " + token); |
| return token; |
| } |
| |
| /** |
| * Requests a backup for the inputted {@code packages}. |
| * |
| * @see #requestBackup(String[], IBackupObserver, IBackupManagerMonitor, int). |
| */ |
| public int requestBackup(String[] packages, IBackupObserver observer, int flags) { |
| return requestBackup(packages, observer, null, flags); |
| } |
| |
| /** |
| * Requests a backup for the inputted {@code packages} with a specified {@link |
| * IBackupManagerMonitor}. |
| */ |
| public int requestBackup(String[] packages, IBackupObserver observer, |
| IBackupManagerMonitor monitor, int flags) { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "requestBackup"); |
| |
| if (packages == null || packages.length < 1) { |
| Slog.e(TAG, "No packages named for backup request"); |
| BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED); |
| monitor = BackupManagerMonitorUtils.monitorEvent(monitor, |
| BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES, |
| null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null); |
| throw new IllegalArgumentException("No packages are provided for backup"); |
| } |
| |
| if (!mEnabled || !mSetupComplete) { |
| Slog.i( |
| TAG, |
| "Backup requested but enabled=" |
| + mEnabled |
| + " setupComplete=" |
| + mSetupComplete); |
| BackupObserverUtils.sendBackupFinished(observer, |
| BackupManager.ERROR_BACKUP_NOT_ALLOWED); |
| final int logTag = mSetupComplete |
| ? BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED |
| : BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED; |
| monitor = BackupManagerMonitorUtils.monitorEvent(monitor, logTag, null, |
| BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); |
| return BackupManager.ERROR_BACKUP_NOT_ALLOWED; |
| } |
| |
| final TransportClient transportClient; |
| final String transportDirName; |
| try { |
| transportDirName = |
| mTransportManager.getTransportDirName( |
| mTransportManager.getCurrentTransportName()); |
| transportClient = |
| mTransportManager.getCurrentTransportClientOrThrow("BMS.requestBackup()"); |
| } catch (TransportNotRegisteredException e) { |
| BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED); |
| monitor = BackupManagerMonitorUtils.monitorEvent(monitor, |
| BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL, |
| null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null); |
| return BackupManager.ERROR_TRANSPORT_ABORTED; |
| } |
| |
| OnTaskFinishedListener listener = |
| caller -> mTransportManager.disposeOfTransportClient(transportClient, caller); |
| |
| ArrayList<String> fullBackupList = new ArrayList<>(); |
| ArrayList<String> kvBackupList = new ArrayList<>(); |
| for (String packageName : packages) { |
| if (PACKAGE_MANAGER_SENTINEL.equals(packageName)) { |
| kvBackupList.add(packageName); |
| continue; |
| } |
| try { |
| PackageInfo packageInfo = mPackageManager.getPackageInfoAsUser(packageName, |
| PackageManager.GET_SIGNING_CERTIFICATES, mUserId); |
| if (!AppBackupUtils.appIsEligibleForBackup(packageInfo.applicationInfo, mUserId)) { |
| BackupObserverUtils.sendBackupOnPackageResult(observer, packageName, |
| BackupManager.ERROR_BACKUP_NOT_ALLOWED); |
| continue; |
| } |
| if (AppBackupUtils.appGetsFullBackup(packageInfo)) { |
| fullBackupList.add(packageInfo.packageName); |
| } else { |
| kvBackupList.add(packageInfo.packageName); |
| } |
| } catch (NameNotFoundException e) { |
| BackupObserverUtils.sendBackupOnPackageResult(observer, packageName, |
| BackupManager.ERROR_PACKAGE_NOT_FOUND); |
| } |
| } |
| EventLog.writeEvent(EventLogTags.BACKUP_REQUESTED, packages.length, kvBackupList.size(), |
| fullBackupList.size()); |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Backup requested for " + packages.length + " packages, of them: " |
| + fullBackupList.size() + " full backups, " + kvBackupList.size() |
| + " k/v backups"); |
| } |
| |
| boolean nonIncrementalBackup = (flags & BackupManager.FLAG_NON_INCREMENTAL_BACKUP) != 0; |
| |
| Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP); |
| msg.obj = new BackupParams(transportClient, transportDirName, kvBackupList, fullBackupList, |
| observer, monitor, listener, true, nonIncrementalBackup); |
| mBackupHandler.sendMessage(msg); |
| return BackupManager.SUCCESS; |
| } |
| |
| /** Cancel all running backups. */ |
| public void cancelBackups() { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "cancelBackups"); |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "cancelBackups() called."); |
| } |
| final long oldToken = Binder.clearCallingIdentity(); |
| try { |
| List<Integer> operationsToCancel = new ArrayList<>(); |
| synchronized (mCurrentOpLock) { |
| for (int i = 0; i < mCurrentOperations.size(); i++) { |
| Operation op = mCurrentOperations.valueAt(i); |
| int token = mCurrentOperations.keyAt(i); |
| if (op.type == OP_TYPE_BACKUP) { |
| operationsToCancel.add(token); |
| } |
| } |
| } |
| for (Integer token : operationsToCancel) { |
| handleCancel(token, true /* cancelAll */); |
| } |
| // We don't want the backup jobs to kick in any time soon. |
| // Reschedules them to run in the distant future. |
| KeyValueBackupJob.schedule(mUserId, mContext, BUSY_BACKOFF_MIN_MILLIS, mConstants); |
| FullBackupJob.schedule(mUserId, mContext, 2 * BUSY_BACKOFF_MIN_MILLIS, mConstants); |
| } finally { |
| Binder.restoreCallingIdentity(oldToken); |
| } |
| } |
| |
| /** Schedule a timeout message for the operation identified by {@code token}. */ |
| public void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback, |
| int operationType) { |
| if (operationType != OP_TYPE_BACKUP_WAIT && operationType != OP_TYPE_RESTORE_WAIT) { |
| Slog.wtf(TAG, "prepareOperationTimeout() doesn't support operation " |
| + Integer.toHexString(token) + " of type " + operationType); |
| return; |
| } |
| if (MORE_DEBUG) { |
| Slog.v(TAG, "starting timeout: token=" + Integer.toHexString(token) |
| + " interval=" + interval + " callback=" + callback); |
| } |
| |
| synchronized (mCurrentOpLock) { |
| mCurrentOperations.put(token, new Operation(OP_PENDING, callback, operationType)); |
| Message msg = mBackupHandler.obtainMessage(getMessageIdForOperationType(operationType), |
| token, 0, callback); |
| mBackupHandler.sendMessageDelayed(msg, interval); |
| } |
| } |
| |
| private int getMessageIdForOperationType(int operationType) { |
| switch (operationType) { |
| case OP_TYPE_BACKUP_WAIT: |
| return MSG_BACKUP_OPERATION_TIMEOUT; |
| case OP_TYPE_RESTORE_WAIT: |
| return MSG_RESTORE_OPERATION_TIMEOUT; |
| default: |
| Slog.wtf(TAG, "getMessageIdForOperationType called on invalid operation type: " |
| + operationType); |
| return -1; |
| } |
| } |
| |
| /** |
| * Add an operation to the list of currently running operations. Used for cancellation, |
| * completion and timeout callbacks that act on the operation via the {@code token}. |
| */ |
| public void putOperation(int token, Operation operation) { |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "Adding operation token=" + Integer.toHexString(token) + ", operation type=" |
| + operation.type); |
| } |
| synchronized (mCurrentOpLock) { |
| mCurrentOperations.put(token, operation); |
| } |
| } |
| |
| /** |
| * Remove an operation from the list of currently running operations. An operation is removed |
| * when it is completed, cancelled, or timed out, and thus no longer running. |
| */ |
| public void removeOperation(int token) { |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "Removing operation token=" + Integer.toHexString(token)); |
| } |
| synchronized (mCurrentOpLock) { |
| if (mCurrentOperations.get(token) == null) { |
| Slog.w(TAG, "Duplicate remove for operation. token=" |
| + Integer.toHexString(token)); |
| } |
| mCurrentOperations.remove(token); |
| } |
| } |
| |
| /** Block until we received an operation complete message (from the agent or cancellation). */ |
| public boolean waitUntilOperationComplete(int token) { |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Blocking until operation complete for " |
| + Integer.toHexString(token)); |
| } |
| int finalState = OP_PENDING; |
| Operation op = null; |
| synchronized (mCurrentOpLock) { |
| while (true) { |
| op = mCurrentOperations.get(token); |
| if (op == null) { |
| // mysterious disappearance: treat as success with no callback |
| break; |
| } else { |
| if (op.state == OP_PENDING) { |
| try { |
| mCurrentOpLock.wait(); |
| } catch (InterruptedException e) { |
| } |
| // When the wait is notified we loop around and recheck the current state |
| } else { |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "Unblocked waiting for operation token=" |
| + Integer.toHexString(token)); |
| } |
| // No longer pending; we're done |
| finalState = op.state; |
| break; |
| } |
| } |
| } |
| } |
| |
| removeOperation(token); |
| if (op != null) { |
| mBackupHandler.removeMessages(getMessageIdForOperationType(op.type)); |
| } |
| if (MORE_DEBUG) { |
| Slog.v(TAG, "operation " + Integer.toHexString(token) |
| + " complete: finalState=" + finalState); |
| } |
| return finalState == OP_ACKNOWLEDGED; |
| } |
| |
| /** Cancel the operation associated with {@code token}. */ |
| public void handleCancel(int token, boolean cancelAll) { |
| // Notify any synchronous waiters |
| Operation op = null; |
| synchronized (mCurrentOpLock) { |
| op = mCurrentOperations.get(token); |
| if (MORE_DEBUG) { |
| if (op == null) { |
| Slog.w(TAG, "Cancel of token " + Integer.toHexString(token) |
| + " but no op found"); |
| } |
| } |
| int state = (op != null) ? op.state : OP_TIMEOUT; |
| if (state == OP_ACKNOWLEDGED) { |
| // The operation finished cleanly, so we have nothing more to do. |
| if (DEBUG) { |
| Slog.w(TAG, "Operation already got an ack." |
| + "Should have been removed from mCurrentOperations."); |
| } |
| op = null; |
| mCurrentOperations.delete(token); |
| } else if (state == OP_PENDING) { |
| if (DEBUG) Slog.v(TAG, "Cancel: token=" + Integer.toHexString(token)); |
| op.state = OP_TIMEOUT; |
| // Can't delete op from mCurrentOperations here. waitUntilOperationComplete may be |
| // called after we receive cancel here. We need this op's state there. |
| |
| // Remove all pending timeout messages of types OP_TYPE_BACKUP_WAIT and |
| // OP_TYPE_RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and |
| // doesn't require cancellation. |
| if (op.type == OP_TYPE_BACKUP_WAIT || op.type == OP_TYPE_RESTORE_WAIT) { |
| mBackupHandler.removeMessages(getMessageIdForOperationType(op.type)); |
| } |
| } |
| mCurrentOpLock.notifyAll(); |
| } |
| |
| // If there's a TimeoutHandler for this event, call it |
| if (op != null && op.callback != null) { |
| if (MORE_DEBUG) { |
| Slog.v(TAG, " Invoking cancel on " + op.callback); |
| } |
| op.callback.handleCancel(cancelAll); |
| } |
| } |
| |
| /** Returns {@code true} if a backup is currently running, else returns {@code false}. */ |
| public boolean isBackupOperationInProgress() { |
| synchronized (mCurrentOpLock) { |
| for (int i = 0; i < mCurrentOperations.size(); i++) { |
| Operation op = mCurrentOperations.valueAt(i); |
| if (op.type == OP_TYPE_BACKUP && op.state == OP_PENDING) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** Unbind the backup agent and kill the app if it's a non-system app. */ |
| public void tearDownAgentAndKill(ApplicationInfo app) { |
| if (app == null) { |
| // Null means the system package, so just quietly move on. :) |
| return; |
| } |
| |
| try { |
| // unbind and tidy up even on timeout or failure, just in case |
| mActivityManager.unbindBackupAgent(app); |
| |
| // The agent was running with a stub Application object, so shut it down. |
| // !!! We hardcode the confirmation UI's package name here rather than use a |
| // manifest flag! TODO something less direct. |
| if (!UserHandle.isCore(app.uid) |
| && !app.packageName.equals("com.android.backupconfirm")) { |
| if (MORE_DEBUG) Slog.d(TAG, "Killing agent host process"); |
| mActivityManager.killApplicationProcess(app.processName, app.uid); |
| } else { |
| if (MORE_DEBUG) Slog.d(TAG, "Not killing after operation: " + app.processName); |
| } |
| } catch (RemoteException e) { |
| Slog.d(TAG, "Lost app trying to shut down"); |
| } |
| } |
| |
| /** For adb backup/restore. */ |
| public boolean deviceIsEncrypted() { |
| try { |
| return mStorageManager.getEncryptionState() |
| != StorageManager.ENCRYPTION_STATE_NONE |
| && mStorageManager.getPasswordType() |
| != StorageManager.CRYPT_TYPE_DEFAULT; |
| } catch (Exception e) { |
| // If we can't talk to the storagemanager service we have a serious problem; fail |
| // "secure" i.e. assuming that the device is encrypted. |
| Slog.e(TAG, "Unable to communicate with storagemanager service: " + e.getMessage()); |
| return true; |
| } |
| } |
| |
| // ----- Full-data backup scheduling ----- |
| |
| /** |
| * Schedule a job to tell us when it's a good time to run a full backup |
| */ |
| public void scheduleNextFullBackupJob(long transportMinLatency) { |
| synchronized (mQueueLock) { |
| if (mFullBackupQueue.size() > 0) { |
| // schedule the next job at the point in the future when the least-recently |
| // backed up app comes due for backup again; or immediately if it's already |
| // due. |
| final long upcomingLastBackup = mFullBackupQueue.get(0).lastBackup; |
| final long timeSinceLast = System.currentTimeMillis() - upcomingLastBackup; |
| final long interval = mConstants.getFullBackupIntervalMilliseconds(); |
| final long appLatency = (timeSinceLast < interval) ? (interval - timeSinceLast) : 0; |
| final long latency = Math.max(transportMinLatency, appLatency); |
| FullBackupJob.schedule(mUserId, mContext, latency, mConstants); |
| } else { |
| if (DEBUG_SCHEDULING) { |
| Slog.i(TAG, "Full backup queue empty; not scheduling"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Remove a package from the full-data queue. |
| */ |
| @GuardedBy("mQueueLock") |
| private void dequeueFullBackupLocked(String packageName) { |
| final int numPackages = mFullBackupQueue.size(); |
| for (int i = numPackages - 1; i >= 0; i--) { |
| final FullBackupEntry e = mFullBackupQueue.get(i); |
| if (packageName.equals(e.packageName)) { |
| mFullBackupQueue.remove(i); |
| } |
| } |
| } |
| |
| /** |
| * Enqueue full backup for the given app, with a note about when it last ran. |
| */ |
| public void enqueueFullBackup(String packageName, long lastBackedUp) { |
| FullBackupEntry newEntry = new FullBackupEntry(packageName, lastBackedUp); |
| synchronized (mQueueLock) { |
| // First, sanity check that we aren't adding a duplicate. Slow but |
| // straightforward; we'll have at most on the order of a few hundred |
| // items in this list. |
| dequeueFullBackupLocked(packageName); |
| |
| // This is also slow but easy for modest numbers of apps: work backwards |
| // from the end of the queue until we find an item whose last backup |
| // time was before this one, then insert this new entry after it. If we're |
| // adding something new we don't bother scanning, and just prepend. |
| int which = -1; |
| if (lastBackedUp > 0) { |
| for (which = mFullBackupQueue.size() - 1; which >= 0; which--) { |
| final FullBackupEntry entry = mFullBackupQueue.get(which); |
| if (entry.lastBackup <= lastBackedUp) { |
| mFullBackupQueue.add(which + 1, newEntry); |
| break; |
| } |
| } |
| } |
| if (which < 0) { |
| // this one is earlier than any existing one, so prepend |
| mFullBackupQueue.add(0, newEntry); |
| } |
| } |
| writeFullBackupScheduleAsync(); |
| } |
| |
| private boolean fullBackupAllowable(String transportName) { |
| if (!mTransportManager.isTransportRegistered(transportName)) { |
| Slog.w(TAG, "Transport not registered; full data backup not performed"); |
| return false; |
| } |
| |
| // Don't proceed unless we have already established package metadata |
| // for the current dataset via a key/value backup pass. |
| try { |
| String transportDirName = mTransportManager.getTransportDirName(transportName); |
| File stateDir = new File(mBaseStateDir, transportDirName); |
| File pmState = new File(stateDir, PACKAGE_MANAGER_SENTINEL); |
| if (pmState.length() <= 0) { |
| if (DEBUG) { |
| Slog.i(TAG, "Full backup requested but dataset not yet initialized"); |
| } |
| return false; |
| } |
| } catch (Exception e) { |
| Slog.w(TAG, "Unable to get transport name: " + e.getMessage()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Conditions are right for a full backup operation, so run one. The model we use is |
| * to perform one app backup per scheduled job execution, and to reschedule the job |
| * with zero latency as long as conditions remain right and we still have work to do. |
| * |
| * <p>This is the "start a full backup operation" entry point called by the scheduled job. |
| * |
| * @return Whether ongoing work will continue. The return value here will be passed |
| * along as the return value to the scheduled job's onStartJob() callback. |
| */ |
| public boolean beginFullBackup(FullBackupJob scheduledJob) { |
| final long now = System.currentTimeMillis(); |
| final long fullBackupInterval; |
| final long keyValueBackupInterval; |
| synchronized (mConstants) { |
| fullBackupInterval = mConstants.getFullBackupIntervalMilliseconds(); |
| keyValueBackupInterval = mConstants.getKeyValueBackupIntervalMilliseconds(); |
| } |
| FullBackupEntry entry = null; |
| long latency = fullBackupInterval; |
| |
| if (!mEnabled || !mSetupComplete) { |
| // Backups are globally disabled, so don't proceed. We also don't reschedule |
| // the job driving automatic backups; that job will be scheduled again when |
| // the user enables backup. |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "beginFullBackup but enabled=" + mEnabled |
| + " setupComplete=" + mSetupComplete + "; ignoring"); |
| } |
| return false; |
| } |
| |
| // Don't run the backup if we're in battery saver mode, but reschedule |
| // to try again in the not-so-distant future. |
| final PowerSaveState result = |
| mPowerManager.getPowerSaveState(ServiceType.FULL_BACKUP); |
| if (result.batterySaverEnabled) { |
| if (DEBUG) Slog.i(TAG, "Deferring scheduled full backups in battery saver mode"); |
| FullBackupJob.schedule(mUserId, mContext, keyValueBackupInterval, mConstants); |
| return false; |
| } |
| |
| if (DEBUG_SCHEDULING) { |
| Slog.i(TAG, "Beginning scheduled full backup operation"); |
| } |
| |
| // Great; we're able to run full backup jobs now. See if we have any work to do. |
| synchronized (mQueueLock) { |
| if (mRunningFullBackupTask != null) { |
| Slog.e(TAG, "Backup triggered but one already/still running!"); |
| return false; |
| } |
| |
| // At this point we think that we have work to do, but possibly not right now. |
| // Any exit without actually running backups will also require that we |
| // reschedule the job. |
| boolean runBackup = true; |
| boolean headBusy; |
| |
| do { |
| // Recheck each time, because culling due to ineligibility may |
| // have emptied the queue. |
| if (mFullBackupQueue.size() == 0) { |
| // no work to do so just bow out |
| if (DEBUG) { |
| Slog.i(TAG, "Backup queue empty; doing nothing"); |
| } |
| runBackup = false; |
| break; |
| } |
| |
| headBusy = false; |
| |
| String transportName = mTransportManager.getCurrentTransportName(); |
| if (!fullBackupAllowable(transportName)) { |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Preconditions not met; not running full backup"); |
| } |
| runBackup = false; |
| // Typically this means we haven't run a key/value backup yet. Back off |
| // full-backup operations by the key/value job's run interval so that |
| // next time we run, we are likely to be able to make progress. |
| latency = keyValueBackupInterval; |
| } |
| |
| if (runBackup) { |
| entry = mFullBackupQueue.get(0); |
| long timeSinceRun = now - entry.lastBackup; |
| runBackup = (timeSinceRun >= fullBackupInterval); |
| if (!runBackup) { |
| // It's too early to back up the next thing in the queue, so bow out |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Device ready but too early to back up next app"); |
| } |
| // Wait until the next app in the queue falls due for a full data backup |
| latency = fullBackupInterval - timeSinceRun; |
| break; // we know we aren't doing work yet, so bail. |
| } |
| |
| try { |
| PackageInfo appInfo = mPackageManager.getPackageInfoAsUser( |
| entry.packageName, 0, mUserId); |
| if (!AppBackupUtils.appGetsFullBackup(appInfo)) { |
| // The head app isn't supposed to get full-data backups [any more]; |
| // so we cull it and force a loop around to consider the new head |
| // app. |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Culling package " + entry.packageName |
| + " in full-backup queue but not eligible"); |
| } |
| mFullBackupQueue.remove(0); |
| headBusy = true; // force the while() condition |
| continue; |
| } |
| |
| final int privFlags = appInfo.applicationInfo.privateFlags; |
| headBusy = (privFlags & PRIVATE_FLAG_BACKUP_IN_FOREGROUND) == 0 |
| && mActivityManagerInternal.isAppForeground( |
| appInfo.applicationInfo.uid); |
| |
| if (headBusy) { |
| final long nextEligible = System.currentTimeMillis() |
| + BUSY_BACKOFF_MIN_MILLIS |
| + mTokenGenerator.nextInt(BUSY_BACKOFF_FUZZ); |
| if (DEBUG_SCHEDULING) { |
| SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| Slog.i(TAG, "Full backup time but " + entry.packageName |
| + " is busy; deferring to " |
| + sdf.format(new Date(nextEligible))); |
| } |
| // This relocates the app's entry from the head of the queue to |
| // its order-appropriate position further down, so upon looping |
| // a new candidate will be considered at the head. |
| enqueueFullBackup(entry.packageName, nextEligible - fullBackupInterval); |
| } |
| } catch (NameNotFoundException nnf) { |
| // So, we think we want to back this up, but it turns out the package |
| // in question is no longer installed. We want to drop it from the |
| // queue entirely and move on, but if there's nothing else in the queue |
| // we should bail entirely. headBusy cannot have been set to true yet. |
| runBackup = (mFullBackupQueue.size() > 1); |
| } |
| } |
| } while (headBusy); |
| |
| if (!runBackup) { |
| if (DEBUG_SCHEDULING) { |
| Slog.i(TAG, "Nothing pending full backup; rescheduling +" + latency); |
| } |
| final long deferTime = latency; // pin for the closure |
| FullBackupJob.schedule(mUserId, mContext, deferTime, mConstants); |
| return false; |
| } |
| |
| // Okay, the top thing is ready for backup now. Do it. |
| mFullBackupQueue.remove(0); |
| CountDownLatch latch = new CountDownLatch(1); |
| String[] pkg = new String[]{entry.packageName}; |
| mRunningFullBackupTask = PerformFullTransportBackupTask.newWithCurrentTransport( |
| this, |
| /* observer */ null, |
| pkg, |
| /* updateSchedule */ true, |
| scheduledJob, |
| latch, |
| /* backupObserver */ null, |
| /* monitor */ null, |
| /* userInitiated */ false, |
| "BMS.beginFullBackup()"); |
| // Acquiring wakelock for PerformFullTransportBackupTask before its start. |
| mWakelock.acquire(); |
| (new Thread(mRunningFullBackupTask)).start(); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * The job scheduler says our constraints don't hold anymore, so tear down any ongoing backup |
| * task right away. |
| */ |
| public void endFullBackup() { |
| // offload the mRunningFullBackupTask.handleCancel() call to another thread, |
| // as we might have to wait for mCancelLock |
| Runnable endFullBackupRunnable = new Runnable() { |
| @Override |
| public void run() { |
| PerformFullTransportBackupTask pftbt = null; |
| synchronized (mQueueLock) { |
| if (mRunningFullBackupTask != null) { |
| pftbt = mRunningFullBackupTask; |
| } |
| } |
| if (pftbt != null) { |
| if (DEBUG_SCHEDULING) { |
| Slog.i(TAG, "Telling running backup to stop"); |
| } |
| pftbt.handleCancel(true); |
| } |
| } |
| }; |
| new Thread(endFullBackupRunnable, "end-full-backup").start(); |
| } |
| |
| /** Used by both incremental and full restore to restore widget data. */ |
| public void restoreWidgetData(String packageName, byte[] widgetData) { |
| // Apply the restored widget state and generate the ID update for the app |
| if (MORE_DEBUG) { |
| Slog.i(TAG, "Incorporating restored widget data"); |
| } |
| AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, mUserId); |
| } |
| |
| // ***************************** |
| // NEW UNIFIED RESTORE IMPLEMENTATION |
| // ***************************** |
| |
| /** Schedule a backup pass for {@code packageName}. */ |
| public void dataChangedImpl(String packageName) { |
| HashSet<String> targets = dataChangedTargets(packageName); |
| dataChangedImpl(packageName, targets); |
| } |
| |
| private void dataChangedImpl(String packageName, HashSet<String> targets) { |
| // Record that we need a backup pass for the caller. Since multiple callers |
| // may share a uid, we need to note all candidates within that uid and schedule |
| // a backup pass for each of them. |
| if (targets == null) { |
| Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" |
| + " uid=" + Binder.getCallingUid()); |
| return; |
| } |
| |
| synchronized (mQueueLock) { |
| // Note that this client has made data changes that need to be backed up |
| if (targets.contains(packageName)) { |
| // Add the caller to the set of pending backups. If there is |
| // one already there, then overwrite it, but no harm done. |
| BackupRequest req = new BackupRequest(packageName); |
| if (mPendingBackups.put(packageName, req) == null) { |
| if (MORE_DEBUG) Slog.d(TAG, "Now staging backup of " + packageName); |
| |
| // Journal this request in case of crash. The put() |
| // operation returned null when this package was not already |
| // in the set; we want to avoid touching the disk redundantly. |
| writeToJournalLocked(packageName); |
| } |
| } |
| } |
| |
| // ...and schedule a backup pass if necessary |
| KeyValueBackupJob.schedule(mUserId, mContext, mConstants); |
| } |
| |
| // Note: packageName is currently unused, but may be in the future |
| private HashSet<String> dataChangedTargets(String packageName) { |
| // If the caller does not hold the BACKUP permission, it can only request a |
| // backup of its own data. |
| if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), |
| Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { |
| synchronized (mBackupParticipants) { |
| return mBackupParticipants.get(Binder.getCallingUid()); |
| } |
| } |
| |
| // a caller with full permission can ask to back up any participating app |
| if (PACKAGE_MANAGER_SENTINEL.equals(packageName)) { |
| return Sets.newHashSet(PACKAGE_MANAGER_SENTINEL); |
| } else { |
| synchronized (mBackupParticipants) { |
| return SparseArrayUtils.union(mBackupParticipants); |
| } |
| } |
| } |
| |
| private void writeToJournalLocked(String str) { |
| try { |
| if (mJournal == null) mJournal = DataChangedJournal.newJournal(mJournalDir); |
| mJournal.addPackage(str); |
| } catch (IOException e) { |
| Slog.e(TAG, "Can't write " + str + " to backup journal", e); |
| mJournal = null; |
| } |
| } |
| |
| // ----- IBackupManager binder interface ----- |
| |
| /** Sent from an app's backup agent to let the service know that there's new data to backup. */ |
| public void dataChanged(final String packageName) { |
| final HashSet<String> targets = dataChangedTargets(packageName); |
| if (targets == null) { |
| Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" |
| + " uid=" + Binder.getCallingUid()); |
| return; |
| } |
| |
| mBackupHandler.post(new Runnable() { |
| public void run() { |
| dataChangedImpl(packageName, targets); |
| } |
| }); |
| } |
| |
| /** Run an initialize operation for the given transport. */ |
| public void initializeTransports(String[] transportNames, IBackupObserver observer) { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, |
| "initializeTransport"); |
| Slog.v(TAG, "initializeTransport(): " + Arrays.asList(transportNames)); |
| |
| final long oldId = Binder.clearCallingIdentity(); |
| try { |
| mWakelock.acquire(); |
| OnTaskFinishedListener listener = caller -> mWakelock.release(); |
| mBackupHandler.post( |
| new PerformInitializeTask(this, transportNames, observer, listener)); |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** |
| * Sets the work profile serial number of the ancestral work profile. |
| */ |
| public void setAncestralSerialNumber(long ancestralSerialNumber) { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, |
| "setAncestralSerialNumber"); |
| Slog.v(TAG, "Setting ancestral work profile id to " + ancestralSerialNumber); |
| // TODO (b/124359804) |
| try (RandomAccessFile af = getAncestralSerialNumberFile()) { |
| af.writeLong(ancestralSerialNumber); |
| } catch (IOException e) { |
| Slog.w(TAG, "Unable to write to work profile serial mapping file:", e); |
| } |
| } |
| |
| /** |
| * Returns the work profile serial number of the ancestral device. This will be set by |
| * {@link #setAncestralSerialNumber(long)}. Will return {@code -1} if not set. |
| */ |
| public long getAncestralSerialNumber() { |
| // TODO (b/124359804) |
| try (RandomAccessFile af = getAncestralSerialNumberFile()) { |
| return af.readLong(); |
| } catch (IOException e) { |
| Slog.w(TAG, "Unable to write to work profile serial number file:", e); |
| return -1; |
| } |
| } |
| |
| private RandomAccessFile getAncestralSerialNumberFile() throws FileNotFoundException { |
| if (mAncestralSerialNumberFile == null) { |
| mAncestralSerialNumberFile = new File( |
| UserBackupManagerFiles.getBaseStateDir(getUserId()), |
| SERIAL_ID_FILE); |
| FileUtils.createNewFile(mAncestralSerialNumberFile); |
| } |
| return new RandomAccessFile(mAncestralSerialNumberFile, "rwd"); |
| } |
| |
| @VisibleForTesting |
| void setAncestralSerialNumberFile(File ancestralSerialNumberFile) { |
| mAncestralSerialNumberFile = ancestralSerialNumberFile; |
| } |
| |
| |
| /** Clear the given package's backup data from the current transport. */ |
| public void clearBackupData(String transportName, String packageName) { |
| if (DEBUG) Slog.v(TAG, "clearBackupData() of " + packageName + " on " + transportName); |
| PackageInfo info; |
| try { |
| info = mPackageManager.getPackageInfoAsUser(packageName, |
| PackageManager.GET_SIGNING_CERTIFICATES, mUserId); |
| } catch (NameNotFoundException e) { |
| Slog.d(TAG, "No such package '" + packageName + "' - not clearing backup data"); |
| return; |
| } |
| |
| // If the caller does not hold the BACKUP permission, it can only request a |
| // wipe of its own backed-up data. |
| Set<String> apps; |
| if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), |
| Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { |
| apps = mBackupParticipants.get(Binder.getCallingUid()); |
| } else { |
| // a caller with full permission can ask to back up any participating app |
| // !!! TODO: allow data-clear of ANY app? |
| if (MORE_DEBUG) Slog.v(TAG, "Privileged caller, allowing clear of other apps"); |
| apps = mProcessedPackagesJournal.getPackagesCopy(); |
| } |
| |
| if (apps.contains(packageName)) { |
| // found it; fire off the clear request |
| if (MORE_DEBUG) Slog.v(TAG, "Found the app - running clear process"); |
| mBackupHandler.removeMessages(MSG_RETRY_CLEAR); |
| synchronized (mQueueLock) { |
| TransportClient transportClient = |
| mTransportManager |
| .getTransportClient(transportName, "BMS.clearBackupData()"); |
| if (transportClient == null) { |
| // transport is currently unregistered -- make sure to retry |
| Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR, |
| new ClearRetryParams(transportName, packageName)); |
| mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL); |
| return; |
| } |
| long oldId = Binder.clearCallingIdentity(); |
| OnTaskFinishedListener listener = |
| caller -> |
| mTransportManager.disposeOfTransportClient(transportClient, caller); |
| mWakelock.acquire(); |
| Message msg = mBackupHandler.obtainMessage( |
| MSG_RUN_CLEAR, |
| new ClearParams(transportClient, info, listener)); |
| mBackupHandler.sendMessage(msg); |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| } |
| |
| /** |
| * Run a backup pass immediately for any applications that have declared that they have pending |
| * updates. |
| */ |
| public void backupNow() { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "backupNow"); |
| |
| long oldId = Binder.clearCallingIdentity(); |
| try { |
| final PowerSaveState result = |
| mPowerManager.getPowerSaveState(ServiceType.KEYVALUE_BACKUP); |
| if (result.batterySaverEnabled) { |
| if (DEBUG) Slog.v(TAG, "Not running backup while in battery save mode"); |
| // Try again in several hours. |
| KeyValueBackupJob.schedule(mUserId, mContext, mConstants); |
| } else { |
| if (DEBUG) Slog.v(TAG, "Scheduling immediate backup pass"); |
| synchronized (mQueueLock) { |
| // Fire the intent that kicks off the whole shebang... |
| try { |
| mRunBackupIntent.send(); |
| } catch (PendingIntent.CanceledException e) { |
| // should never happen |
| Slog.e(TAG, "run-backup intent cancelled!"); |
| } |
| |
| // ...and cancel any pending scheduled job, because we've just superseded it |
| KeyValueBackupJob.cancel(mUserId, mContext); |
| } |
| } |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** |
| * Used by 'adb backup' to run a backup pass for packages supplied via the command line, writing |
| * the resulting data stream to the supplied {@code fd}. This method is synchronous and does not |
| * return to the caller until the backup has been completed. It requires on-screen confirmation |
| * by the user. |
| */ |
| public void adbBackup(ParcelFileDescriptor fd, boolean includeApks, |
| boolean includeObbs, boolean includeShared, boolean doWidgets, boolean doAllApps, |
| boolean includeSystem, boolean compress, boolean doKeyValue, String[] pkgList) { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "adbBackup"); |
| |
| final int callingUserHandle = UserHandle.getCallingUserId(); |
| // TODO: http://b/22388012 |
| if (callingUserHandle != UserHandle.USER_SYSTEM) { |
| throw new IllegalStateException("Backup supported only for the device owner"); |
| } |
| |
| // Validate |
| if (!doAllApps) { |
| if (!includeShared) { |
| // If we're backing up shared data (sdcard or equivalent), then we can run |
| // without any supplied app names. Otherwise, we'd be doing no work, so |
| // report the error. |
| if (pkgList == null || pkgList.length == 0) { |
| throw new IllegalArgumentException( |
| "Backup requested but neither shared nor any apps named"); |
| } |
| } |
| } |
| |
| long oldId = Binder.clearCallingIdentity(); |
| try { |
| if (!mSetupComplete) { |
| Slog.i(TAG, "Backup not supported before setup"); |
| return; |
| } |
| |
| if (DEBUG) { |
| Slog.v(TAG, "Requesting backup: apks=" + includeApks + " obb=" + includeObbs |
| + " shared=" + includeShared + " all=" + doAllApps + " system=" |
| + includeSystem + " includekeyvalue=" + doKeyValue + " pkgs=" + pkgList); |
| } |
| Slog.i(TAG, "Beginning adb backup..."); |
| |
| AdbBackupParams params = new AdbBackupParams(fd, includeApks, includeObbs, |
| includeShared, doWidgets, doAllApps, includeSystem, compress, doKeyValue, |
| pkgList); |
| final int token = generateRandomIntegerToken(); |
| synchronized (mAdbBackupRestoreConfirmations) { |
| mAdbBackupRestoreConfirmations.put(token, params); |
| } |
| |
| // start up the confirmation UI |
| if (DEBUG) Slog.d(TAG, "Starting backup confirmation UI, token=" + token); |
| if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) { |
| Slog.e(TAG, "Unable to launch backup confirmation UI"); |
| mAdbBackupRestoreConfirmations.delete(token); |
| return; |
| } |
| |
| // make sure the screen is lit for the user interaction |
| mPowerManager.userActivity(SystemClock.uptimeMillis(), |
| PowerManager.USER_ACTIVITY_EVENT_OTHER, |
| 0); |
| |
| // start the confirmation countdown |
| startConfirmationTimeout(token, params); |
| |
| // wait for the backup to be performed |
| if (DEBUG) Slog.d(TAG, "Waiting for backup completion..."); |
| waitForCompletion(params); |
| } finally { |
| try { |
| fd.close(); |
| } catch (IOException e) { |
| Slog.e(TAG, "IO error closing output for adb backup: " + e.getMessage()); |
| } |
| Binder.restoreCallingIdentity(oldId); |
| Slog.d(TAG, "Adb backup processing complete."); |
| } |
| } |
| |
| /** Run a full backup pass for the given packages. Used by 'adb shell bmgr'. */ |
| public void fullTransportBackup(String[] pkgNames) { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, |
| "fullTransportBackup"); |
| |
| final int callingUserHandle = UserHandle.getCallingUserId(); |
| // TODO: http://b/22388012 |
| if (callingUserHandle != UserHandle.USER_SYSTEM) { |
| throw new IllegalStateException("Restore supported only for the device owner"); |
| } |
| |
| String transportName = mTransportManager.getCurrentTransportName(); |
| if (!fullBackupAllowable(transportName)) { |
| Slog.i(TAG, "Full backup not currently possible -- key/value backup not yet run?"); |
| } else { |
| if (DEBUG) { |
| Slog.d(TAG, "fullTransportBackup()"); |
| } |
| |
| final long oldId = Binder.clearCallingIdentity(); |
| try { |
| CountDownLatch latch = new CountDownLatch(1); |
| Runnable task = PerformFullTransportBackupTask.newWithCurrentTransport( |
| this, |
| /* observer */ null, |
| pkgNames, |
| /* updateSchedule */ false, |
| /* runningJob */ null, |
| latch, |
| /* backupObserver */ null, |
| /* monitor */ null, |
| /* userInitiated */ false, |
| "BMS.fullTransportBackup()"); |
| // Acquiring wakelock for PerformFullTransportBackupTask before its start. |
| mWakelock.acquire(); |
| (new Thread(task, "full-transport-master")).start(); |
| do { |
| try { |
| latch.await(); |
| break; |
| } catch (InterruptedException e) { |
| // Just go back to waiting for the latch to indicate completion |
| } |
| } while (true); |
| |
| // We just ran a backup on these packages, so kick them to the end of the queue |
| final long now = System.currentTimeMillis(); |
| for (String pkg : pkgNames) { |
| enqueueFullBackup(pkg, now); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| if (DEBUG) { |
| Slog.d(TAG, "Done with full transport backup."); |
| } |
| } |
| |
| /** |
| * Used by 'adb restore' to run a restore pass, blocking until completion. Requires user |
| * confirmation. |
| */ |
| public void adbRestore(ParcelFileDescriptor fd) { |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "adbRestore"); |
| |
| final int callingUserHandle = UserHandle.getCallingUserId(); |
| // TODO: http://b/22388012 |
| if (callingUserHandle != UserHandle.USER_SYSTEM) { |
| throw new IllegalStateException("Restore supported only for the device owner"); |
| } |
| |
| long oldId = Binder.clearCallingIdentity(); |
| |
| try { |
| if (!mSetupComplete) { |
| Slog.i(TAG, "Full restore not permitted before setup"); |
| return; |
| } |
| |
| Slog.i(TAG, "Beginning restore..."); |
| |
| AdbRestoreParams params = new AdbRestoreParams(fd); |
| final int token = generateRandomIntegerToken(); |
| synchronized (mAdbBackupRestoreConfirmations) { |
| mAdbBackupRestoreConfirmations.put(token, params); |
| } |
| |
| // start up the confirmation UI |
| if (DEBUG) Slog.d(TAG, "Starting restore confirmation UI, token=" + token); |
| if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) { |
| Slog.e(TAG, "Unable to launch restore confirmation"); |
| mAdbBackupRestoreConfirmations.delete(token); |
| return; |
| } |
| |
| // make sure the screen is lit for the user interaction |
| mPowerManager.userActivity(SystemClock.uptimeMillis(), |
| PowerManager.USER_ACTIVITY_EVENT_OTHER, |
| 0); |
| |
| // start the confirmation countdown |
| startConfirmationTimeout(token, params); |
| |
| // wait for the restore to be performed |
| if (DEBUG) Slog.d(TAG, "Waiting for restore completion..."); |
| waitForCompletion(params); |
| } finally { |
| try { |
| fd.close(); |
| } catch (IOException e) { |
| Slog.w(TAG, "Error trying to close fd after adb restore: " + e); |
| } |
| Binder.restoreCallingIdentity(oldId); |
| Slog.i(TAG, "adb restore processing complete."); |
| } |
| } |
| |
| private boolean startConfirmationUi(int token, String action) { |
| try { |
| Intent confIntent = new Intent(action); |
| confIntent.setClassName("com.android.backupconfirm", |
| "com.android.backupconfirm.BackupRestoreConfirmation"); |
| confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token); |
| confIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); |
| mContext.startActivityAsUser(confIntent, UserHandle.SYSTEM); |
| } catch (ActivityNotFoundException e) { |
| return false; |
| } |
| return true; |
| } |
| |
| private void startConfirmationTimeout(int token, AdbParams params) { |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "Posting conf timeout msg after " |
| + TIMEOUT_FULL_CONFIRMATION + " millis"); |
| } |
| Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT, |
| token, 0, params); |
| mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION); |
| } |
| |
| private void waitForCompletion(AdbParams params) { |
| synchronized (params.latch) { |
| while (!params.latch.get()) { |
| try { |
| params.latch.wait(); |
| } catch (InterruptedException e) { /* never interrupted */ } |
| } |
| } |
| } |
| |
| /** Called when adb backup/restore has completed. */ |
| public void signalAdbBackupRestoreCompletion(AdbParams params) { |
| synchronized (params.latch) { |
| params.latch.set(true); |
| params.latch.notifyAll(); |
| } |
| } |
| |
| /** |
| * Confirm that the previously-requested full backup/restore operation can proceed. This is used |
| * to require a user-facing disclosure about the operation. |
| */ |
| public void acknowledgeAdbBackupOrRestore(int token, boolean allow, |
| String curPassword, String encPpassword, IFullBackupRestoreObserver observer) { |
| if (DEBUG) { |
| Slog.d(TAG, "acknowledgeAdbBackupOrRestore : token=" + token |
| + " allow=" + allow); |
| } |
| |
| // TODO: possibly require not just this signature-only permission, but even |
| // require that the specific designated confirmation-UI app uid is the caller? |
| mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, |
| "acknowledgeAdbBackupOrRestore"); |
| |
| long oldId = Binder.clearCallingIdentity(); |
| try { |
| |
| AdbParams params; |
| synchronized (mAdbBackupRestoreConfirmations) { |
| params = mAdbBackupRestoreConfirmations.get(token); |
| if (params != null) { |
| mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params); |
| mAdbBackupRestoreConfirmations.delete(token); |
| |
| if (allow) { |
| final int verb = params instanceof AdbBackupParams |
| ? MSG_RUN_ADB_BACKUP |
| : MSG_RUN_ADB_RESTORE; |
| |
| params.observer = observer; |
| params.curPassword = curPassword; |
| |
| params.encryptPassword = encPpassword; |
| |
| if (MORE_DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb); |
| mWakelock.acquire(); |
| Message msg = mBackupHandler.obtainMessage(verb, params); |
| mBackupHandler.sendMessage(msg); |
| } else { |
| Slog.w(TAG, "User rejected full backup/restore operation"); |
| // indicate completion without having actually transferred any data |
| signalAdbBackupRestoreCompletion(params); |
| } |
| } else { |
| Slog.w(TAG, "Attempted to ack full backup/restore with invalid token"); |
| } |
| } |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** User-configurable enabling/disabling of backups. */ |
| public void setBackupEnabled(boolean enable) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "setBackupEnabled"); |
| |
| Slog.i(TAG, "Backup enabled => " + enable); |
| |
| long oldId = Binder.clearCallingIdentity(); |
| try { |
| boolean wasEnabled = mEnabled; |
| synchronized (this) { |
| UserBackupManagerFilePersistedSettings.writeBackupEnableState(mUserId, enable); |
| mEnabled = enable; |
| } |
| |
| synchronized (mQueueLock) { |
| if (enable && !wasEnabled && mSetupComplete) { |
| // if we've just been enabled, start scheduling backup passes |
| KeyValueBackupJob.schedule(mUserId, mContext, mConstants); |
| scheduleNextFullBackupJob(0); |
| } else if (!enable) { |
| // No longer enabled, so stop running backups |
| if (MORE_DEBUG) Slog.i(TAG, "Opting out of backup"); |
| |
| KeyValueBackupJob.cancel(mUserId, mContext); |
| |
| // This also constitutes an opt-out, so we wipe any data for |
| // this device from the backend. We start that process with |
| // an alarm in order to guarantee wakelock states. |
| if (wasEnabled && mSetupComplete) { |
| // NOTE: we currently flush every registered transport, not just |
| // the currently-active one. |
| List<String> transportNames = new ArrayList<>(); |
| List<String> transportDirNames = new ArrayList<>(); |
| mTransportManager.forEachRegisteredTransport( |
| name -> { |
| final String dirName; |
| try { |
| dirName = |
| mTransportManager |
| .getTransportDirName(name); |
| } catch (TransportNotRegisteredException e) { |
| // Should never happen |
| Slog.e(TAG, "Unexpected unregistered transport", e); |
| return; |
| } |
| transportNames.add(name); |
| transportDirNames.add(dirName); |
| }); |
| |
| // build the set of transports for which we are posting an init |
| for (int i = 0; i < transportNames.size(); i++) { |
| recordInitPending( |
| true, |
| transportNames.get(i), |
| transportDirNames.get(i)); |
| } |
| mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), |
| mRunInitIntent); |
| } |
| } |
| } |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** Enable/disable automatic restore of app data at install time. */ |
| public void setAutoRestore(boolean doAutoRestore) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "setAutoRestore"); |
| |
| Slog.i(TAG, "Auto restore => " + doAutoRestore); |
| |
| final long oldId = Binder.clearCallingIdentity(); |
| try { |
| synchronized (this) { |
| Settings.Secure.putIntForUser(mContext.getContentResolver(), |
| Settings.Secure.BACKUP_AUTO_RESTORE, doAutoRestore ? 1 : 0, mUserId); |
| mAutoRestore = doAutoRestore; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** Report whether the backup mechanism is currently enabled. */ |
| public boolean isBackupEnabled() { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "isBackupEnabled"); |
| return mEnabled; // no need to synchronize just to read it |
| } |
| |
| /** Report the name of the currently active transport. */ |
| public String getCurrentTransport() { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "getCurrentTransport"); |
| String currentTransport = mTransportManager.getCurrentTransportName(); |
| if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + currentTransport); |
| return currentTransport; |
| } |
| |
| /** |
| * Returns the {@link ComponentName} of the host service of the selected transport or {@code |
| * null} if no transport selected or if the transport selected is not registered. |
| */ |
| @Nullable |
| public ComponentName getCurrentTransportComponent() { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "getCurrentTransportComponent"); |
| long oldId = Binder.clearCallingIdentity(); |
| try { |
| return mTransportManager.getCurrentTransportComponent(); |
| } catch (TransportNotRegisteredException e) { |
| return null; |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** Report all known, available backup transports by name. */ |
| public String[] listAllTransports() { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "listAllTransports"); |
| |
| return mTransportManager.getRegisteredTransportNames(); |
| } |
| |
| /** Report all known, available backup transports by component. */ |
| public ComponentName[] listAllTransportComponents() { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "listAllTransportComponents"); |
| return mTransportManager.getRegisteredTransportComponents(); |
| } |
| |
| /** |
| * Update the attributes of the transport identified by {@code transportComponent}. If the |
| * specified transport has not been bound at least once (for registration), this call will be |
| * ignored. Only the host process of the transport can change its description, otherwise a |
| * {@link SecurityException} will be thrown. |
| * |
| * @param transportComponent The identity of the transport being described. |
| * @param name A {@link String} with the new name for the transport. This is NOT for |
| * identification. MUST NOT be {@code null}. |
| * @param configurationIntent An {@link Intent} that can be passed to |
| * {@link Context#startActivity} in order to launch the transport's configuration UI. It may |
| * be {@code null} if the transport does not offer any user-facing configuration UI. |
| * @param currentDestinationString A {@link String} describing the destination to which the |
| * transport is currently sending data. MUST NOT be {@code null}. |
| * @param dataManagementIntent An {@link Intent} that can be passed to |
| * {@link Context#startActivity} in order to launch the transport's data-management UI. It |
| * may be {@code null} if the transport does not offer any user-facing data |
| * management UI. |
| * @param dataManagementLabel A {@link String} to be used as the label for the transport's data |
| * management affordance. This MUST be {@code null} when dataManagementIntent is |
| * {@code null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}. |
| * @throws SecurityException If the UID of the calling process differs from the package UID of |
| * {@code transportComponent} or if the caller does NOT have BACKUP permission. |
| */ |
| public void updateTransportAttributes( |
| ComponentName transportComponent, |
| String name, |
| @Nullable Intent configurationIntent, |
| String currentDestinationString, |
| @Nullable Intent dataManagementIntent, |
| @Nullable String dataManagementLabel) { |
| updateTransportAttributes( |
| Binder.getCallingUid(), |
| transportComponent, |
| name, |
| configurationIntent, |
| currentDestinationString, |
| dataManagementIntent, |
| dataManagementLabel); |
| } |
| |
| @VisibleForTesting |
| void updateTransportAttributes( |
| int callingUid, |
| ComponentName transportComponent, |
| String name, |
| @Nullable Intent configurationIntent, |
| String currentDestinationString, |
| @Nullable Intent dataManagementIntent, |
| @Nullable String dataManagementLabel) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "updateTransportAttributes"); |
| |
| Preconditions.checkNotNull(transportComponent, "transportComponent can't be null"); |
| Preconditions.checkNotNull(name, "name can't be null"); |
| Preconditions.checkNotNull( |
| currentDestinationString, "currentDestinationString can't be null"); |
| Preconditions.checkArgument( |
| (dataManagementIntent == null) == (dataManagementLabel == null), |
| "dataManagementLabel should be null iff dataManagementIntent is null"); |
| |
| try { |
| int transportUid = |
| mContext.getPackageManager() |
| .getPackageUidAsUser(transportComponent.getPackageName(), 0, mUserId); |
| if (callingUid != transportUid) { |
| throw new SecurityException("Only the transport can change its description"); |
| } |
| } catch (NameNotFoundException e) { |
| throw new SecurityException("Transport package not found", e); |
| } |
| |
| final long oldId = Binder.clearCallingIdentity(); |
| try { |
| mTransportManager.updateTransportAttributes( |
| transportComponent, |
| name, |
| configurationIntent, |
| currentDestinationString, |
| dataManagementIntent, |
| dataManagementLabel); |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** |
| * Selects transport {@code transportName} and returns previously selected transport. |
| * |
| * @deprecated Use {@link #selectBackupTransportAsync(ComponentName, |
| * ISelectBackupTransportCallback)} instead. |
| */ |
| @Deprecated |
| @Nullable |
| public String selectBackupTransport(String transportName) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "selectBackupTransport"); |
| |
| final long oldId = Binder.clearCallingIdentity(); |
| try { |
| String previousTransportName = mTransportManager.selectTransport(transportName); |
| updateStateForTransport(transportName); |
| Slog.v(TAG, "selectBackupTransport(transport = " + transportName |
| + "): previous transport = " + previousTransportName); |
| return previousTransportName; |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| /** |
| * Selects transport {@code transportComponent} asynchronously and notifies {@code listener} |
| * with the result upon completion. |
| */ |
| public void selectBackupTransportAsync( |
| ComponentName transportComponent, ISelectBackupTransportCallback listener) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "selectBackupTransportAsync"); |
| |
| final long oldId = Binder.clearCallingIdentity(); |
| try { |
| String transportString = transportComponent.flattenToShortString(); |
| Slog.v(TAG, "selectBackupTransportAsync(transport = " + transportString + ")"); |
| mBackupHandler.post( |
| () -> { |
| String transportName = null; |
| int result = |
| mTransportManager.registerAndSelectTransport(transportComponent); |
| if (result == BackupManager.SUCCESS) { |
| try { |
| transportName = |
| mTransportManager.getTransportName(transportComponent); |
| updateStateForTransport(transportName); |
| } catch (TransportNotRegisteredException e) { |
| Slog.e(TAG, "Transport got unregistered"); |
| result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE; |
| } |
| } |
| |
| try { |
| if (transportName != null) { |
| listener.onSuccess(transportName); |
| } else { |
| listener.onFailure(result); |
| } |
| } catch (RemoteException e) { |
| Slog.e(TAG, "ISelectBackupTransportCallback listener not available"); |
| } |
| }); |
| } finally { |
| Binder.restoreCallingIdentity(oldId); |
| } |
| } |
| |
| private void updateStateForTransport(String newTransportName) { |
| // Publish the name change |
| Settings.Secure.putStringForUser(mContext.getContentResolver(), |
| Settings.Secure.BACKUP_TRANSPORT, newTransportName, mUserId); |
| |
| // And update our current-dataset bookkeeping |
| String callerLogString = "BMS.updateStateForTransport()"; |
| TransportClient transportClient = |
| mTransportManager.getTransportClient(newTransportName, callerLogString); |
| if (transportClient != null) { |
| try { |
| IBackupTransport transport = transportClient.connectOrThrow(callerLogString); |
| mCurrentToken = transport.getCurrentRestoreSet(); |
| } catch (Exception e) { |
| // Oops. We can't know the current dataset token, so reset and figure it out |
| // when we do the next k/v backup operation on this transport. |
| mCurrentToken = 0; |
| Slog.w(TAG, "Transport " + newTransportName + " not available: current token = 0"); |
| } |
| mTransportManager.disposeOfTransportClient(transportClient, callerLogString); |
| } else { |
| Slog.w(TAG, "Transport " + newTransportName + " not registered: current token = 0"); |
| // The named transport isn't registered, so we can't know what its current dataset token |
| // is. Reset as above. |
| mCurrentToken = 0; |
| } |
| } |
| |
| /** |
| * Supply the configuration intent for the given transport. If the name is not one of the |
| * available transports, or if the transport does not supply any configuration UI, the method |
| * returns {@code null}. |
| */ |
| public Intent getConfigurationIntent(String transportName) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "getConfigurationIntent"); |
| try { |
| Intent intent = mTransportManager.getTransportConfigurationIntent(transportName); |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "getConfigurationIntent() returning intent " + intent); |
| } |
| return intent; |
| } catch (TransportNotRegisteredException e) { |
| Slog.e(TAG, "Unable to get configuration intent from transport: " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| /** |
| * Supply the current destination string for the given transport. If the name is not one of the |
| * registered transports the method will return null. |
| * |
| * <p>This string is used VERBATIM as the summary text of the relevant Settings item. |
| * |
| * @param transportName The name of the registered transport. |
| * @return The current destination string or null if the transport is not registered. |
| */ |
| public String getDestinationString(String transportName) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "getDestinationString"); |
| |
| try { |
| String string = mTransportManager.getTransportCurrentDestinationString(transportName); |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "getDestinationString() returning " + string); |
| } |
| return string; |
| } catch (TransportNotRegisteredException e) { |
| Slog.e(TAG, "Unable to get destination string from transport: " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| /** Supply the manage-data intent for the given transport. */ |
| public Intent getDataManagementIntent(String transportName) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "getDataManagementIntent"); |
| |
| try { |
| Intent intent = mTransportManager.getTransportDataManagementIntent(transportName); |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "getDataManagementIntent() returning intent " + intent); |
| } |
| return intent; |
| } catch (TransportNotRegisteredException e) { |
| Slog.e(TAG, "Unable to get management intent from transport: " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| /** |
| * Supply the menu label for affordances that fire the manage-data intent for the given |
| * transport. |
| */ |
| public String getDataManagementLabel(String transportName) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "getDataManagementLabel"); |
| |
| try { |
| String label = mTransportManager.getTransportDataManagementLabel(transportName); |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "getDataManagementLabel() returning " + label); |
| } |
| return label; |
| } catch (TransportNotRegisteredException e) { |
| Slog.e(TAG, "Unable to get management label from transport: " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| /** |
| * Callback: a requested backup agent has been instantiated. This should only be called from the |
| * {@link ActivityManager}. |
| */ |
| public void agentConnected(String packageName, IBinder agentBinder) { |
| synchronized (mAgentConnectLock) { |
| if (Binder.getCallingUid() == Process.SYSTEM_UID) { |
| Slog.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder); |
| mConnectedAgent = IBackupAgent.Stub.asInterface(agentBinder); |
| mConnecting = false; |
| } else { |
| Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() |
| + " claiming agent connected"); |
| } |
| mAgentConnectLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Callback: a backup agent has failed to come up, or has unexpectedly quit. If the agent failed |
| * to come up in the first place, the agentBinder argument will be {@code null}. This should |
| * only be called from the {@link ActivityManager}. |
| */ |
| public void agentDisconnected(String packageName) { |
| // TODO: handle backup being interrupted |
| synchronized (mAgentConnectLock) { |
| if (Binder.getCallingUid() == Process.SYSTEM_UID) { |
| mConnectedAgent = null; |
| mConnecting = false; |
| } else { |
| Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() |
| + " claiming agent disconnected"); |
| } |
| mAgentConnectLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * An application being installed will need a restore pass, then the {@link PackageManager} will |
| * need to be told when the restore is finished. |
| */ |
| public void restoreAtInstall(String packageName, int token) { |
| if (Binder.getCallingUid() != Process.SYSTEM_UID) { |
| Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() |
| + " attemping install-time restore"); |
| return; |
| } |
| |
| boolean skip = false; |
| |
| long restoreSet = getAvailableRestoreToken(packageName); |
| if (DEBUG) { |
| Slog.v(TAG, "restoreAtInstall pkg=" + packageName |
| + " token=" + Integer.toHexString(token) |
| + " restoreSet=" + Long.toHexString(restoreSet)); |
| } |
| if (restoreSet == 0) { |
| if (MORE_DEBUG) Slog.i(TAG, "No restore set"); |
| skip = true; |
| } |
| |
| TransportClient transportClient = |
| mTransportManager.getCurrentTransportClient("BMS.restoreAtInstall()"); |
| if (transportClient == null) { |
| if (DEBUG) Slog.w(TAG, "No transport client"); |
| skip = true; |
| } |
| |
| if (!mAutoRestore) { |
| if (DEBUG) { |
| Slog.w(TAG, "Non-restorable state: auto=" + mAutoRestore); |
| } |
| skip = true; |
| } |
| |
| if (!skip) { |
| try { |
| // okay, we're going to attempt a restore of this package from this restore set. |
| // The eventual message back into the Package Manager to run the post-install |
| // steps for 'token' will be issued from the restore handling code. |
| |
| mWakelock.acquire(); |
| |
| OnTaskFinishedListener listener = caller -> { |
| mTransportManager.disposeOfTransportClient(transportClient, caller); |
| mWakelock.release(); |
| }; |
| |
| if (MORE_DEBUG) { |
| Slog.d(TAG, "Restore at install of " + packageName); |
| } |
| Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); |
| msg.obj = |
| RestoreParams.createForRestoreAtInstall( |
| transportClient, |
| /* observer */ null, |
| /* monitor */ null, |
| restoreSet, |
| packageName, |
| token, |
| listener); |
| mBackupHandler.sendMessage(msg); |
| } catch (Exception e) { |
| // Calling into the transport broke; back off and proceed with the installation. |
| Slog.e(TAG, "Unable to contact transport: " + e.getMessage()); |
| skip = true; |
| } |
| } |
| |
| if (skip) { |
| // Auto-restore disabled or no way to attempt a restore |
| |
| if (transportClient != null) { |
| mTransportManager.disposeOfTransportClient( |
| transportClient, "BMS.restoreAtInstall()"); |
| } |
| |
| // Tell the PackageManager to proceed with the post-install handling for this package. |
| if (DEBUG) Slog.v(TAG, "Finishing install immediately"); |
| try { |
| mPackageManagerBinder.finishPackageInstall(token, false); |
| } catch (RemoteException e) { /* can't happen */ } |
| } |
| } |
| |
| /** Hand off a restore session. */ |
| public IRestoreSession beginRestoreSession(String packageName, String transport) { |
| if (DEBUG) { |
| Slog.v(TAG, "beginRestoreSession: pkg=" + packageName |
| + " transport=" + transport); |
| } |
| |
| boolean needPermission = true; |
| if (transport == null) { |
| transport = mTransportManager.getCurrentTransportName(); |
| |
| if (packageName != null) { |
| PackageInfo app = null; |
| try { |
| app = mPackageManager.getPackageInfoAsUser(packageName, 0, mUserId); |
| } catch (NameNotFoundException nnf) { |
| Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); |
| throw new IllegalArgumentException("Package " + packageName + " not found"); |
| } |
| |
| if (app.applicationInfo.uid == Binder.getCallingUid()) { |
| // So: using the current active transport, and the caller has asked |
| // that its own package will be restored. In this narrow use case |
| // we do not require the caller to hold the permission. |
| needPermission = false; |
| } |
| } |
| } |
| |
| if (needPermission) { |
| mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, |
| "beginRestoreSession"); |
| } else { |
| if (DEBUG) Slog.d(TAG, "restoring self on current transport; no permission needed"); |
| } |
| |
| synchronized (this) { |
| if (mActiveRestoreSession != null) { |
| Slog.i(TAG, "Restore session requested but one already active"); |
| return null; |
| } |
| if (mBackupRunning) { |
| Slog.i(TAG, "Restore session requested but currently running backups"); |
| return null; |
| } |
| mActiveRestoreSession = new ActiveRestoreSession(this, packageName, transport); |
| mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT, |
| mAgentTimeoutParameters.getRestoreAgentTimeoutMillis()); |
| } |
| return mActiveRestoreSession; |
| } |
| |
| /** Clear the specified restore session. */ |
| public void clearRestoreSession(ActiveRestoreSession currentSession) { |
| synchronized (this) { |
| if (currentSession != mActiveRestoreSession) { |
| Slog.e(TAG, "ending non-current restore session"); |
| } else { |
| if (DEBUG) Slog.v(TAG, "Clearing restore session and halting timeout"); |
| mActiveRestoreSession = null; |
| mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT); |
| } |
| } |
| } |
| |
| /** |
| * Note that a currently-active backup agent has notified us that it has completed the given |
| * outstanding asynchronous backup/restore operation. |
| */ |
| public void opComplete(int token, long result) { |
| if (MORE_DEBUG) { |
| Slog.v(TAG, "opComplete: " + Integer.toHexString(token) + " result=" + result); |
| } |
| Operation op = null; |
| synchronized (mCurrentOpLock) { |
| op = mCurrentOperations.get(token); |
| if (op != null) { |
| if (op.state == OP_TIMEOUT) { |
| // The operation already timed out, and this is a late response. Tidy up |
| // and ignore it; we've already dealt with the timeout. |
| op = null; |
| mCurrentOperations.delete(token); |
| } else if (op.state == OP_ACKNOWLEDGED) { |
| if (DEBUG) { |
| Slog.w(TAG, "Received duplicate ack for token=" |
| + Integer.toHexString(token)); |
| } |
| op = null; |
| mCurrentOperations.remove(token); |
| } else if (op.state == OP_PENDING) { |
| // Can't delete op from mCurrentOperations. waitUntilOperationComplete can be |
| // called after we we receive this call. |
| op.state = OP_ACKNOWLEDGED; |
| } |
| } |
| mCurrentOpLock.notifyAll(); |
| } |
| |
| // The completion callback, if any, is invoked on the handler |
| if (op != null && op.callback != null) { |
| Pair<BackupRestoreTask, Long> callbackAndResult = Pair.create(op.callback, result); |
| Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, callbackAndResult); |
| mBackupHandler.sendMessage(msg); |
| } |
| } |
| |
| /** Checks if the package is eligible for backup. */ |
| public boolean isAppEligibleForBackup(String packageName) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "isAppEligibleForBackup"); |
| |
| long oldToken = Binder.clearCallingIdentity(); |
| try { |
| String callerLogString = "BMS.isAppEligibleForBackup"; |
| TransportClient transportClient = |
| mTransportManager.getCurrentTransportClient(callerLogString); |
| boolean eligible = |
| AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport( |
| transportClient, packageName, mPackageManager, mUserId); |
| if (transportClient != null) { |
| mTransportManager.disposeOfTransportClient(transportClient, callerLogString); |
| } |
| return eligible; |
| } finally { |
| Binder.restoreCallingIdentity(oldToken); |
| } |
| } |
| |
| /** Returns the inputted packages that are eligible for backup. */ |
| public String[] filterAppsEligibleForBackup(String[] packages) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.BACKUP, "filterAppsEligibleForBackup"); |
| |
| long oldToken = Binder.clearCallingIdentity(); |
| try { |
| String callerLogString = "BMS.filterAppsEligibleForBackup"; |
| TransportClient transportClient = |
| mTransportManager.getCurrentTransportClient(callerLogString); |
| List<String> eligibleApps = new LinkedList<>(); |
| for (String packageName : packages) { |
| if (AppBackupUtils |
| .appIsRunningAndEligibleForBackupWithTransport( |
| transportClient, packageName, mPackageManager, mUserId)) { |
| eligibleApps.add(packageName); |
| } |
| } |
| if (transportClient != null) { |
| mTransportManager.disposeOfTransportClient(transportClient, callerLogString); |
| } |
| return eligibleApps.toArray(new String[eligibleApps.size()]); |
| } finally { |
| Binder.restoreCallingIdentity(oldToken); |
| } |
| } |
| |
| /** Prints service state for 'dumpsys backup'. */ |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return; |
| |
| long identityToken = Binder.clearCallingIdentity(); |
| try { |
| if (args != null) { |
| for (String arg : args) { |
| if ("-h".equals(arg)) { |
| pw.println("'dumpsys backup' optional arguments:"); |
| pw.println(" -h : this help text"); |
| pw.println(" a[gents] : dump information about defined backup agents"); |
| return; |
| } else if ("agents".startsWith(arg)) { |
| dumpAgents(pw); |
| return; |
| } else if ("transportclients".equals(arg.toLowerCase())) { |
| mTransportManager.dumpTransportClients(pw); |
| return; |
| } else if ("transportstats".equals(arg.toLowerCase())) { |
| mTransportManager.dumpTransportStats(pw); |
| return; |
| } |
| } |
| } |
| dumpInternal(pw); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| |
| private void dumpAgents(PrintWriter pw) { |
| List<PackageInfo> agentPackages = allAgentPackages(); |
| pw.println("Defined backup agents:"); |
| for (PackageInfo pkg : agentPackages) { |
| pw.print(" "); |
| pw.print(pkg.packageName); |
| pw.println(':'); |
| pw.print(" "); |
| pw.println(pkg.applicationInfo.backupAgentName); |
| } |
| } |
| |
| private void dumpInternal(PrintWriter pw) { |
| synchronized (mQueueLock) { |
| pw.println("Backup Manager is " + (mEnabled ? "enabled" : "disabled") |
| + " / " + (!mSetupComplete ? "not " : "") + "setup complete / " |
| + (this.mPendingInits.size() == 0 ? "not " : "") + "pending init"); |
| pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled")); |
| if (mBackupRunning) pw.println("Backup currently running"); |
| pw.println(isBackupOperationInProgress() ? "Backup in progress" : "No backups running"); |
| pw.println("Last backup pass started: " + mLastBackupPass |
| + " (now = " + System.currentTimeMillis() + ')'); |
| pw.println(" next scheduled: " + KeyValueBackupJob.nextScheduled(mUserId)); |
| |
| pw.println("Transport whitelist:"); |
| for (ComponentName transport : mTransportManager.getTransportWhitelist()) { |
| pw.print(" "); |
| pw.println(transport.flattenToShortString()); |
| } |
| |
| pw.println("Available transports:"); |
| final String[] transports = listAllTransports(); |
| if (transports != null) { |
| for (String t : transports) { |
| pw.println((t.equals(mTransportManager.getCurrentTransportName()) ? " * " |
| : " ") + t); |
| try { |
| File dir = new File(mBaseStateDir, |
| mTransportManager.getTransportDirName(t)); |
| pw.println(" destination: " |
| + mTransportManager.getTransportCurrentDestinationString(t)); |
| pw.println(" intent: " |
| + mTransportManager.getTransportConfigurationIntent(t)); |
| for (File f : dir.listFiles()) { |
| pw.println( |
| " " + f.getName() + " - " + f.length() + " state bytes"); |
| } |
| } catch (Exception e) { |
| Slog.e(TAG, "Error in transport", e); |
| pw.println(" Error: " + e); |
| } |
| } |
| } |
| |
| mTransportManager.dumpTransportClients(pw); |
| |
| pw.println("Pending init: " + mPendingInits.size()); |
| for (String s : mPendingInits) { |
| pw.println(" " + s); |
| } |
| |
| pw.print("Ancestral: "); |
| pw.println(Long.toHexString(mAncestralToken)); |
| pw.print("Current: "); |
| pw.println(Long.toHexString(mCurrentToken)); |
| |
| int numPackages = mBackupParticipants.size(); |
| pw.println("Participants:"); |
| for (int i = 0; i < numPackages; i++) { |
| int uid = mBackupParticipants.keyAt(i); |
| pw.print(" uid: "); |
| pw.println(uid); |
| HashSet<String> participants = mBackupParticipants.valueAt(i); |
| for (String app : participants) { |
| pw.println(" " + app); |
| } |
| } |
| |
| pw.println("Ancestral packages: " |
| + (mAncestralPackages == null ? "none" : mAncestralPackages.size())); |
| if (mAncestralPackages != null) { |
| for (String pkg : mAncestralPackages) { |
| pw.println(" " + pkg); |
| } |
| } |
| |
| Set<String> processedPackages = mProcessedPackagesJournal.getPackagesCopy(); |
| pw.println("Ever backed up: " + processedPackages.size()); |
| for (String pkg : processedPackages) { |
| pw.println(" " + pkg); |
| } |
| |
| pw.println("Pending key/value backup: " + mPendingBackups.size()); |
| for (BackupRequest req : mPendingBackups.values()) { |
| pw.println(" " + req); |
| } |
| |
| pw.println("Full backup queue:" + mFullBackupQueue.size()); |
| for (FullBackupEntry entry : mFullBackupQueue) { |
| pw.print(" "); |
| pw.print(entry.lastBackup); |
| pw.print(" : "); |
| pw.println(entry.packageName); |
| } |
| } |
| } |
| |
| |
| public IBackupManager getBackupManagerBinder() { |
| return mBackupManagerBinder; |
| } |
| } |