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.server.incident;
  
  import android.app.AppOpsManager;
  import android.app.BroadcastOptions;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.net.Uri;
  import android.os.Handler;
  import android.os.IIncidentAuthListener;
  import android.os.IncidentManager;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.util.Log;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.Iterator;
  import java.util.List;
  
  // TODO: User changes should deny everything that's pending.
  
  /**
   * Tracker for reports pending approval.
   */
  class PendingReports {
      static final String TAG = IncidentCompanionService.TAG;
  
      private final Handler mHandler = new Handler();
      private final RequestQueue mRequestQueue = new RequestQueue(mHandler);
      private final Context mContext;
      private final PackageManager mPackageManager;
      private final AppOpsManager mAppOpsManager;
  
      //
      // All fields below must be protected by mLock
      //
      private final Object mLock = new Object();
      private final ArrayList<PendingReportRec> mPending = new ArrayList();
  
      /**
       * The next ID we'll use when we make a PendingReportRec.
       */
      private int mNextPendingId = 1;
  
      /**
       * One for each authorization that's pending.
       */
      private final class PendingReportRec {
          public int id;
          public String callingPackage;
          public int flags;
          public IIncidentAuthListener listener;
          public long addedRealtime;
          public long addedWalltime;
          public String receiverClass;
          public String reportId;
  
          /**
           * Construct a PendingReportRec, with an auto-incremented id.
           */
          PendingReportRec(String callingPackage, String receiverClass, String reportId, int flags,
                  IIncidentAuthListener listener) {
              this.id = mNextPendingId++;
              this.callingPackage = callingPackage;
              this.flags = flags;
              this.listener = listener;
              this.addedRealtime = SystemClock.elapsedRealtime();
              this.addedWalltime = System.currentTimeMillis();
              this.receiverClass = receiverClass;
              this.reportId = reportId;
          }
  
          /**
           * Get the Uri that contains the flattened data.
           */
          Uri getUri() {
              final Uri.Builder builder = (new Uri.Builder())
                      .scheme(IncidentManager.URI_SCHEME)
                      .authority(IncidentManager.URI_AUTHORITY)
                      .path(IncidentManager.URI_PATH)
                      .appendQueryParameter(IncidentManager.URI_PARAM_ID, Integer.toString(id))
                      .appendQueryParameter(IncidentManager.URI_PARAM_CALLING_PACKAGE, callingPackage)
                      .appendQueryParameter(IncidentManager.URI_PARAM_FLAGS, Integer.toString(flags))
                      .appendQueryParameter(IncidentManager.URI_PARAM_TIMESTAMP,
                              Long.toString(addedWalltime));
              if (receiverClass != null && receiverClass.length() > 0) {
                  builder.appendQueryParameter(IncidentManager.URI_PARAM_RECEIVER_CLASS,
                          receiverClass);
              }
              if (reportId != null && reportId.length() > 0) {
                  builder.appendQueryParameter(IncidentManager.URI_PARAM_REPORT_ID, reportId);
              }
              return builder.build();
          }
      }
  
      /**
       * Construct new PendingReports with the context.
       */
      PendingReports(Context context) {
          mContext = context;
          mPackageManager = context.getPackageManager();
          mAppOpsManager = context.getSystemService(AppOpsManager.class);
      }
  
      /**
       * ONEWAY binder call to initiate authorizing the report.  The actual logic is posted
       * to mRequestQueue, and may happen later.
       * <p>
       * The security checks are handled by IncidentCompanionService.
       */
      public void authorizeReport(int callingUid, final String callingPackage,
              final String receiverClass, final String reportId, final int flags,
              final IIncidentAuthListener listener) {
          // Starting the system server is complicated, and rather than try to
          // have a complicated lifecycle that we share with dumpstated and incidentd,
          // we will accept the request, and then display it whenever it becomes possible to.
          mRequestQueue.enqueue(listener.asBinder(), true, () -> {
              authorizeReportImpl(callingUid, callingPackage, receiverClass, reportId,
                      flags, listener);
          });
      }
  
      /**
       * ONEWAY binder call to cancel the inbound authorization request.
       * <p>
       * This is a oneway call, and so is authorizeReport, so the
       * caller's ordering is preserved.  The other calls on this object are synchronous, so
       * their ordering is not guaranteed with respect to these calls.  So the implementation
       * sends out extra broadcasts to allow for eventual consistency.
       * <p>
       * The security checks are handled by IncidentCompanionService.
       */
      public void cancelAuthorization(final IIncidentAuthListener listener) {
          mRequestQueue.enqueue(listener.asBinder(), false, () -> {
              cancelReportImpl(listener);
          });
      }
  
      /**
       * SYNCHRONOUS binder call to get the list of reports that are pending confirmation
       * by the user.
       * <p>
       * The security checks are handled by IncidentCompanionService.
       */
      public List<String> getPendingReports() {
          synchronized (mLock) {
              final int size = mPending.size();
              final ArrayList<String> result = new ArrayList(size);
              for (int i = 0; i < size; i++) {
                  result.add(mPending.get(i).getUri().toString());
              }
              return result;
          }
      }
  
      /**
       * SYNCHRONOUS binder call to mark a report as approved.
       * <p>
       * The security checks are handled by IncidentCompanionService.
       */
      public void approveReport(String uri) {
          final PendingReportRec rec;
          synchronized (mLock) {
              rec = findAndRemovePendingReportRecLocked(uri);
              if (rec == null) {
                  Log.e(TAG, "confirmApproved: Couldn't find record for uri: " + uri);
                  return;
              }
          }
  
          // Re-do the broadcast, so whoever is listening knows the list changed,
          // in case another one was added in the meantime.
          sendBroadcast();
  
          Log.i(TAG, "Approved report: " + uri);
          try {
              rec.listener.onReportApproved();
          } catch (RemoteException ex) {
              Log.w(TAG, "Failed calling back for approval for: " + uri, ex);
          }
      }
  
      /**
       * SYNCHRONOUS binder call to mark a report as NOT approved.
       */
      public void denyReport(String uri) {
          final PendingReportRec rec;
          synchronized (mLock) {
              rec = findAndRemovePendingReportRecLocked(uri);
              if (rec == null) {
                  Log.e(TAG, "confirmDenied: Couldn't find record for uri: " + uri);
                  return;
              }
          }
  
          // Re-do the broadcast, so whoever is listening knows the list changed,
          // in case another one was added in the meantime.
          sendBroadcast();
  
          Log.i(TAG, "Denied report: " + uri);
          try {
              rec.listener.onReportDenied();
          } catch (RemoteException ex) {
              Log.w(TAG, "Failed calling back for denial for: " + uri, ex);
          }
      }
  
      /**
       * Implementation of adb shell dumpsys debugreportcompanion.
       */
      protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
          if (args.length == 0) {
              // Standard text dumpsys
              final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
              synchronized (mLock) {
                  final int size = mPending.size();
                  writer.println("mPending: (" + size + ")");
                  for (int i = 0; i < size; i++) {
                      final PendingReportRec entry = mPending.get(i);
                      writer.println(String.format("  %11d %s: %s", entry.addedRealtime,
                                  df.format(new Date(entry.addedWalltime)),
                                  entry.getUri().toString()));
                  }
              }
          }
      }
  
      /**
       * Handle the boot process... Starts everything running once the system is
       * up enough for us to do UI.
       */
      public void onBootCompleted() {
          // Release the enqueued work.
          mRequestQueue.start();
      }
  
      /**
       * Start the confirmation process.
       */
      private void authorizeReportImpl(int callingUid, final String callingPackage,
              final String receiverClass, final String reportId,
              int flags, final IIncidentAuthListener listener) {
          // Enforce that the calling package pertains to the callingUid.
          if (callingUid != 0 && !isPackageInUid(callingUid, callingPackage)) {
              Log.w(TAG, "Calling uid " + callingUid + " doesn't match package "
                      + callingPackage);
              denyReportBeforeAddingRec(listener, callingPackage);
              return;
          }
  
          // Find the primary user of this device.
          final int primaryUser = getAndValidateUser();
          if (primaryUser == UserHandle.USER_NULL) {
              denyReportBeforeAddingRec(listener, callingPackage);
              return;
          }
  
          // Find the approver app (hint: it's PermissionController).
          final ComponentName receiver = getApproverComponent(primaryUser);
          if (receiver == null) {
              // We couldn't find an approver... so deny the request here and now, before we
              // do anything else.
              denyReportBeforeAddingRec(listener, callingPackage);
              return;
          }
  
          // Save the record for when the PermissionController comes back to authorize it.
          PendingReportRec rec = null;
          synchronized (mLock) {
              rec = new PendingReportRec(callingPackage, receiverClass, reportId, flags, listener);
              mPending.add(rec);
          }
  
          try {
              listener.asBinder().linkToDeath(() -> {
                  Log.i(TAG, "Got death notification listener=" + listener);
                  cancelReportImpl(listener, receiver, primaryUser);
              }, 0);
          } catch (RemoteException ex) {
              Log.e(TAG, "Remote died while trying to register death listener: " + rec.getUri());
              // First, remove from our list.
              cancelReportImpl(listener, receiver, primaryUser);
          }
  
          // Go tell Permission controller to start asking the user.
          sendBroadcast(receiver, primaryUser);
      }
  
      /**
       * Cancel a pending report request (because of an explicit call to cancel)
       */
      private void cancelReportImpl(IIncidentAuthListener listener) {
          final int primaryUser = getAndValidateUser();
          final ComponentName receiver = getApproverComponent(primaryUser);
          if (primaryUser != UserHandle.USER_NULL && receiver != null) {
              cancelReportImpl(listener, receiver, primaryUser);
          }
      }
  
      /**
       * Cancel a pending report request (either because of an explicit call to cancel
       * by the calling app, or because of a binder death).
       */
      private void cancelReportImpl(IIncidentAuthListener listener, ComponentName receiver,
              int primaryUser) {
          // First, remove from our list.
          synchronized (mLock) {
              removePendingReportRecLocked(listener);
          }
          // Second, call back to PermissionController to say it's canceled.
          sendBroadcast(receiver, primaryUser);
      }
  
      /**
       * Send an extra copy of the broadcast, to tell them that the list has changed
       * because of an addition or removal.  This function is less aggressive than
       * authorizeReportImpl in logging about failures, because this is for use in
       * cleanup cases to keep the apps' list in sync with ours.
       */
      private void sendBroadcast() {
          final int primaryUser = getAndValidateUser();
          if (primaryUser == UserHandle.USER_NULL) {
              return;
          }
          final ComponentName receiver = getApproverComponent(primaryUser);
          if (receiver == null) {
              return;
          }
          sendBroadcast(receiver, primaryUser);
      }
  
      /**
       * Send the confirmation broadcast.
       */
      private void sendBroadcast(ComponentName receiver, int primaryUser) {
          final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
          intent.setComponent(receiver);
          final BroadcastOptions options = BroadcastOptions.makeBasic();
          options.setBackgroundActivityStartsAllowed(true);
  
          // Send it to the primary user.
          mContext.sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(primaryUser),
                  android.Manifest.permission.APPROVE_INCIDENT_REPORTS, options.toBundle());
      }
  
      /**
       * Remove a PendingReportRec keyed by uri, and return it.
       */
      private PendingReportRec findAndRemovePendingReportRecLocked(String uriString) {
          final Uri uri = Uri.parse(uriString);
          final int id;
          try {
              final String idStr = uri.getQueryParameter(IncidentManager.URI_PARAM_ID);
              id = Integer.parseInt(idStr);
          } catch (NumberFormatException ex) {
              Log.w(TAG, "Can't parse id from: " + uriString);
              return null;
          }
  
          for (Iterator<PendingReportRec> i = mPending.iterator(); i.hasNext();) {
              final PendingReportRec rec = i.next();
              if (rec.id == id) {
                  i.remove();
                  return rec;
              }
          }
          return null;
      }
  
      /**
       * Remove a PendingReportRec keyed by listener.
       */
      private void removePendingReportRecLocked(IIncidentAuthListener listener) {
  
          for (Iterator<PendingReportRec> i = mPending.iterator(); i.hasNext();) {
              final PendingReportRec rec = i.next();
              if (rec.listener.asBinder() == listener.asBinder()) {
                  Log.i(TAG, "  ...Removed PendingReportRec index=" + i + ": " + rec.getUri());
                  i.remove();
              }
          }
      }
  
      /**
       * Just call listener.deny() (wrapping the RemoteException), without try to
       * add it to the list.
       */
      private void denyReportBeforeAddingRec(IIncidentAuthListener listener, String pkg) {
          try {
              listener.onReportDenied();
          } catch (RemoteException ex) {
              Log.w(TAG, "Failed calling back for denial for " + pkg, ex);
          }
      }
  
      /**
       * Check whether the current user is the primary user, and return the user id if they are.
       * Returns UserHandle.USER_NULL if not valid.
       */
      private int getAndValidateUser() {
          return IncidentCompanionService.getAndValidateUser(mContext);
      }
  
      /**
       * Return the ComponentName of the BroadcastReceiver that will approve reports.
       * The system must have zero or one of these installed.  We only look on the
       * system partition.  When the broadcast happens, the component will also need
       * have the APPROVE_INCIDENT_REPORTS permission.
       */
      private ComponentName getApproverComponent(int userId) {
          // Find the one true BroadcastReceiver
          final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
          final List<ResolveInfo> matches = mPackageManager.queryBroadcastReceiversAsUser(intent,
                  PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
                  | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
          if (matches.size() == 1) {
              return matches.get(0).getComponentInfo().getComponentName();
          } else {
              Log.w(TAG, "Didn't find exactly one BroadcastReceiver to handle "
                      + Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED
                      + ". The report will be denied. size="
                      + matches.size() + ": matches=" + matches);
              return null;
          }
      }
  
      /**
       * Return whether the package is one of the packages installed for the uid.
       */
      private boolean isPackageInUid(int uid, String packageName) {
          try {
              mAppOpsManager.checkPackage(uid, packageName);
              return true;
          } catch (SecurityException ex) {
              return false;
          }
      }
  }
  
  