content: /*
   * Copyright (C) 2016 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;
  
  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.SystemService;
  import android.annotation.TestApi;
  import android.content.Context;
  import android.net.Uri;
  import android.util.Slog;
  
  import java.io.Closeable;
  import java.io.IOException;
  import java.io.InputStream;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.Executor;
  
  /**
   * Class to take an incident report.
   *
   * @hide
   */
  @SystemApi
  @TestApi
  @SystemService(Context.INCIDENT_SERVICE)
  public class IncidentManager {
      private static final String TAG = "IncidentManager";
  
      /**
       * Authority for pending report id urls.
       *
       * @hide
       */
      public static final String URI_SCHEME = "content";
  
      /**
       * Authority for pending report id urls.
       *
       * @hide
       */
      public static final String URI_AUTHORITY = "android.os.IncidentManager";
  
      /**
       * Authority for pending report id urls.
       *
       * @hide
       */
      public static final String URI_PATH = "/pending";
  
      /**
       * Query parameter for the uris for the pending report id.
       *
       * @hide
       */
      public static final String URI_PARAM_ID = "id";
  
      /**
       * Query parameter for the uris for the incident report id.
       *
       * @hide
       */
      public static final String URI_PARAM_REPORT_ID = "r";
  
      /**
       * Query parameter for the uris for the pending report id.
       *
       * @hide
       */
      public static final String URI_PARAM_CALLING_PACKAGE = "pkg";
  
      /**
       * Query parameter for the uris for the pending report id, in wall clock
       * ({@link System.currentTimeMillis()}) timebase.
       *
       * @hide
       */
      public static final String URI_PARAM_TIMESTAMP = "t";
  
      /**
       * Query parameter for the uris for the pending report id.
       *
       * @hide
       */
      public static final String URI_PARAM_FLAGS = "flags";
  
      /**
       * Query parameter for the uris for the pending report id.
       *
       * @hide
       */
      public static final String URI_PARAM_RECEIVER_CLASS = "receiver";
  
      /**
       * Do the confirmation with a dialog instead of the default, which is a notification.
       * It is possible for the dialog to be downgraded to a notification in some cases.
       */
      public static final int FLAG_CONFIRMATION_DIALOG = 0x1;
  
      /**
       * Flag marking fields and incident reports than can be taken
       * off the device only via adb.
       */
      public static final int PRIVACY_POLICY_LOCAL = 0;
  
      /**
       * Flag marking fields and incident reports than can be taken
       * off the device with contemporary consent.
       */
      public static final int PRIVACY_POLICY_EXPLICIT = 100;
  
      /**
       * Flag marking fields and incident reports than can be taken
       * off the device with prior consent.
       */
      public static final int PRIVACY_POLICY_AUTO = 200;
  
      /** @hide */
      @IntDef(flag = false, prefix = { "PRIVACY_POLICY_" }, value = {
              PRIVACY_POLICY_AUTO,
              PRIVACY_POLICY_EXPLICIT,
              PRIVACY_POLICY_LOCAL,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface PrivacyPolicy {}
  
      private final Context mContext;
  
      private Object mLock = new Object();
      private IIncidentManager mIncidentService;
      private IIncidentCompanion mCompanionService;
  
      /**
       * Record for a report that has been taken and is pending user authorization
       * to share it.
       * @hide
       */
      @SystemApi
      @TestApi
      public static class PendingReport {
          /**
           * Encoded data.
           */
          private final Uri mUri;
  
          /**
           * URI_PARAM_FLAGS from the uri
           */
          private final int mFlags;
  
          /**
           * URI_PARAM_CALLING_PACKAGE from the uri
           */
          private final String mRequestingPackage;
  
          /**
           * URI_PARAM_TIMESTAMP from the uri
           */
          private final long mTimestamp;
  
          /**
           * Constructor.
           */
          public PendingReport(@NonNull Uri uri) {
              int flags = 0;
              try {
                  flags = Integer.parseInt(uri.getQueryParameter(URI_PARAM_FLAGS));
              } catch (NumberFormatException ex) {
                  throw new RuntimeException("Invalid URI: No " + URI_PARAM_FLAGS
                          + " parameter. " + uri);
              }
              mFlags = flags;
  
              String requestingPackage = uri.getQueryParameter(URI_PARAM_CALLING_PACKAGE);
              if (requestingPackage == null) {
                  throw new RuntimeException("Invalid URI: No " + URI_PARAM_CALLING_PACKAGE
                          + " parameter. " + uri);
              }
              mRequestingPackage = requestingPackage;
  
              long timestamp = -1;
              try {
                  timestamp = Long.parseLong(uri.getQueryParameter(URI_PARAM_TIMESTAMP));
              } catch (NumberFormatException ex) {
                  throw new RuntimeException("Invalid URI: No " + URI_PARAM_TIMESTAMP
                          + " parameter. " + uri);
              }
              mTimestamp = timestamp;
  
              mUri = uri;
          }
  
          /**
           * Get the package with which this report will be shared.
           */
          public @NonNull String getRequestingPackage() {
              return mRequestingPackage;
          }
  
          /**
           * Get the flags requested for this pending report.
           *
           * @see #FLAG_CONFIRMATION_DIALOG
           */
          public int getFlags() {
              return mFlags;
          }
  
          /**
           * Get the time this pending report was posted.
           */
          public long getTimestamp() {
              return mTimestamp;
          }
  
          /**
           * Get the URI associated with this PendingReport.  It can be used to
           * re-retrieve it from {@link IncidentManager} or set as the data field of
           * an Intent.
           */
          public @NonNull Uri getUri() {
              return mUri;
          }
  
          /**
           * String representation of this PendingReport.
           */
          @Override
          public @NonNull String toString() {
              return "PendingReport(" + getUri().toString() + ")";
          }
  
          /**
           * @inheritDoc
           */
          @Override
          public boolean equals(Object obj) {
              if (this == obj) {
                  return true;
              }
              if (!(obj instanceof PendingReport)) {
                  return false;
              }
              final PendingReport that = (PendingReport) obj;
              return this.mUri.equals(that.mUri)
                      && this.mFlags == that.mFlags
                      && this.mRequestingPackage.equals(that.mRequestingPackage)
                      && this.mTimestamp == that.mTimestamp;
          }
      }
  
      /**
       * Record of an incident report that has previously been taken.
       * @hide
       */
      @SystemApi
      @TestApi
      public static class IncidentReport implements Parcelable, Closeable {
          private final long mTimestampNs;
          private final int mPrivacyPolicy;
          private ParcelFileDescriptor mFileDescriptor;
  
          public IncidentReport(Parcel in) {
              mTimestampNs = in.readLong();
              mPrivacyPolicy = in.readInt();
              if (in.readInt() != 0) {
                  mFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
              } else {
                  mFileDescriptor = null;
              }
          }
  
          /**
           * Close the input stream associated with this entry.
           */
          public void close() {
              try {
                  if (mFileDescriptor != null) {
                      mFileDescriptor.close();
                      mFileDescriptor = null;
                  }
              } catch (IOException e) {
              }
          }
  
          /**
           * Get the time at which this incident report was taken, in wall clock time
           * ({@link System#currenttimeMillis System.currenttimeMillis()} time base).
           */
          public long getTimestamp() {
              return mTimestampNs / 1000000;
          }
  
          /**
           * Get the privacy level to which this report has been filtered.
           *
           * @see #PRIVACY_POLICY_AUTO
           * @see #PRIVACY_POLICY_EXPLICIT
           * @see #PRIVACY_POLICY_LOCAL
           */
          public long getPrivacyPolicy() {
              return mPrivacyPolicy;
          }
  
          /**
           * Get the contents of this incident report.
           */
          public InputStream getInputStream() throws IOException {
              if (mFileDescriptor == null) {
                  return null;
              }
              return new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor);
          }
  
          /**
           * @inheritDoc
           */
          public int describeContents() {
              return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
          }
  
          /**
           * @inheritDoc
           */
          public void writeToParcel(Parcel out, int flags) {
              out.writeLong(mTimestampNs);
              out.writeInt(mPrivacyPolicy);
              if (mFileDescriptor != null) {
                  out.writeInt(1);
                  mFileDescriptor.writeToParcel(out, flags);
              } else {
                  out.writeInt(0);
              }
          }
  
          /**
           * {@link Parcelable.Creator Creator} for {@link IncidentReport}.
           */
          public static final @android.annotation.NonNull Parcelable.Creator<IncidentReport> CREATOR = new Parcelable.Creator() {
              /**
               * @inheritDoc
               */
              public IncidentReport[] newArray(int size) {
                  return new IncidentReport[size];
              }
  
              /**
               * @inheritDoc
               */
              public IncidentReport createFromParcel(Parcel in) {
                  return new IncidentReport(in);
              }
          };
      }
  
      /**
       * Listener for the status of an incident report being authorized or denied.
       *
       * @see #requestAuthorization
       * @see #cancelAuthorization
       */
      public static class AuthListener {
          Executor mExecutor;
  
          IIncidentAuthListener.Stub mBinder = new IIncidentAuthListener.Stub() {
              @Override
              public void onReportApproved() {
                  if (mExecutor != null) {
                      mExecutor.execute(() -> {
                          AuthListener.this.onReportApproved();
                      });
                  } else {
                      AuthListener.this.onReportApproved();
                  }
              }
  
              @Override
              public void onReportDenied() {
                  if (mExecutor != null) {
                      mExecutor.execute(() -> {
                          AuthListener.this.onReportDenied();
                      });
                  } else {
                      AuthListener.this.onReportDenied();
                  }
              }
          };
  
          /**
           * Called when a report is approved.
           */
          public void onReportApproved() {
          }
  
          /**
           * Called when a report is denied.
           */
          public void onReportDenied() {
          }
      }
  
      /**
       * @hide
       */
      public IncidentManager(Context context) {
          mContext = context;
      }
  
      /**
       * Take an incident report.
       */
      @RequiresPermission(allOf = {
              android.Manifest.permission.DUMP,
              android.Manifest.permission.PACKAGE_USAGE_STATS
      })
      public void reportIncident(IncidentReportArgs args) {
          reportIncidentInternal(args);
      }
  
      /**
       * Request authorization of an incident report.
       */
      @RequiresPermission(android.Manifest.permission.REQUEST_INCIDENT_REPORT_APPROVAL)
      public void requestAuthorization(int callingUid, String callingPackage, int flags,
              AuthListener listener) {
          requestAuthorization(callingUid, callingPackage, flags,
                  mContext.getMainExecutor(), listener);
      }
  
      /**
       * Request authorization of an incident report.
       * @hide
       */
      @RequiresPermission(android.Manifest.permission.REQUEST_INCIDENT_REPORT_APPROVAL)
      public void requestAuthorization(int callingUid, @NonNull String callingPackage, int flags,
               @NonNull @CallbackExecutor Executor executor, @NonNull AuthListener listener) {
          try {
              if (listener.mExecutor != null) {
                  throw new RuntimeException("Do not reuse AuthListener objects when calling"
                          + " requestAuthorization");
              }
              listener.mExecutor = executor;
              getCompanionServiceLocked().authorizeReport(callingUid, callingPackage, null, null,
                      flags, listener.mBinder);
          } catch (RemoteException ex) {
              // System process going down
              throw new RuntimeException(ex);
          }
      }
  
      /**
       * Cancel a previous request for incident report authorization.
       */
      @RequiresPermission(android.Manifest.permission.REQUEST_INCIDENT_REPORT_APPROVAL)
      public void cancelAuthorization(AuthListener listener) {
          try {
              getCompanionServiceLocked().cancelAuthorization(listener.mBinder);
          } catch (RemoteException ex) {
              // System process going down
              throw new RuntimeException(ex);
          }
      }
  
      /**
       * Get incident (and bug) reports that are pending approval to share.
       */
      @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS)
      public List<PendingReport> getPendingReports() {
          List<String> strings;
          try {
              strings = getCompanionServiceLocked().getPendingReports();
          } catch (RemoteException ex) {
              throw new RuntimeException(ex);
          }
          final int size = strings.size();
          ArrayList<PendingReport> result = new ArrayList(size);
          for (int i = 0; i < size; i++) {
              result.add(new PendingReport(Uri.parse(strings.get(i))));
          }
          return result;
      }
  
      /**
       * Allow this report to be shared with the given app.
       */
      @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS)
      public void approveReport(Uri uri) {
          try {
              getCompanionServiceLocked().approveReport(uri.toString());
          } catch (RemoteException ex) {
              // System process going down
              throw new RuntimeException(ex);
          }
      }
  
      /**
       * Do not allow this report to be shared with the given app.
       */
      @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS)
      public void denyReport(Uri uri) {
          try {
              getCompanionServiceLocked().denyReport(uri.toString());
          } catch (RemoteException ex) {
              // System process going down
              throw new RuntimeException(ex);
          }
      }
  
      /**
       * Get the incident reports that are available for upload for the supplied
       * broadcast recevier.
       *
       * @param receiverClass Class name of broadcast receiver in this package that
       *   was registered to retrieve reports.
       *
       * @return A list of {@link Uri Uris} that are awaiting upload.
       */
      @RequiresPermission(allOf = {
              android.Manifest.permission.DUMP,
              android.Manifest.permission.PACKAGE_USAGE_STATS
      })
      public @NonNull List<Uri> getIncidentReportList(String receiverClass) {
          List<String> strings;
          try {
              strings = getCompanionServiceLocked().getIncidentReportList(
                      mContext.getPackageName(), receiverClass);
          } catch (RemoteException ex) {
              throw new RuntimeException("System server or incidentd going down", ex);
          }
          final int size = strings.size();
          ArrayList<Uri> result = new ArrayList(size);
          for (int i = 0; i < size; i++) {
              result.add(Uri.parse(strings.get(i)));
          }
          return result;
      }
  
      /**
       * Get the incident report with the given URI id.
       *
       * @param uri Identifier of the incident report.
       *
       * @return an IncidentReport object, or null if the incident report has been
       *  expired from disk.
       */
      @RequiresPermission(allOf = {
              android.Manifest.permission.DUMP,
              android.Manifest.permission.PACKAGE_USAGE_STATS
      })
      public @Nullable IncidentReport getIncidentReport(Uri uri) {
          final String id = uri.getQueryParameter(URI_PARAM_REPORT_ID);
          if (id == null) {
              // If there's no report id, it's a bug report, so we can't return the incident
              // report.
              return null;
          }
  
          final String pkg = uri.getQueryParameter(URI_PARAM_CALLING_PACKAGE);
          if (pkg == null) {
              throw new RuntimeException("Invalid URI: No "
                      + URI_PARAM_CALLING_PACKAGE + " parameter. " + uri);
          }
  
          final String cls = uri.getQueryParameter(URI_PARAM_RECEIVER_CLASS);
          if (cls == null) {
              throw new RuntimeException("Invalid URI: No "
                      + URI_PARAM_RECEIVER_CLASS + " parameter. " + uri);
          }
  
          try {
              return getCompanionServiceLocked().getIncidentReport(pkg, cls, id);
          } catch (RemoteException ex) {
              throw new RuntimeException("System server or incidentd going down", ex);
          }
      }
  
      /**
       * Delete the incident report with the given URI id.
       *
       * @param uri Identifier of the incident report. Pass null to delete all
       *              incident reports owned by this application.
       */
      @RequiresPermission(allOf = {
              android.Manifest.permission.DUMP,
              android.Manifest.permission.PACKAGE_USAGE_STATS
      })
      public void deleteIncidentReports(Uri uri) {
          if (uri == null) {
              try {
                  getCompanionServiceLocked().deleteAllIncidentReports(mContext.getPackageName());
              } catch (RemoteException ex) {
                  throw new RuntimeException("System server or incidentd going down", ex);
              }
          } else {
              final String pkg = uri.getQueryParameter(URI_PARAM_CALLING_PACKAGE);
              if (pkg == null) {
                  throw new RuntimeException("Invalid URI: No "
                          + URI_PARAM_CALLING_PACKAGE + " parameter. " + uri);
              }
  
              final String cls = uri.getQueryParameter(URI_PARAM_RECEIVER_CLASS);
              if (cls == null) {
                  throw new RuntimeException("Invalid URI: No "
                          + URI_PARAM_RECEIVER_CLASS + " parameter. " + uri);
              }
  
              final String id = uri.getQueryParameter(URI_PARAM_REPORT_ID);
              if (id == null) {
                  throw new RuntimeException("Invalid URI: No "
                          + URI_PARAM_REPORT_ID + " parameter. " + uri);
              }
          
              try {
                  getCompanionServiceLocked().deleteIncidentReports(pkg, cls, id);
              } catch (RemoteException ex) {
                  throw new RuntimeException("System server or incidentd going down", ex);
              }
          }
      }
  
      private void reportIncidentInternal(IncidentReportArgs args) {
          try {
              final IIncidentManager service = getIIncidentManagerLocked();
              if (service == null) {
                  Slog.e(TAG, "reportIncident can't find incident binder service");
                  return;
              }
              service.reportIncident(args);
          } catch (RemoteException ex) {
              Slog.e(TAG, "reportIncident failed", ex);
          }
      }
  
      private IIncidentManager getIIncidentManagerLocked() throws RemoteException {
          if (mIncidentService != null) {
              return mIncidentService;
          }
  
          synchronized (mLock) {
              if (mIncidentService != null) {
                  return mIncidentService;
              }
              mIncidentService = IIncidentManager.Stub.asInterface(
                  ServiceManager.getService(Context.INCIDENT_SERVICE));
              if (mIncidentService != null) {
                  mIncidentService.asBinder().linkToDeath(() -> {
                      synchronized (mLock) {
                          mIncidentService = null;
                      }
                  }, 0);
              }
              return mIncidentService;
          }
      }
  
      private IIncidentCompanion getCompanionServiceLocked() throws RemoteException {
          if (mCompanionService != null) {
              return mCompanionService;
          }
  
          synchronized (this) {
              if (mCompanionService != null) {
                  return mCompanionService;
              }
              mCompanionService = IIncidentCompanion.Stub.asInterface(
                  ServiceManager.getService(Context.INCIDENT_COMPANION_SERVICE));
              if (mCompanionService != null) {
                  mCompanionService.asBinder().linkToDeath(() -> {
                      synchronized (mLock) {
                          mCompanionService = null;
                      }
                  }, 0);
              }
              return mCompanionService;
          }
      }
  }
  
  