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 com.android.car.setupwizardlib;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.os.AsyncTask;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.RemoteException;
  import android.util.Log;
  
  import com.android.car.setupwizardlib.InitialLockSetupConstants.LockTypes;
  import com.android.car.setupwizardlib.InitialLockSetupConstants.SetLockCodes;
  import com.android.car.setupwizardlib.InitialLockSetupConstants.ValidateLockFlags;
  
  import java.lang.ref.WeakReference;
  import java.util.HashMap;
  import java.util.Map;
  
  /**
   * Client for communicating with the InitialLockSetupService in Car Settings.
   * This allows a device setup wizard to set the initial lock on a device that does not have one.
   */
  public class InitialLockSetupClient implements ServiceConnection {
  
      private static final String INITIAL_LOCK_SERVICE_PACKAGE = "com.android.car.settings";
      private static final String INITIAL_LOCK_SERVICE_CLASS_NAME =
              INITIAL_LOCK_SERVICE_PACKAGE + ".setupservice.InitialLockSetupService";
      private static final Intent INITIAL_LOCK_SERVICE_INTENT = new Intent().setComponent(
              new ComponentName(INITIAL_LOCK_SERVICE_PACKAGE, INITIAL_LOCK_SERVICE_CLASS_NAME));
  
      private static final String TAG = "InitialLockSetupClient";
      // Arbitrary timeout before giving up on connecting to service.
      private static final long CONNECTION_TIMEOUT_MS = 5000;
  
      private final Runnable mTimeoutRunnable = this::connectionTimeout;
      private final Handler mHandler = new Handler(Looper.getMainLooper());
  
      private InitialLockListener mInitialLockListener;
      private Context mContext;
      private IInitialLockSetupService mInitialLockSetupService;
      private ValidateLockAsyncTask mCurrentValidateLockTask;
      private SaveLockAsyncTask mCurrentSaveLockTask;
      private boolean mCurrentlyBinding;
      // Tracks whether the connection has timed out to prevent duplicate callbacks to listener.
      private boolean mTimedOut;
  
      public InitialLockSetupClient(Context context) {
          mContext = context.getApplicationContext();
      }
  
      /**
       * Starts the connection to the service.
       */
      public void startConnection(InitialLockListener listener) {
          logVerbose("startConnection");
          mInitialLockListener = listener;
          if (mInitialLockSetupService != null || mCurrentlyBinding) {
              logVerbose("Unable to bind to initial setup service, already connected or connecting.");
              return;
          }
          // Reset whether this has timed out.
          mTimedOut = false;
          try {
              mHandler.postDelayed(mTimeoutRunnable, CONNECTION_TIMEOUT_MS);
              mCurrentlyBinding = mContext.bindService(INITIAL_LOCK_SERVICE_INTENT, /* connection= */
                      this,
                      Context.BIND_AUTO_CREATE);
  
          } catch (Exception e) {
              e.printStackTrace();
              mInitialLockListener.onConnectionAttemptFinished(false);
          }
          if (!mCurrentlyBinding) {
              logVerbose("Unable to bind to initial setup service");
          }
      }
  
      /**
       * Stops the connection and removes the active listener. Does nothing if not connected.
       */
      public void stopConnection() {
          logVerbose("stopConnection");
          if (mInitialLockSetupService == null) {
              logVerbose("Attempting to disconnect from service when not connected");
              return;
          }
          mContext.unbindService(this);
          mInitialLockSetupService = null;
          mInitialLockListener = null;
          mCurrentlyBinding = false;
      }
  
      /**
       * Returns whether the client is currently connected to the service.
       */
      public boolean isServiceConnected() {
          return false;
      }
  
      /**
       * Fetches the set of {@link LockConfig}s that define the lock constraints for the device.
       */
      public void getLockConfigs() {
          LockConfigsAsyncTask lockConfigsAsyncTask = new LockConfigsAsyncTask(mInitialLockListener,
                  mInitialLockSetupService);
          lockConfigsAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
      }
  
      /**
       * Returns whether the current password fits the Car Settings password
       * criteria. Otherwise returns the related error code (length, character
       * types, etc).
       */
      public void checkValidPassword(byte[] password) {
          logVerbose("checkValidPassword");
          if (mCurrentValidateLockTask != null
                  && mCurrentValidateLockTask.getStatus() != AsyncTask.Status.FINISHED) {
              mCurrentValidateLockTask.cancel(true);
          }
          mCurrentValidateLockTask = new ValidateLockAsyncTask(
                  mInitialLockListener, mInitialLockSetupService, LockTypes.PASSWORD);
          mCurrentValidateLockTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, password);
      }
  
      /**
       * Returns whether the current PIN fits the Car Settings PIN criteria.
       * Otherwise returns the related error code (length, character types, etc).
       */
      public void checkValidPin(byte[] pin) {
          logVerbose("checkValidPin");
          if (mCurrentValidateLockTask != null
                  && mCurrentValidateLockTask.getStatus() != AsyncTask.Status.FINISHED) {
              mCurrentValidateLockTask.cancel(true);
          }
          mCurrentValidateLockTask = new ValidateLockAsyncTask(
                  mInitialLockListener, mInitialLockSetupService, LockTypes.PIN);
          mCurrentValidateLockTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pin);
      }
  
      /**
       * Returns whether the current pattern fits the Car Settings pattern
       * criteria. Otherwise returns the related error code.
       */
      public void checkValidPattern(byte[] pattern) {
          logVerbose("checkValidPattern");
          if (mCurrentValidateLockTask != null
                  && mCurrentValidateLockTask.getStatus() != AsyncTask.Status.FINISHED) {
              mCurrentValidateLockTask.cancel(true);
          }
          mCurrentValidateLockTask = new ValidateLockAsyncTask(
                  mInitialLockListener, mInitialLockSetupService, LockTypes.PATTERN);
          mCurrentValidateLockTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pattern);
      }
  
      /**
       * Calls to the service to set the given password as the initial device
       * password.
       */
      public void saveLockPassword(byte[] password) {
          if (mCurrentSaveLockTask != null
                  && mCurrentSaveLockTask.getStatus() != AsyncTask.Status.FINISHED) {
              // If a save operation is already started, ignore this. Should not try to save
              // multiple locks at once.
              Log.e(TAG, "Can't save multiple passwords at once");
              return;
          }
          mCurrentSaveLockTask = new SaveLockAsyncTask(mInitialLockListener,
                  mInitialLockSetupService, LockTypes.PASSWORD);
          mCurrentSaveLockTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, password);
      }
  
      /**
       * Calls to the service to set the given PIN as the initial device PIN.
       */
      public void saveLockPin(byte[] pin) {
          if (mCurrentSaveLockTask != null
                  && mCurrentSaveLockTask.getStatus() != AsyncTask.Status.FINISHED) {
              // If a save operation is already started, ignore this. Should not try to save
              // multiple locks at once.
              Log.e(TAG, "Can't save multiple passwords at once");
              return;
          }
          mCurrentSaveLockTask = new SaveLockAsyncTask(mInitialLockListener,
                  mInitialLockSetupService, LockTypes.PIN);
          mCurrentSaveLockTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pin);
      }
  
      /**
       * Calls the service to set the given pattern as the initial device lock
       * pattern.
       */
      public void saveLockPattern(byte[] pattern) {
          if (mCurrentSaveLockTask != null
                  && mCurrentSaveLockTask.getStatus() != AsyncTask.Status.FINISHED) {
              // If a save operation is already started, ignore this. Should not try to save
              // multiple locks at once.
              Log.e(TAG, "Can't save multiple passwords at once");
              return;
          }
          mCurrentSaveLockTask = new SaveLockAsyncTask(mInitialLockListener,
                  mInitialLockSetupService, LockTypes.PATTERN);
          mCurrentSaveLockTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pattern);
      }
  
      @Override
      public void onServiceConnected(ComponentName name, IBinder service) {
          logVerbose("onServiceConnected");
          if (mTimedOut) {
              return;
          }
          mHandler.removeCallbacks(mTimeoutRunnable);
          mCurrentlyBinding = false;
          mInitialLockSetupService = IInitialLockSetupService.Stub.asInterface(service);
          if (mInitialLockListener != null) {
              mInitialLockListener.onConnectionAttemptFinished(true);
          }
      }
  
      @Override
      public void onServiceDisconnected(ComponentName name) {
          logVerbose("onServiceDisconnected");
          mCurrentlyBinding = false;
          mInitialLockSetupService = null;
      }
  
      @Override
      public void onNullBinding(ComponentName name) {
          logVerbose("onNullBinding");
          if (mTimedOut) {
              return;
          }
          mHandler.removeCallbacks(mTimeoutRunnable);
          mCurrentlyBinding = false;
          if (mInitialLockListener != null) {
              mInitialLockListener.onConnectionAttemptFinished(false);
          }
      }
  
      @Override
      public void onBindingDied(ComponentName name) {
          logVerbose("onBindingDied");
          if (mTimedOut) {
              return;
          }
          mHandler.removeCallbacks(mTimeoutRunnable);
          mCurrentlyBinding = false;
          if (mInitialLockListener != null) {
              mInitialLockListener.onConnectionAttemptFinished(false);
          }
      }
  
      private void connectionTimeout() {
          logVerbose("connectionTimeout");
          if (mInitialLockListener == null) {
              return;
          }
          mInitialLockListener.onConnectionAttemptFinished(false);
          mTimedOut = true;
      }
  
      private static class LockConfigsAsyncTask extends
              AsyncTask<Void, Void, Map<Integer, LockConfig>> {
  
          private WeakReference<InitialLockListener> mInitialLockListener;
          private WeakReference<IInitialLockSetupService> mInitialLockSetupService;
  
          LockConfigsAsyncTask(InitialLockListener initialLockListener,
                  IInitialLockSetupService initialLockSetupService) {
              mInitialLockListener = new WeakReference<>(initialLockListener);
              mInitialLockSetupService = new WeakReference<>(initialLockSetupService);
          }
  
          @Override
          protected Map<Integer, LockConfig> doInBackground(Void... voids) {
              IInitialLockSetupService initialLockSetupService = mInitialLockSetupService.get();
              if (initialLockSetupService == null) {
                  InitialLockSetupClient.logVerbose(
                          "Lost reference to service in LockConfigsAsyncTask");
                  return null;
              }
              LockConfig passwordConfig, pinConfig, patternConfig;
              try {
                  passwordConfig = initialLockSetupService.getLockConfig(LockTypes.PASSWORD);
                  pinConfig = initialLockSetupService.getLockConfig(LockTypes.PIN);
                  patternConfig = initialLockSetupService.getLockConfig(LockTypes.PATTERN);
              } catch (RemoteException e) {
                  e.printStackTrace();
                  return null;
              }
              Map<Integer, LockConfig> map = new HashMap<>();
              map.put(LockTypes.PASSWORD, passwordConfig);
              map.put(LockTypes.PIN, pinConfig);
              map.put(LockTypes.PATTERN, patternConfig);
              return map;
          }
  
          @Override
          protected void onPostExecute(Map<Integer, LockConfig> map) {
              InitialLockListener listener = mInitialLockListener.get();
              if (listener == null) {
                  return;
              }
              listener.onGetLockConfigs(map);
          }
      }
  
      private static class ValidateLockAsyncTask extends AsyncTask<byte[], Void, Integer> {
  
          private WeakReference<InitialLockListener> mInitialLockListener;
          private WeakReference<IInitialLockSetupService> mInitialLockSetupService;
          private int mLockType;
  
          ValidateLockAsyncTask(
                  InitialLockListener initialLockListener,
                  IInitialLockSetupService initialLockSetupService,
                  @LockTypes int lockType) {
              mInitialLockListener = new WeakReference<>(initialLockListener);
              mInitialLockSetupService = new WeakReference<>(initialLockSetupService);
              mLockType = lockType;
          }
  
          @Override
          protected Integer doInBackground(byte[]... passwords) {
              InitialLockSetupClient.logVerbose("ValidateLockAsyncTask doInBackground");
              IInitialLockSetupService initialLockSetupService = mInitialLockSetupService.get();
              if (initialLockSetupService == null) {
                  InitialLockSetupClient.logVerbose(
                          "Lost reference to service in ValidateLockAsyncTask");
                  return ValidateLockFlags.INVALID_GENERIC;
              }
              try {
                  int output = initialLockSetupService.checkValidLock(mLockType, passwords[0]);
                  return output;
              } catch (RemoteException e) {
                  e.printStackTrace();
                  return ValidateLockFlags.INVALID_GENERIC;
              }
          }
  
          @Override
          protected void onPostExecute(Integer resultCode) {
              if (isCancelled()) {
                  return;
              }
              InitialLockListener listener = mInitialLockListener.get();
              if (listener == null) {
                  return;
              }
              listener.onLockValidated(resultCode);
          }
      }
  
      private static class SaveLockAsyncTask extends AsyncTask<byte[], Void, Integer> {
  
          private WeakReference<InitialLockListener> mInitialLockListener;
          private WeakReference<IInitialLockSetupService> mInitialLockSetupService;
          private int mLockType;
  
          SaveLockAsyncTask(
                  InitialLockListener initialLockListener,
                  IInitialLockSetupService initialLockSetupService,
                  @LockTypes int lockType) {
              mInitialLockListener = new WeakReference<>(initialLockListener);
              mInitialLockSetupService = new WeakReference<>(initialLockSetupService);
              mLockType = lockType;
          }
  
          @Override
          protected Integer doInBackground(byte[]... passwords) {
              InitialLockSetupClient.logVerbose("SaveLockAsyncTask doInBackground");
              IInitialLockSetupService initialLockSetupService = mInitialLockSetupService.get();
              if (initialLockSetupService == null) {
                  InitialLockSetupClient.logVerbose(
                          "Lost reference to service in SaveLockAsyncTask");
                  return SetLockCodes.FAIL_LOCK_GENERIC;
              }
              try {
                  int output = initialLockSetupService.setLock(mLockType, passwords[0]);
                  return output;
              } catch (RemoteException e) {
                  e.printStackTrace();
                  return SetLockCodes.FAIL_LOCK_GENERIC;
              }
          }
  
          @Override
          protected void onPostExecute(Integer resultCode) {
              InitialLockListener listener = mInitialLockListener.get();
              if (listener == null) {
                  return;
              }
              listener.onSetLockFinished(resultCode);
          }
      }
  
      private static void logVerbose(String logStatement) {
          if (Log.isLoggable(TAG, Log.VERBOSE)) {
              Log.v(TAG, logStatement);
          }
      }
  }
  
  