content: /*
   * 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.restore;
  
  import static com.android.server.backup.BackupManagerService.DEBUG;
  import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
  import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
  import static com.android.server.backup.internal.BackupHandler.MSG_RUN_GET_RESTORE_SETS;
  import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.backup.IBackupManagerMonitor;
  import android.app.backup.IRestoreObserver;
  import android.app.backup.IRestoreSession;
  import android.app.backup.RestoreSet;
  import android.content.pm.PackageInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.Message;
  import android.util.Slog;
  
  import com.android.server.backup.TransportManager;
  import com.android.server.backup.UserBackupManagerService;
  import com.android.server.backup.internal.OnTaskFinishedListener;
  import com.android.server.backup.params.RestoreGetSetsParams;
  import com.android.server.backup.params.RestoreParams;
  import com.android.server.backup.transport.TransportClient;
  
  import java.util.function.BiFunction;
  
  /**
   * Restore session.
   */
  public class ActiveRestoreSession extends IRestoreSession.Stub {
      private static final String TAG = "RestoreSession";
  
      private final TransportManager mTransportManager;
      private final String mTransportName;
      private final UserBackupManagerService mBackupManagerService;
      private final int mUserId;
      @Nullable private final String mPackageName;
      public RestoreSet[] mRestoreSets = null;
      boolean mEnded = false;
      boolean mTimedOut = false;
  
      public ActiveRestoreSession(
              UserBackupManagerService backupManagerService,
              @Nullable String packageName,
              String transportName) {
          mBackupManagerService = backupManagerService;
          mPackageName = packageName;
          mTransportManager = backupManagerService.getTransportManager();
          mTransportName = transportName;
          mUserId = backupManagerService.getUserId();
      }
  
      public void markTimedOut() {
          mTimedOut = true;
      }
  
      // --- Binder interface ---
      public synchronized int getAvailableRestoreSets(IRestoreObserver observer,
              IBackupManagerMonitor monitor) {
          mBackupManagerService.getContext().enforceCallingOrSelfPermission(
                  android.Manifest.permission.BACKUP,
                  "getAvailableRestoreSets");
          if (observer == null) {
              throw new IllegalArgumentException("Observer must not be null");
          }
  
          if (mEnded) {
              throw new IllegalStateException("Restore session already ended");
          }
  
          if (mTimedOut) {
              Slog.i(TAG, "Session already timed out");
              return -1;
          }
  
          long oldId = Binder.clearCallingIdentity();
          try {
              TransportClient transportClient =
                      mTransportManager.getTransportClient(
                                      mTransportName, "RestoreSession.getAvailableRestoreSets()");
              if (transportClient == null) {
                  Slog.w(TAG, "Null transport client getting restore sets");
                  return -1;
              }
  
              // We know we're doing legit work now, so halt the timeout
              // until we're done.  It gets started again when the result
              // comes in.
              mBackupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
  
              UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock();
              wakelock.acquire();
  
              // Prevent lambda from leaking 'this'
              TransportManager transportManager = mTransportManager;
              OnTaskFinishedListener listener = caller -> {
                      transportManager.disposeOfTransportClient(transportClient, caller);
                      wakelock.release();
              };
              Message msg = mBackupManagerService.getBackupHandler().obtainMessage(
                      MSG_RUN_GET_RESTORE_SETS,
                      new RestoreGetSetsParams(transportClient, this, observer, monitor, listener));
              mBackupManagerService.getBackupHandler().sendMessage(msg);
              return 0;
          } catch (Exception e) {
              Slog.e(TAG, "Error in getAvailableRestoreSets", e);
              return -1;
          } finally {
              Binder.restoreCallingIdentity(oldId);
          }
      }
  
      public synchronized int restoreAll(long token, IRestoreObserver observer,
              IBackupManagerMonitor monitor) {
          mBackupManagerService.getContext().enforceCallingOrSelfPermission(
                  android.Manifest.permission.BACKUP,
                  "performRestore");
  
          if (DEBUG) {
              Slog.d(TAG, "restoreAll token=" + Long.toHexString(token)
                      + " observer=" + observer);
          }
  
          if (mEnded) {
              throw new IllegalStateException("Restore session already ended");
          }
  
          if (mTimedOut) {
              Slog.i(TAG, "Session already timed out");
              return -1;
          }
  
          if (mRestoreSets == null) {
              Slog.e(TAG, "Ignoring restoreAll() with no restore set");
              return -1;
          }
  
          if (mPackageName != null) {
              Slog.e(TAG, "Ignoring restoreAll() on single-package session");
              return -1;
          }
  
          if (!mTransportManager.isTransportRegistered(mTransportName)) {
              Slog.e(TAG, "Transport " + mTransportName + " not registered");
              return -1;
          }
  
          synchronized (mBackupManagerService.getQueueLock()) {
              for (int i = 0; i < mRestoreSets.length; i++) {
                  if (token == mRestoreSets[i].token) {
                      long oldId = Binder.clearCallingIdentity();
                      try {
                          return sendRestoreToHandlerLocked(
                                  (transportClient, listener) ->
                                          RestoreParams.createForRestoreAll(
                                                  transportClient,
                                                  observer,
                                                  monitor,
                                                  token,
                                                  listener),
                                  "RestoreSession.restoreAll()");
                      } finally {
                          Binder.restoreCallingIdentity(oldId);
                      }
                  }
              }
          }
  
          Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found");
          return -1;
      }
  
      // Restores of more than a single package are treated as 'system' restores
      public synchronized int restorePackages(long token, @Nullable IRestoreObserver observer,
              @NonNull String[] packages, @Nullable IBackupManagerMonitor monitor) {
          mBackupManagerService.getContext().enforceCallingOrSelfPermission(
                  android.Manifest.permission.BACKUP,
                  "performRestore");
  
          if (DEBUG) {
              StringBuilder b = new StringBuilder(128);
              b.append("restorePackages token=");
              b.append(Long.toHexString(token));
              b.append(" observer=");
              if (observer == null) {
                  b.append("null");
              } else {
                  b.append(observer.toString());
              }
              b.append(" monitor=");
              if (monitor == null) {
                  b.append("null");
              } else {
                  b.append(monitor.toString());
              }
              b.append(" packages=");
              if (packages == null) {
                  b.append("null");
              } else {
                  b.append('{');
                  boolean first = true;
                  for (String s : packages) {
                      if (!first) {
                          b.append(", ");
                      } else {
                          first = false;
                      }
                      b.append(s);
                  }
                  b.append('}');
              }
              Slog.d(TAG, b.toString());
          }
  
          if (mEnded) {
              throw new IllegalStateException("Restore session already ended");
          }
  
          if (mTimedOut) {
              Slog.i(TAG, "Session already timed out");
              return -1;
          }
  
          if (mRestoreSets == null) {
              Slog.e(TAG, "Ignoring restoreAll() with no restore set");
              return -1;
          }
  
          if (mPackageName != null) {
              Slog.e(TAG, "Ignoring restoreAll() on single-package session");
              return -1;
          }
  
          if (!mTransportManager.isTransportRegistered(mTransportName)) {
              Slog.e(TAG, "Transport " + mTransportName + " not registered");
              return -1;
          }
  
          synchronized (mBackupManagerService.getQueueLock()) {
              for (int i = 0; i < mRestoreSets.length; i++) {
                  if (token == mRestoreSets[i].token) {
                      long oldId = Binder.clearCallingIdentity();
                      try {
                          return sendRestoreToHandlerLocked(
                                  (transportClient, listener) ->
                                          RestoreParams.createForRestorePackages(
                                                  transportClient,
                                                  observer,
                                                  monitor,
                                                  token,
                                                  packages,
                                                  /* isSystemRestore */ packages.length > 1,
                                                  listener),
                                  "RestoreSession.restorePackages(" + packages.length + " packages)");
                      } finally {
                          Binder.restoreCallingIdentity(oldId);
                      }
                  }
              }
          }
  
          Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found");
          return -1;
      }
  
      public synchronized int restorePackage(String packageName, IRestoreObserver observer,
              IBackupManagerMonitor monitor) {
          if (DEBUG) {
              Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer
                      + "monitor=" + monitor);
          }
  
          if (mEnded) {
              throw new IllegalStateException("Restore session already ended");
          }
  
          if (mTimedOut) {
              Slog.i(TAG, "Session already timed out");
              return -1;
          }
  
          if (mPackageName != null) {
              if (!mPackageName.equals(packageName)) {
                  Slog.e(TAG, "Ignoring attempt to restore pkg=" + packageName
                          + " on session for package " + mPackageName);
                  return -1;
              }
          }
  
          final PackageInfo app;
          try {
              app = mBackupManagerService.getPackageManager().getPackageInfoAsUser(
                      packageName, 0, mUserId);
          } catch (NameNotFoundException nnf) {
              Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName);
              return -1;
          }
  
          // If the caller is not privileged and is not coming from the target
          // app's uid, throw a permission exception back to the caller.
          int perm = mBackupManagerService.getContext().checkPermission(
                  android.Manifest.permission.BACKUP,
                  Binder.getCallingPid(), Binder.getCallingUid());
          if ((perm == PackageManager.PERMISSION_DENIED) &&
                  (app.applicationInfo.uid != Binder.getCallingUid())) {
              Slog.w(TAG, "restorePackage: bad packageName=" + packageName
                      + " or calling uid=" + Binder.getCallingUid());
              throw new SecurityException("No permission to restore other packages");
          }
  
          if (!mTransportManager.isTransportRegistered(mTransportName)) {
              Slog.e(TAG, "Transport " + mTransportName + " not registered");
              return -1;
          }
  
          // So far so good; we're allowed to try to restore this package.
          long oldId = Binder.clearCallingIdentity();
          try {
              // Check whether there is data for it in the current dataset, falling back
              // to the ancestral dataset if not.
              long token = mBackupManagerService.getAvailableRestoreToken(packageName);
              if (DEBUG) {
                  Slog.v(TAG, "restorePackage pkg=" + packageName
                          + " token=" + Long.toHexString(token));
              }
  
              // If we didn't come up with a place to look -- no ancestral dataset and
              // the app has never been backed up from this device -- there's nothing
              // to do but return failure.
              if (token == 0) {
                  if (DEBUG) {
                      Slog.w(TAG, "No data available for this package; not restoring");
                  }
                  return -1;
              }
  
              return sendRestoreToHandlerLocked(
                      (transportClient, listener) ->
                              RestoreParams.createForSinglePackage(
                                      transportClient,
                                      observer,
                                      monitor,
                                      token,
                                      app,
                                      listener),
                      "RestoreSession.restorePackage(" + packageName + ")");
          } finally {
              Binder.restoreCallingIdentity(oldId);
          }
      }
  
      public void setRestoreSets(RestoreSet[] restoreSets) {
          mRestoreSets = restoreSets;
      }
  
      /**
       * Returns 0 if operation sent or -1 otherwise.
       */
      private int sendRestoreToHandlerLocked(
              BiFunction<TransportClient, OnTaskFinishedListener, RestoreParams> restoreParamsBuilder,
              String callerLogString) {
          TransportClient transportClient =
                  mTransportManager.getTransportClient(mTransportName, callerLogString);
          if (transportClient == null) {
              Slog.e(TAG, "Transport " + mTransportName + " got unregistered");
              return -1;
          }
  
          // Stop the session timeout until we finalize the restore
          Handler backupHandler = mBackupManagerService.getBackupHandler();
          backupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
  
          UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock();
          wakelock.acquire();
          if (MORE_DEBUG) {
              Slog.d(TAG, callerLogString);
          }
  
          // Prevent lambda from leaking 'this'
          TransportManager transportManager = mTransportManager;
          OnTaskFinishedListener listener = caller -> {
                  transportManager.disposeOfTransportClient(transportClient, caller);
                  wakelock.release();
          };
          Message msg = backupHandler.obtainMessage(MSG_RUN_RESTORE);
          msg.obj = restoreParamsBuilder.apply(transportClient, listener);
          backupHandler.sendMessage(msg);
          return 0;
      }
  
      // Posted to the handler to tear down a restore session in a cleanly synchronized way
      public class EndRestoreRunnable implements Runnable {
  
          UserBackupManagerService mBackupManager;
          ActiveRestoreSession mSession;
  
          public EndRestoreRunnable(UserBackupManagerService manager, ActiveRestoreSession session) {
              mBackupManager = manager;
              mSession = session;
          }
  
          public void run() {
              // clean up the session's bookkeeping
              synchronized (mSession) {
                  mSession.mEnded = true;
              }
  
              // clean up the BackupManagerImpl side of the bookkeeping
              // and cancel any pending timeout message
              mBackupManager.clearRestoreSession(mSession);
          }
      }
  
      public synchronized void endRestoreSession() {
          if (DEBUG) {
              Slog.d(TAG, "endRestoreSession");
          }
  
          if (mTimedOut) {
              Slog.i(TAG, "Session already timed out");
              return;
          }
  
          if (mEnded) {
              throw new IllegalStateException("Restore session already ended");
          }
  
          mBackupManagerService.getBackupHandler().post(
                  new EndRestoreRunnable(mBackupManagerService, this));
      }
  }
  