summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp2
-rw-r--r--api/system-current.txt21
-rw-r--r--api/test-current.txt20
-rw-r--r--core/java/android/content/Context.java8
-rw-r--r--core/java/android/content/Intent.java12
-rw-r--r--core/java/android/os/IncidentManager.java317
-rw-r--r--core/res/AndroidManifest.xml5
-rw-r--r--data/etc/platform.xml1
-rw-r--r--data/etc/privapp-permissions-platform.xml2
-rw-r--r--services/core/java/com/android/server/incident/IncidentCompanionService.java545
-rw-r--r--services/core/java/com/android/server/incident/RequestQueue.java155
-rw-r--r--services/java/com/android/server/SystemServer.java6
12 files changed, 1078 insertions, 16 deletions
diff --git a/Android.bp b/Android.bp
index 7ce8b31e42c2..6c5473a6864a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -656,6 +656,7 @@ java_defaults {
":vold_aidl",
":installd_aidl",
":dumpstate_aidl",
+ ":incidentcompanion_aidl",
"lowpan/java/android/net/lowpan/ILowpanEnergyScanCallback.aidl",
"lowpan/java/android/net/lowpan/ILowpanNetScanCallback.aidl",
@@ -706,6 +707,7 @@ java_defaults {
"system/update_engine/binder_bindings",
"frameworks/native/aidl/binder",
"frameworks/native/cmds/dumpstate/binder",
+ "frameworks/native/libs/incidentcompanion/binder",
"frameworks/av/camera/aidl",
"frameworks/av/media/libaudioclient/aidl",
"frameworks/native/aidl/gui",
diff --git a/api/system-current.txt b/api/system-current.txt
index 7214eb7112ab..ca1dee39b497 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1307,6 +1307,7 @@ package android.content {
field public static final String ACTION_MANAGE_PERMISSION_APPS = "android.intent.action.MANAGE_PERMISSION_APPS";
field @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public static final String ACTION_MANAGE_SPECIAL_APP_ACCESSES = "android.intent.action.MANAGE_SPECIAL_APP_ACCESSES";
field public static final String ACTION_MASTER_CLEAR_NOTIFICATION = "android.intent.action.MASTER_CLEAR_NOTIFICATION";
+ field public static final String ACTION_PENDING_INCIDENT_REPORTS_CHANGED = "android.intent.action.PENDING_INCIDENT_REPORTS_CHANGED";
field public static final String ACTION_PRE_BOOT_COMPLETED = "android.intent.action.PRE_BOOT_COMPLETED";
field public static final String ACTION_QUERY_PACKAGE_RESTART = "android.intent.action.QUERY_PACKAGE_RESTART";
field public static final String ACTION_RESOLVE_INSTANT_APP_PACKAGE = "android.intent.action.RESOLVE_INSTANT_APP_PACKAGE";
@@ -5333,7 +5334,27 @@ package android.os {
}
public class IncidentManager {
+ method @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS) public void approveReport(android.net.Uri);
+ method @RequiresPermission("android.permission.REQUEST_INCIDENT_REPORT_APPROVAL") public void cancelAuthorization(android.os.IncidentManager.AuthListener);
+ method @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS) public void denyReport(android.net.Uri);
+ method @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS) public java.util.List<android.os.IncidentManager.PendingReport> getPendingReports();
method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void reportIncident(android.os.IncidentReportArgs);
+ method @RequiresPermission("android.permission.REQUEST_INCIDENT_REPORT_APPROVAL") public void requestAuthorization(int, String, int, android.os.IncidentManager.AuthListener);
+ field public static final int FLAG_CONFIRMATION_DIALOG = 1; // 0x1
+ }
+
+ public static class IncidentManager.AuthListener {
+ ctor public IncidentManager.AuthListener();
+ method public void onReportApproved();
+ method public void onReportDenied();
+ }
+
+ public static class IncidentManager.PendingReport {
+ ctor public IncidentManager.PendingReport(@NonNull android.net.Uri);
+ method public int getFlags();
+ method @NonNull public String getRequestingPackage();
+ method public long getTimestamp();
+ method @NonNull public android.net.Uri getUri();
}
public final class IncidentReportArgs implements android.os.Parcelable {
diff --git a/api/test-current.txt b/api/test-current.txt
index 2d654846aef0..4ca3f568243c 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1282,7 +1282,27 @@ package android.os {
}
public class IncidentManager {
+ method @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS) public void approveReport(android.net.Uri);
+ method @RequiresPermission("android.permission.REQUEST_INCIDENT_REPORT_APPROVAL") public void cancelAuthorization(android.os.IncidentManager.AuthListener);
+ method @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS) public void denyReport(android.net.Uri);
+ method @RequiresPermission(android.Manifest.permission.APPROVE_INCIDENT_REPORTS) public java.util.List<android.os.IncidentManager.PendingReport> getPendingReports();
method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void reportIncident(android.os.IncidentReportArgs);
+ method @RequiresPermission("android.permission.REQUEST_INCIDENT_REPORT_APPROVAL") public void requestAuthorization(int, String, int, android.os.IncidentManager.AuthListener);
+ field public static final int FLAG_CONFIRMATION_DIALOG = 1; // 0x1
+ }
+
+ public static class IncidentManager.AuthListener {
+ ctor public IncidentManager.AuthListener();
+ method public void onReportApproved();
+ method public void onReportDenied();
+ }
+
+ public static class IncidentManager.PendingReport {
+ ctor public IncidentManager.PendingReport(@NonNull android.net.Uri);
+ method public int getFlags();
+ method @NonNull public String getRequestingPackage();
+ method public long getTimestamp();
+ method @NonNull public android.net.Uri getUri();
}
public final class IncidentReportArgs implements android.os.Parcelable {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 87f9e464cdc2..d9d0ee98e5ac 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3180,6 +3180,7 @@ public abstract class Context {
//@hide: CONTEXTHUB_SERVICE,
SYSTEM_HEALTH_SERVICE,
//@hide: INCIDENT_SERVICE,
+ //@hide: INCIDENT_COMPANION_SERVICE,
//@hide: STATS_COMPANION_SERVICE,
COMPANION_DEVICE_SERVICE,
CROSS_PROFILE_APPS_SERVICE,
@@ -4466,6 +4467,13 @@ public abstract class Context {
public static final String INCIDENT_SERVICE = "incident";
/**
+ * Service to assist incidentd and dumpstated in reporting status to the user
+ * and in confirming authorization to take an incident report or bugreport
+ * @hide
+ */
+ public static final String INCIDENT_COMPANION_SERVICE = "incidentcompanion";
+
+ /**
* Service to assist statsd in obtaining general stats.
* @hide
*/
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 22f73dbd4644..8d14091478c3 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -1475,6 +1475,18 @@ public class Intent implements Parcelable, Cloneable {
public static final String ACTION_APP_ERROR = "android.intent.action.APP_ERROR";
/**
+ * An incident or bug report has been taken, and a system app has requested it to be shared,
+ * so trigger the confirmation screen.
+ *
+ * This will be sent directly to the registered receiver with the
+ * android.permission.APPROVE_INCIDENT_REPORTS permission.
+ * @hide
+ */
+ @SystemApi
+ public static final String ACTION_PENDING_INCIDENT_REPORTS_CHANGED =
+ "android.intent.action.PENDING_INCIDENT_REPORTS_CHANGED";
+
+ /**
* Activity Action: Show power usage information to the user.
* <p>Input: Nothing.
* <p>Output: Nothing.
diff --git a/core/java/android/os/IncidentManager.java b/core/java/android/os/IncidentManager.java
index 0e6652d471a5..88a578a5b6de 100644
--- a/core/java/android/os/IncidentManager.java
+++ b/core/java/android/os/IncidentManager.java
@@ -16,13 +16,18 @@
package android.os;
+import android.annotation.NonNull;
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.util.ArrayList;
+import java.util.List;
+
/**
* Class to take an incident report.
*
@@ -34,9 +39,200 @@ import android.util.Slog;
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 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";
+
+ /**
+ * 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;
+
private final Context mContext;
- private IIncidentManager mService;
+ 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() + ")";
+ }
+ }
+
+ /**
+ * Listener for the status of an incident report being authroized or denied.
+ *
+ * @see #requestAuthorization
+ * @see #cancelAuthorization
+ */
+ public static class AuthListener {
+ IIncidentAuthListener.Stub mBinder = new IIncidentAuthListener.Stub() {
+ @Override
+ public void onReportApproved() {
+ AuthListener.this.onReportApproved();
+ }
+
+ @Override
+ public void onReportDenied() {
+ AuthListener.this.onReportDenied();
+ }
+ };
+
+ /**
+ * Called when a report is approved.
+ */
+ public void onReportApproved() {
+ }
+
+ /**
+ * Called when a report is denied.
+ */
+ public void onReportDenied() {
+ }
+ }
/**
* @hide
@@ -56,12 +252,76 @@ public class IncidentManager {
reportIncidentInternal(args);
}
- private class IncidentdDeathRecipient implements IBinder.DeathRecipient {
- @Override
- public void binderDied() {
- synchronized (this) {
- mService = null;
- }
+ /**
+ * 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) {
+ try {
+ getCompanionServiceLocked().authorizeReport(callingUid, callingPackage, 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);
}
}
@@ -79,22 +339,47 @@ public class IncidentManager {
}
private IIncidentManager getIIncidentManagerLocked() throws RemoteException {
- if (mService != null) {
- return mService;
+ if (mIncidentService != null) {
+ return mIncidentService;
}
- synchronized (this) {
- if (mService != null) {
- return mService;
+ synchronized (mLock) {
+ if (mIncidentService != null) {
+ return mIncidentService;
}
- mService = IIncidentManager.Stub.asInterface(
+ mIncidentService = IIncidentManager.Stub.asInterface(
ServiceManager.getService(Context.INCIDENT_SERVICE));
- if (mService != null) {
- mService.asBinder().linkToDeath(new IncidentdDeathRecipient(), 0);
+ if (mIncidentService != null) {
+ mIncidentService.asBinder().linkToDeath(() -> {
+ synchronized (mLock) {
+ mIncidentService = null;
+ }
+ }, 0);
}
- return mService;
+ 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;
+ }
+ }
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 14143d857a58..1a402c064b0e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2808,6 +2808,11 @@
<permission android:name="android.permission.APPROVE_INCIDENT_REPORTS"
android:protectionLevel="signature|incidentReportApprover" />
+ <!-- @hide Allow an application to approve an incident or bug report approval from
+ the system. -->
+ <permission android:name="android.permission.REQUEST_INCIDENT_REPORT_APPROVAL"
+ android:protectionLevel="signature|privileged" />
+
<!-- ==================================== -->
<!-- Private permissions -->
<!-- ==================================== -->
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index fb43e41010b4..44d71e269a95 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -169,6 +169,7 @@
<assign-permission name="android.permission.DUMP" uid="incidentd" />
<assign-permission name="android.permission.PACKAGE_USAGE_STATS" uid="incidentd" />
<assign-permission name="android.permission.INTERACT_ACROSS_USERS" uid="incidentd" />
+ <assign-permission name="android.permission.REQUEST_INCIDENT_REPORT_APPROVAL" uid="incidentd" />
<assign-permission name="android.permission.ACCESS_LOWPAN_STATE" uid="lowpan" />
<assign-permission name="android.permission.MANAGE_LOWPAN_INTERFACES" uid="lowpan" />
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 3331f3a88fc2..a47ab875804f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -141,6 +141,7 @@ applications that come with the platform
<permission name="android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS"/>
<permission name="android.permission.GET_APP_OPS_STATS"/>
<permission name="android.permission.UPDATE_APP_OPS_STATS"/>
+ <permission name="android.permission.REQUEST_INCIDENT_REPORT_APPROVAL"/>
<permission name="android.permission.APPROVE_INCIDENT_REPORTS"/>
</privapp-permissions>
@@ -328,6 +329,7 @@ applications that come with the platform
<permission name="android.permission.USE_RESERVED_DISK"/>
<permission name="android.permission.WRITE_MEDIA_STORAGE"/>
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <permission name="android.permission.REQUEST_INCIDENT_REPORT_APPROVAL"/>
</privapp-permissions>
<privapp-permissions package="com.android.statementservice">
diff --git a/services/core/java/com/android/server/incident/IncidentCompanionService.java b/services/core/java/com/android/server/incident/IncidentCompanionService.java
new file mode 100644
index 000000000000..3ebba0074a1c
--- /dev/null
+++ b/services/core/java/com/android/server/incident/IncidentCompanionService.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2018 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.ActivityManager;
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IIncidentAuthListener;
+import android.os.IIncidentCompanion;
+import android.os.IncidentManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.internal.util.DumpUtils;
+import com.android.server.SystemService;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+// TODO: User changes should deny everything that's pending.
+
+/**
+ * Helper service for incidentd and dumpstated to provide user feedback
+ * and authorization for bug and inicdent reports to be taken.
+ */
+public class IncidentCompanionService extends SystemService {
+ static final String TAG = "IncidentCompanionService";
+
+ private final Handler mHandler = new Handler();
+ private final RequestQueue mRequestQueue = new RequestQueue(mHandler);
+ 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;
+
+ /**
+ * Construct a PendingReportRec, with an auto-incremented id.
+ */
+ PendingReportRec(String callingPackage, int flags, IIncidentAuthListener listener) {
+ this.id = mNextPendingId++;
+ this.callingPackage = callingPackage;
+ this.flags = flags;
+ this.listener = listener;
+ this.addedRealtime = SystemClock.elapsedRealtime();
+ this.addedWalltime = System.currentTimeMillis();
+ }
+
+ /**
+ * Get the Uri that contains the flattened data.
+ */
+ Uri getUri() {
+ return (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))
+ .build();
+ }
+ }
+
+ /**
+ * Implementation of the IIncidentCompanion binder interface.
+ */
+ private final class BinderService extends IIncidentCompanion.Stub {
+ /**
+ * ONEWAY binder call to initiate authorizing the report. The actual logic is posted
+ * to mRequestQueue, and may happen later. The security checks need to happen here.
+ */
+ @Override
+ public void authorizeReport(int callingUid, final String callingPackage, final int flags,
+ final IIncidentAuthListener listener) {
+ enforceRequestAuthorizationPermission();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ // 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, flags, listener);
+ });
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public void cancelAuthorization(final IIncidentAuthListener listener) {
+ enforceRequestAuthorizationPermission();
+
+ // Caller can cancel if they don't want it anymore, and mRequestQueue elides
+ // authorize/cancel pairs.
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mRequestQueue.enqueue(listener.asBinder(), false, () -> {
+ cancelReportImpl(listener);
+ });
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * SYNCHRONOUS binder call to get the list of reports that are pending confirmation
+ * by the user.
+ */
+ @Override
+ public List<String> getPendingReports() {
+ enforceAuthorizePermission();
+
+ 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;
+ }
+ }
+
+ /**
+ * ONEWAY binder call to mark a report as approved.
+ */
+ @Override
+ public void approveReport(String uri) {
+ enforceAuthorizePermission();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ 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);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * ONEWAY binder call to mark a report as NOT approved.
+ */
+ @Override
+ public void denyReport(String uri) {
+ enforceAuthorizePermission();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ 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);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * Implementation of adb shell dumpsys debugreportcompanion.
+ */
+ @Override
+ protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
+ if (!DumpUtils.checkDumpPermission(getContext(), TAG, writer)) {
+ return;
+ }
+ 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()));
+ }
+ }
+ }
+ }
+
+ private void enforceRequestAuthorizationPermission() {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.REQUEST_INCIDENT_REPORT_APPROVAL, null);
+ }
+
+ private void enforceAuthorizePermission() {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.APPROVE_INCIDENT_REPORTS, null);
+ }
+
+ }
+
+ /**
+ * Construct new IncidentCompanionService with the context.
+ */
+ public IncidentCompanionService(Context context) {
+ super(context);
+ mPackageManager = context.getPackageManager();
+ mAppOpsManager = context.getSystemService(AppOpsManager.class);
+ }
+
+ /**
+ * Initialize the service. It is still not safe to do UI until
+ * onBootPhase(SystemService.PHASE_BOOT_COMPLETED).
+ */
+ @Override
+ public void onStart() {
+ publishBinderService(Context.INCIDENT_COMPANION_SERVICE, new BinderService());
+ }
+
+ /**
+ * Handle the boot process... Starts everything running once the system is
+ * up enough for us to do UI.
+ */
+ @Override
+ public void onBootPhase(int phase) {
+ super.onBootPhase(phase);
+ switch (phase) {
+ case SystemService.PHASE_BOOT_COMPLETED:
+ // Release the enqueued work.
+ mRequestQueue.start();
+ break;
+ }
+ }
+
+ /**
+ * Start the confirmation process.
+ */
+ private void authorizeReportImpl(int callingUid, final String callingPackage, int flags,
+ final IIncidentAuthListener listener) {
+ // Enforce that the calling package pertains to the callingUid.
+ if (!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, 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);
+
+ // Send it to the primary user.
+ getContext().sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(primaryUser),
+ android.Manifest.permission.APPROVE_INCIDENT_REPORTS);
+ }
+
+ /**
+ * 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;
+ }
+ final int size = mPending.size();
+ for (int i = 0; i < size; i++) {
+ final PendingReportRec rec = mPending.get(i);
+ if (rec.id == id) {
+ mPending.remove(i);
+ return rec;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Remove a PendingReportRec keyed by listener.
+ */
+ private void removePendingReportRecLocked(IIncidentAuthListener listener) {
+ final int size = mPending.size();
+ for (int i = 0; i < size; i++) {
+ final PendingReportRec rec = mPending.get(i);
+ if (rec.listener.asBinder() == listener.asBinder()) {
+ Log.i(TAG, " ...Removed PendingReportRec index=" + i + ": " + rec.getUri());
+ mPending.remove(i);
+ }
+ }
+ }
+
+ /**
+ * 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() {
+ // Current user
+ UserInfo currentUser;
+ try {
+ currentUser = ActivityManager.getService().getCurrentUser();
+ } catch (RemoteException ex) {
+ // We're already inside the system process.
+ throw new RuntimeException(ex);
+ }
+
+ // Primary user
+ final UserManager um = UserManager.get(getContext());
+ final UserInfo primaryUser = um.getPrimaryUser();
+
+ // Check that we're using the right user.
+ if (currentUser == null) {
+ Log.w(TAG, "No current user. Nobody to approve the report."
+ + " The report will be denied.");
+ return UserHandle.USER_NULL;
+ }
+ if (primaryUser == null) {
+ Log.w(TAG, "No primary user. Nobody to approve the report."
+ + " The report will be denied.");
+ return UserHandle.USER_NULL;
+ }
+ if (primaryUser.id != currentUser.id) {
+ Log.w(TAG, "Only the primary user can approve bugreports, but they are not"
+ + " the current user. The report will be denied.");
+ return UserHandle.USER_NULL;
+ }
+
+ return primaryUser.id;
+ }
+
+ /**
+ * 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;
+ }
+ }
+}
+
diff --git a/services/core/java/com/android/server/incident/RequestQueue.java b/services/core/java/com/android/server/incident/RequestQueue.java
new file mode 100644
index 000000000000..85758e2efe38
--- /dev/null
+++ b/services/core/java/com/android/server/incident/RequestQueue.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2018 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.os.Handler;
+import android.os.IBinder;
+
+import java.util.ArrayList;
+
+/**
+ * Class to enqueue work until the system is ready.
+ */
+class RequestQueue {
+ /*
+ * All fields are protected by synchronized (mPending)
+ */
+
+ /**
+ * Requests that we can't start yet because system server isn't booted enough yet.
+ * Set to null when we have started.
+ */
+ private ArrayList<Rec> mPending = new ArrayList();
+
+ /**
+ * Where to run the requests.
+ */
+ private final Handler mHandler;
+
+ /**
+ * Whether someone has called start() yet.
+ */
+ private boolean mStarted;
+
+ /**
+ * Queue item.
+ */
+ private class Rec {
+ /**
+ * Key for the record.
+ */
+ public final IBinder key;
+
+ /**
+ * True / false pairs will be elided by enqueue().
+ */
+ public final boolean value;
+
+ /**
+ * The runnable to run.
+ */
+ public final Runnable runnable;
+
+ /**
+ * Constructor
+ */
+ Rec(IBinder key, boolean value, Runnable runnable) {
+ this.key = key;
+ this.value = value;
+ this.runnable = runnable;
+ }
+ }
+
+ /**
+ * Handler on the main thread.
+ */
+ private final Runnable mWorker = new Runnable() {
+ @Override
+ public void run() {
+ ArrayList<Rec> copy = null;
+ synchronized (mPending) {
+ if (mPending.size() > 0) {
+ copy = new ArrayList<Rec>(mPending);
+ mPending.clear();
+ }
+ }
+ if (copy != null) {
+ final int size = copy.size();
+ for (int i = 0; i < size; i++) {
+ copy.get(i).runnable.run();
+ }
+ }
+ }
+ };
+
+ /**
+ * Construct RequestQueue.
+ *
+ * @param handler Handler to use.
+ */
+ RequestQueue(Handler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * We're now ready to go. Start any previously pending runnables.
+ */
+ public void start() {
+ synchronized (mPending) {
+ if (!mStarted) {
+ if (mPending.size() > 0) {
+ mHandler.post(mWorker);
+ }
+ mStarted = true;
+ }
+ }
+ }
+
+ /**
+ * If we can run this now, then do it on the Handler provided in the constructor.
+ * If not, then enqueue it until start is called.
+ *
+ * The queue will elide keys with pairs of true/false values, so the user doesn't
+ * see confirmations that were previously canceled.
+ */
+ public void enqueue(IBinder key, boolean value, Runnable runnable) {
+ synchronized (mPending) {
+ boolean skip = false;
+ if (!value) {
+ for (int i = mPending.size() - 1; i >= 0; i--) {
+ final Rec r = mPending.get(i);
+ if (r.key == key) {
+ if (r.value) {
+ skip = true;
+ mPending.remove(i);
+ break;
+ }
+ }
+ }
+ }
+ if (!skip) {
+ mPending.add(new Rec(key, value, runnable));
+ }
+ if (mStarted) {
+ // Already started. Post now.
+ mHandler.post(mWorker);
+ }
+ }
+ }
+}
+
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 586136802619..793818ca38a4 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -91,6 +91,7 @@ import com.android.server.display.DisplayManagerService;
import com.android.server.dreams.DreamManagerService;
import com.android.server.emergency.EmergencyAffordanceService;
import com.android.server.hdmi.HdmiControlService;
+import com.android.server.incident.IncidentCompanionService;
import com.android.server.input.InputManagerService;
import com.android.server.inputmethod.InputMethodManagerService;
import com.android.server.inputmethod.MultiClientInputMethodManagerService;
@@ -1856,6 +1857,11 @@ public final class SystemServer {
mSystemServiceManager.startService(StatsCompanionService.Lifecycle.class);
traceEnd();
+ // Incidentd and dumpstated helper
+ traceBeginAndSlog("StartIncidentCompanionService");
+ mSystemServiceManager.startService(IncidentCompanionService.class);
+ traceEnd();
+
if (safeMode) {
traceBeginAndSlog("EnterSafeModeAndDisableJitCompilation");
mActivityManagerService.enterSafeMode();