ronnie
2022-10-14 1504bb53e29d3d46222c0b3ea994fc494b48e153
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package com.android.server.incident;
 
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.IIncidentAuthListener;
import android.os.IncidentManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
 
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
 
// TODO: User changes should deny everything that's pending.
 
/**
 * Tracker for reports pending approval.
 */
class PendingReports {
    static final String TAG = IncidentCompanionService.TAG;
 
    private final Handler mHandler = new Handler();
    private final RequestQueue mRequestQueue = new RequestQueue(mHandler);
    private final Context mContext;
    private final PackageManager mPackageManager;
    private final AppOpsManager mAppOpsManager;
 
    //
    // All fields below must be protected by mLock
    //
    private final Object mLock = new Object();
    private final ArrayList<PendingReportRec> mPending = new ArrayList();
 
    /**
     * The next ID we'll use when we make a PendingReportRec.
     */
    private int mNextPendingId = 1;
 
    /**
     * One for each authorization that's pending.
     */
    private final class PendingReportRec {
        public int id;
        public String callingPackage;
        public int flags;
        public IIncidentAuthListener listener;
        public long addedRealtime;
        public long addedWalltime;
        public String receiverClass;
        public String reportId;
 
        /**
         * Construct a PendingReportRec, with an auto-incremented id.
         */
        PendingReportRec(String callingPackage, String receiverClass, String reportId, int flags,
                IIncidentAuthListener listener) {
            this.id = mNextPendingId++;
            this.callingPackage = callingPackage;
            this.flags = flags;
            this.listener = listener;
            this.addedRealtime = SystemClock.elapsedRealtime();
            this.addedWalltime = System.currentTimeMillis();
            this.receiverClass = receiverClass;
            this.reportId = reportId;
        }
 
        /**
         * Get the Uri that contains the flattened data.
         */
        Uri getUri() {
            final Uri.Builder builder = (new Uri.Builder())
                    .scheme(IncidentManager.URI_SCHEME)
                    .authority(IncidentManager.URI_AUTHORITY)
                    .path(IncidentManager.URI_PATH)
                    .appendQueryParameter(IncidentManager.URI_PARAM_ID, Integer.toString(id))
                    .appendQueryParameter(IncidentManager.URI_PARAM_CALLING_PACKAGE, callingPackage)
                    .appendQueryParameter(IncidentManager.URI_PARAM_FLAGS, Integer.toString(flags))
                    .appendQueryParameter(IncidentManager.URI_PARAM_TIMESTAMP,
                            Long.toString(addedWalltime));
            if (receiverClass != null && receiverClass.length() > 0) {
                builder.appendQueryParameter(IncidentManager.URI_PARAM_RECEIVER_CLASS,
                        receiverClass);
            }
            if (reportId != null && reportId.length() > 0) {
                builder.appendQueryParameter(IncidentManager.URI_PARAM_REPORT_ID, reportId);
            }
            return builder.build();
        }
    }
 
    /**
     * Construct new PendingReports with the context.
     */
    PendingReports(Context context) {
        mContext = context;
        mPackageManager = context.getPackageManager();
        mAppOpsManager = context.getSystemService(AppOpsManager.class);
    }
 
    /**
     * ONEWAY binder call to initiate authorizing the report.  The actual logic is posted
     * to mRequestQueue, and may happen later.
     * <p>
     * The security checks are handled by IncidentCompanionService.
     */
    public void authorizeReport(int callingUid, final String callingPackage,
            final String receiverClass, final String reportId, final int flags,
            final IIncidentAuthListener listener) {
        // Starting the system server is complicated, and rather than try to
        // have a complicated lifecycle that we share with dumpstated and incidentd,
        // we will accept the request, and then display it whenever it becomes possible to.
        mRequestQueue.enqueue(listener.asBinder(), true, () -> {
            authorizeReportImpl(callingUid, callingPackage, receiverClass, reportId,
                    flags, listener);
        });
    }
 
    /**
     * ONEWAY binder call to cancel the inbound authorization request.
     * <p>
     * This is a oneway call, and so is authorizeReport, so the
     * caller's ordering is preserved.  The other calls on this object are synchronous, so
     * their ordering is not guaranteed with respect to these calls.  So the implementation
     * sends out extra broadcasts to allow for eventual consistency.
     * <p>
     * The security checks are handled by IncidentCompanionService.
     */
    public void cancelAuthorization(final IIncidentAuthListener listener) {
        mRequestQueue.enqueue(listener.asBinder(), false, () -> {
            cancelReportImpl(listener);
        });
    }
 
    /**
     * SYNCHRONOUS binder call to get the list of reports that are pending confirmation
     * by the user.
     * <p>
     * The security checks are handled by IncidentCompanionService.
     */
    public List<String> getPendingReports() {
        synchronized (mLock) {
            final int size = mPending.size();
            final ArrayList<String> result = new ArrayList(size);
            for (int i = 0; i < size; i++) {
                result.add(mPending.get(i).getUri().toString());
            }
            return result;
        }
    }
 
    /**
     * SYNCHRONOUS binder call to mark a report as approved.
     * <p>
     * The security checks are handled by IncidentCompanionService.
     */
    public void approveReport(String uri) {
        final PendingReportRec rec;
        synchronized (mLock) {
            rec = findAndRemovePendingReportRecLocked(uri);
            if (rec == null) {
                Log.e(TAG, "confirmApproved: Couldn't find record for uri: " + uri);
                return;
            }
        }
 
        // Re-do the broadcast, so whoever is listening knows the list changed,
        // in case another one was added in the meantime.
        sendBroadcast();
 
        Log.i(TAG, "Approved report: " + uri);
        try {
            rec.listener.onReportApproved();
        } catch (RemoteException ex) {
            Log.w(TAG, "Failed calling back for approval for: " + uri, ex);
        }
    }
 
    /**
     * SYNCHRONOUS binder call to mark a report as NOT approved.
     */
    public void denyReport(String uri) {
        final PendingReportRec rec;
        synchronized (mLock) {
            rec = findAndRemovePendingReportRecLocked(uri);
            if (rec == null) {
                Log.e(TAG, "confirmDenied: Couldn't find record for uri: " + uri);
                return;
            }
        }
 
        // Re-do the broadcast, so whoever is listening knows the list changed,
        // in case another one was added in the meantime.
        sendBroadcast();
 
        Log.i(TAG, "Denied report: " + uri);
        try {
            rec.listener.onReportDenied();
        } catch (RemoteException ex) {
            Log.w(TAG, "Failed calling back for denial for: " + uri, ex);
        }
    }
 
    /**
     * Implementation of adb shell dumpsys debugreportcompanion.
     */
    protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
        if (args.length == 0) {
            // Standard text dumpsys
            final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            synchronized (mLock) {
                final int size = mPending.size();
                writer.println("mPending: (" + size + ")");
                for (int i = 0; i < size; i++) {
                    final PendingReportRec entry = mPending.get(i);
                    writer.println(String.format("  %11d %s: %s", entry.addedRealtime,
                                df.format(new Date(entry.addedWalltime)),
                                entry.getUri().toString()));
                }
            }
        }
    }
 
    /**
     * Handle the boot process... Starts everything running once the system is
     * up enough for us to do UI.
     */
    public void onBootCompleted() {
        // Release the enqueued work.
        mRequestQueue.start();
    }
 
    /**
     * Start the confirmation process.
     */
    private void authorizeReportImpl(int callingUid, final String callingPackage,
            final String receiverClass, final String reportId,
            int flags, final IIncidentAuthListener listener) {
        // Enforce that the calling package pertains to the callingUid.
        if (callingUid != 0 && !isPackageInUid(callingUid, callingPackage)) {
            Log.w(TAG, "Calling uid " + callingUid + " doesn't match package "
                    + callingPackage);
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }
 
        // Find the primary user of this device.
        final int primaryUser = getAndValidateUser();
        if (primaryUser == UserHandle.USER_NULL) {
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }
 
        // Find the approver app (hint: it's PermissionController).
        final ComponentName receiver = getApproverComponent(primaryUser);
        if (receiver == null) {
            // We couldn't find an approver... so deny the request here and now, before we
            // do anything else.
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }
 
        // Save the record for when the PermissionController comes back to authorize it.
        PendingReportRec rec = null;
        synchronized (mLock) {
            rec = new PendingReportRec(callingPackage, receiverClass, reportId, flags, listener);
            mPending.add(rec);
        }
 
        try {
            listener.asBinder().linkToDeath(() -> {
                Log.i(TAG, "Got death notification listener=" + listener);
                cancelReportImpl(listener, receiver, primaryUser);
            }, 0);
        } catch (RemoteException ex) {
            Log.e(TAG, "Remote died while trying to register death listener: " + rec.getUri());
            // First, remove from our list.
            cancelReportImpl(listener, receiver, primaryUser);
        }
 
        // Go tell Permission controller to start asking the user.
        sendBroadcast(receiver, primaryUser);
    }
 
    /**
     * Cancel a pending report request (because of an explicit call to cancel)
     */
    private void cancelReportImpl(IIncidentAuthListener listener) {
        final int primaryUser = getAndValidateUser();
        final ComponentName receiver = getApproverComponent(primaryUser);
        if (primaryUser != UserHandle.USER_NULL && receiver != null) {
            cancelReportImpl(listener, receiver, primaryUser);
        }
    }
 
    /**
     * Cancel a pending report request (either because of an explicit call to cancel
     * by the calling app, or because of a binder death).
     */
    private void cancelReportImpl(IIncidentAuthListener listener, ComponentName receiver,
            int primaryUser) {
        // First, remove from our list.
        synchronized (mLock) {
            removePendingReportRecLocked(listener);
        }
        // Second, call back to PermissionController to say it's canceled.
        sendBroadcast(receiver, primaryUser);
    }
 
    /**
     * Send an extra copy of the broadcast, to tell them that the list has changed
     * because of an addition or removal.  This function is less aggressive than
     * authorizeReportImpl in logging about failures, because this is for use in
     * cleanup cases to keep the apps' list in sync with ours.
     */
    private void sendBroadcast() {
        final int primaryUser = getAndValidateUser();
        if (primaryUser == UserHandle.USER_NULL) {
            return;
        }
        final ComponentName receiver = getApproverComponent(primaryUser);
        if (receiver == null) {
            return;
        }
        sendBroadcast(receiver, primaryUser);
    }
 
    /**
     * Send the confirmation broadcast.
     */
    private void sendBroadcast(ComponentName receiver, int primaryUser) {
        final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
        intent.setComponent(receiver);
        final BroadcastOptions options = BroadcastOptions.makeBasic();
        options.setBackgroundActivityStartsAllowed(true);
 
        // Send it to the primary user.
        mContext.sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(primaryUser),
                android.Manifest.permission.APPROVE_INCIDENT_REPORTS, options.toBundle());
    }
 
    /**
     * Remove a PendingReportRec keyed by uri, and return it.
     */
    private PendingReportRec findAndRemovePendingReportRecLocked(String uriString) {
        final Uri uri = Uri.parse(uriString);
        final int id;
        try {
            final String idStr = uri.getQueryParameter(IncidentManager.URI_PARAM_ID);
            id = Integer.parseInt(idStr);
        } catch (NumberFormatException ex) {
            Log.w(TAG, "Can't parse id from: " + uriString);
            return null;
        }
 
        for (Iterator<PendingReportRec> i = mPending.iterator(); i.hasNext();) {
            final PendingReportRec rec = i.next();
            if (rec.id == id) {
                i.remove();
                return rec;
            }
        }
        return null;
    }
 
    /**
     * Remove a PendingReportRec keyed by listener.
     */
    private void removePendingReportRecLocked(IIncidentAuthListener listener) {
 
        for (Iterator<PendingReportRec> i = mPending.iterator(); i.hasNext();) {
            final PendingReportRec rec = i.next();
            if (rec.listener.asBinder() == listener.asBinder()) {
                Log.i(TAG, "  ...Removed PendingReportRec index=" + i + ": " + rec.getUri());
                i.remove();
            }
        }
    }
 
    /**
     * Just call listener.deny() (wrapping the RemoteException), without try to
     * add it to the list.
     */
    private void denyReportBeforeAddingRec(IIncidentAuthListener listener, String pkg) {
        try {
            listener.onReportDenied();
        } catch (RemoteException ex) {
            Log.w(TAG, "Failed calling back for denial for " + pkg, ex);
        }
    }
 
    /**
     * Check whether the current user is the primary user, and return the user id if they are.
     * Returns UserHandle.USER_NULL if not valid.
     */
    private int getAndValidateUser() {
        return IncidentCompanionService.getAndValidateUser(mContext);
    }
 
    /**
     * Return the ComponentName of the BroadcastReceiver that will approve reports.
     * The system must have zero or one of these installed.  We only look on the
     * system partition.  When the broadcast happens, the component will also need
     * have the APPROVE_INCIDENT_REPORTS permission.
     */
    private ComponentName getApproverComponent(int userId) {
        // Find the one true BroadcastReceiver
        final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
        final List<ResolveInfo> matches = mPackageManager.queryBroadcastReceiversAsUser(intent,
                PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
        if (matches.size() == 1) {
            return matches.get(0).getComponentInfo().getComponentName();
        } else {
            Log.w(TAG, "Didn't find exactly one BroadcastReceiver to handle "
                    + Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED
                    + ". The report will be denied. size="
                    + matches.size() + ": matches=" + matches);
            return null;
        }
    }
 
    /**
     * Return whether the package is one of the packages installed for the uid.
     */
    private boolean isPackageInUid(int uid, String packageName) {
        try {
            mAppOpsManager.checkPackage(uid, packageName);
            return true;
        } catch (SecurityException ex) {
            return false;
        }
    }
}