content: /*
   * Copyright (C) 2019 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 android.os.image;
  
  import android.annotation.BytesLong;
  import android.annotation.CallbackExecutor;
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresPermission;
  import android.annotation.SystemApi;
  import android.annotation.TestApi;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.net.Uri;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Messenger;
  import android.os.ParcelableException;
  import android.os.RemoteException;
  import android.os.SystemProperties;
  import android.util.FeatureFlagUtils;
  import android.util.Slog;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.ref.WeakReference;
  import java.util.concurrent.Executor;
  
  /**
   * <p>This class contains methods and constants used to start a {@code DynamicSystem} installation,
   * and a listener for status updates.</p>
   *
   * <p>{@code DynamicSystem} allows users to run certified system images in a non destructive manner
   * without needing to prior OEM unlock. It creates a temporary system partition to install the new
   * system image, and a temporary data partition for the newly installed system to run with.</p>
   *
   * After the installation is completed, the device will be running in the new system on next the
   * reboot. Then, when the user reboots the device again, it will leave {@code DynamicSystem} and go
   * back to the original system. While running in {@code DynamicSystem}, persitent storage for
   * factory reset protection (FRP) remains unchanged. Since the user is running the new system with
   * a temporarily created data partition, their original user data are kept unchanged.</p>
   *
   * <p>With {@link #setOnStatusChangedListener}, API users can register an
   * {@link #OnStatusChangedListener} to get status updates and their causes when the installation is
   * started, stopped, or cancelled. It also sends progress updates during the installation. With
   * {@link #start}, API users can start an installation with the {@link Uri} to a unsparsed and
   * gzipped system image. The {@link Uri} can be a web URL or a content Uri to a local path.</p>
   *
   * @hide
   */
  @SystemApi
  @TestApi
  public class DynamicSystemClient {
      /** @hide */
      @IntDef(prefix = { "STATUS_" }, value = {
              STATUS_UNKNOWN,
              STATUS_NOT_STARTED,
              STATUS_IN_PROGRESS,
              STATUS_READY,
              STATUS_IN_USE,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface InstallationStatus {}
  
      /** @hide */
      @IntDef(prefix = { "CAUSE_" }, value = {
              CAUSE_NOT_SPECIFIED,
              CAUSE_INSTALL_COMPLETED,
              CAUSE_INSTALL_CANCELLED,
              CAUSE_ERROR_IO,
              CAUSE_ERROR_INVALID_URL,
              CAUSE_ERROR_IPC,
              CAUSE_ERROR_EXCEPTION,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface StatusChangedCause {}
  
      private static final String TAG = "DynSystemClient";
  
      private static final long DEFAULT_USERDATA_SIZE = (10L << 30);
  
  
      /** Listener for installation status updates. */
      public interface OnStatusChangedListener {
          /**
           * This callback is called when installation status is changed, and when the
           * client is {@link #bind} to {@code DynamicSystem} installation service.
           *
           * @param status status code, also defined in {@code DynamicSystemClient}.
           * @param cause cause code, also defined in {@code DynamicSystemClient}.
           * @param progress number of bytes installed.
           * @param detail additional detail about the error if available, otherwise null.
           */
          void onStatusChanged(@InstallationStatus int status, @StatusChangedCause int cause,
                  @BytesLong long progress, @Nullable Throwable detail);
      }
  
      /*
       * Status codes
       */
      /** We are bound to installation service, but failed to get its status */
      public static final int STATUS_UNKNOWN = 0;
  
      /** Installation is not started yet. */
      public static final int STATUS_NOT_STARTED = 1;
  
      /** Installation is in progress. */
      public static final int STATUS_IN_PROGRESS = 2;
  
      /** Installation is finished but the user has not launched it. */
      public static final int STATUS_READY = 3;
  
      /** Device is running in {@code DynamicSystem}. */
      public static final int STATUS_IN_USE = 4;
  
      /*
       * Causes
       */
      /** Cause is not specified. This means the status is not changed. */
      public static final int CAUSE_NOT_SPECIFIED = 0;
  
      /** Status changed because installation is completed. */
      public static final int CAUSE_INSTALL_COMPLETED = 1;
  
      /** Status changed because installation is cancelled. */
      public static final int CAUSE_INSTALL_CANCELLED = 2;
  
      /** Installation failed due to {@code IOException}. */
      public static final int CAUSE_ERROR_IO = 3;
  
      /** Installation failed because the image URL source is not supported. */
      public static final int CAUSE_ERROR_INVALID_URL = 4;
  
      /** Installation failed due to IPC error. */
      public static final int CAUSE_ERROR_IPC = 5;
  
      /** Installation failed due to unhandled exception. */
      public static final int CAUSE_ERROR_EXCEPTION = 6;
  
      /*
       * IPC Messages
       */
      /**
       * Message to register listener.
       * @hide
       */
      public static final int MSG_REGISTER_LISTENER = 1;
  
      /**
       * Message to unregister listener.
       * @hide
       */
      public static final int MSG_UNREGISTER_LISTENER = 2;
  
      /**
       * Message for status updates.
       * @hide
       */
      public static final int MSG_POST_STATUS = 3;
  
      /*
       * Messages keys
       */
      /**
       * Message key, for progress updates.
       * @hide
       */
      public static final String KEY_INSTALLED_SIZE = "KEY_INSTALLED_SIZE";
  
      /**
       * Message key, used when the service is sending exception detail to the client.
       * @hide
       */
      public static final String KEY_EXCEPTION_DETAIL = "KEY_EXCEPTION_DETAIL";
  
      /*
       * Intent Actions
       */
      /**
       * Intent action: start installation.
       * @hide
       */
      public static final String ACTION_START_INSTALL =
              "android.os.image.action.START_INSTALL";
  
      /**
       * Intent action: notify user if we are currently running in {@code DynamicSystem}.
       * @hide
       */
      public static final String ACTION_NOTIFY_IF_IN_USE =
              "android.os.image.action.NOTIFY_IF_IN_USE";
  
      /*
       * Intent Keys
       */
      /**
       * Intent key: Size of the system image, in bytes.
       * @hide
       */
      public static final String KEY_SYSTEM_SIZE = "KEY_SYSTEM_SIZE";
  
      /**
       * Intent key: Number of bytes to reserve for userdata.
       * @hide
       */
      public static final String KEY_USERDATA_SIZE = "KEY_USERDATA_SIZE";
  
  
      private static class IncomingHandler extends Handler {
          private final WeakReference<DynamicSystemClient> mWeakClient;
  
          IncomingHandler(DynamicSystemClient service) {
              super(Looper.getMainLooper());
              mWeakClient = new WeakReference<>(service);
          }
  
          @Override
          public void handleMessage(Message msg) {
              DynamicSystemClient service = mWeakClient.get();
  
              if (service != null) {
                  service.handleMessage(msg);
              }
          }
      }
  
      private class DynSystemServiceConnection implements ServiceConnection {
          public void onServiceConnected(ComponentName className, IBinder service) {
              Slog.v(TAG, "DynSystemService connected");
  
              mService = new Messenger(service);
  
              try {
                  Message msg = Message.obtain(null, MSG_REGISTER_LISTENER);
                  msg.replyTo = mMessenger;
  
                  mService.send(msg);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Unable to get status from installation service");
                  mExecutor.execute(() -> {
                      mListener.onStatusChanged(STATUS_UNKNOWN, CAUSE_ERROR_IPC, 0, e);
                  });
              }
          }
  
          public void onServiceDisconnected(ComponentName className) {
              Slog.v(TAG, "DynSystemService disconnected");
              mService = null;
          }
      }
  
      private final Context mContext;
      private final DynSystemServiceConnection mConnection;
      private final Messenger mMessenger;
  
      private boolean mBound;
      private Executor mExecutor;
      private OnStatusChangedListener mListener;
      private Messenger mService;
  
      /**
       * Create a new {@code DynamicSystem} client.
       *
       * @param context a {@link Context} will be used to bind the installation service.
       *
       * @hide
       */
      @SystemApi
      @TestApi
      public DynamicSystemClient(@NonNull Context context) {
          mContext = context;
          mConnection = new DynSystemServiceConnection();
          mMessenger = new Messenger(new IncomingHandler(this));
      }
  
      /**
       * This method register a listener for status change. The listener is called using
       * the executor.
       */
      public void setOnStatusChangedListener(
              @NonNull @CallbackExecutor Executor executor,
              @NonNull OnStatusChangedListener listener) {
          mListener = listener;
          mExecutor = executor;
      }
  
      /**
       * This method register a listener for status change. The listener is called in main
       * thread.
       */
      public void setOnStatusChangedListener(
              @NonNull OnStatusChangedListener listener) {
          mListener = listener;
          mExecutor = null;
      }
  
      /**
       * Bind to {@code DynamicSystem} installation service. Binding to the installation service
       * allows it to send status updates to {@link #OnStatusChangedListener}. It is recommanded
       * to bind before calling {@link #start} and get status updates.
       * @hide
       */
      @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
      @SystemApi
      @TestApi
      public void bind() {
          if (!featureFlagEnabled()) {
              Slog.w(TAG, FeatureFlagUtils.DYNAMIC_SYSTEM + " not enabled; bind() aborted.");
              return;
          }
  
          Intent intent = new Intent();
          intent.setClassName("com.android.dynsystem",
                  "com.android.dynsystem.DynamicSystemInstallationService");
  
          mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
  
          mBound = true;
      }
  
      /**
       * Unbind from {@code DynamicSystem} installation service. Unbinding from the installation
       * service stops it from sending following status updates.
       * @hide
       */
      @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
      @SystemApi
      @TestApi
      public void unbind() {
          if (!mBound) {
              return;
          }
  
          if (mService != null) {
              try {
                  Message msg = Message.obtain(null, MSG_UNREGISTER_LISTENER);
                  msg.replyTo = mMessenger;
                  mService.send(msg);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Unable to unregister from installation service");
              }
          }
  
          // Detach our existing connection.
          mContext.unbindService(mConnection);
  
          mBound = false;
      }
  
      /**
       * Start installing {@code DynamicSystem} from URL with default userdata size.
       *
       * Calling this function will first start an Activity to confirm device credential, using
       * {@link KeyguardManager}. If it's confirmed, the installation service will be started.
       *
       * This function doesn't require prior calling {@link #bind}.
       *
       * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
       * @param systemSize size of system image.
       * @hide
       */
      @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
      @SystemApi
      @TestApi
      public void start(@NonNull Uri systemUrl, @BytesLong long systemSize) {
          start(systemUrl, systemSize, DEFAULT_USERDATA_SIZE);
      }
  
      /**
       * Start installing {@code DynamicSystem} from URL.
       *
       * Calling this function will first start an Activity to confirm device credential, using
       * {@link KeyguardManager}. If it's confirmed, the installation service will be started.
       *
       * This function doesn't require prior calling {@link #bind}.
       *
       * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
       * @param systemSize size of system image.
       * @param userdataSize bytes reserved for userdata.
       */
      @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
      public void start(@NonNull Uri systemUrl, @BytesLong long systemSize,
              @BytesLong long userdataSize) {
          if (!featureFlagEnabled()) {
              Slog.w(TAG, FeatureFlagUtils.DYNAMIC_SYSTEM + " not enabled; start() aborted.");
              return;
          }
  
          Intent intent = new Intent();
  
          intent.setClassName("com.android.dynsystem",
                  "com.android.dynsystem.VerificationActivity");
  
          intent.setData(systemUrl);
          intent.setAction(ACTION_START_INSTALL);
  
          intent.putExtra(KEY_SYSTEM_SIZE, systemSize);
          intent.putExtra(KEY_USERDATA_SIZE, userdataSize);
  
          mContext.startActivity(intent);
      }
  
      private boolean featureFlagEnabled() {
          return SystemProperties.getBoolean(
                  FeatureFlagUtils.PERSIST_PREFIX + FeatureFlagUtils.DYNAMIC_SYSTEM, false);
      }
  
      private void handleMessage(Message msg) {
          switch (msg.what) {
              case MSG_POST_STATUS:
                  int status = msg.arg1;
                  int cause = msg.arg2;
                  // obj is non-null
                  Bundle bundle = (Bundle) msg.obj;
                  long progress = bundle.getLong(KEY_INSTALLED_SIZE);
                  ParcelableException t = (ParcelableException) bundle.getSerializable(
                          KEY_EXCEPTION_DETAIL);
  
                  Throwable detail = t == null ? null : t.getCause();
  
                  if (mExecutor != null) {
                      mExecutor.execute(() -> {
                          mListener.onStatusChanged(status, cause, progress, detail);
                      });
                  } else {
                      mListener.onStatusChanged(status, cause, progress, detail);
                  }
                  break;
              default:
                  // do nothing
  
          }
      }
  }
  