diff options
| -rw-r--r-- | Android.bp | 2 | ||||
| -rw-r--r-- | api/system-current.txt | 21 | ||||
| -rw-r--r-- | api/test-current.txt | 20 | ||||
| -rw-r--r-- | core/java/android/content/Context.java | 8 | ||||
| -rw-r--r-- | core/java/android/content/Intent.java | 12 | ||||
| -rw-r--r-- | core/java/android/os/IncidentManager.java | 317 | ||||
| -rw-r--r-- | core/res/AndroidManifest.xml | 5 | ||||
| -rw-r--r-- | data/etc/platform.xml | 1 | ||||
| -rw-r--r-- | data/etc/privapp-permissions-platform.xml | 2 | ||||
| -rw-r--r-- | services/core/java/com/android/server/incident/IncidentCompanionService.java | 545 | ||||
| -rw-r--r-- | services/core/java/com/android/server/incident/RequestQueue.java | 155 | ||||
| -rw-r--r-- | services/java/com/android/server/SystemServer.java | 6 |
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(); |