summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk52
-rw-r--r--AndroidManifest.xml21
-rw-r--r--PermissionController.mk50
-rw-r--r--res/drawable/ic_bug_report_black_24dp.xml26
-rw-r--r--res/values/strings.xml31
-rw-r--r--src/com/android/packageinstaller/Constants.java15
-rw-r--r--src/com/android/packageinstaller/incident/ApprovalReceiver.java54
-rw-r--r--src/com/android/packageinstaller/incident/ConfirmationActivity.java144
-rw-r--r--src/com/android/packageinstaller/incident/ConfirmationReceiver.java33
-rw-r--r--src/com/android/packageinstaller/incident/Formatting.java74
-rw-r--r--src/com/android/packageinstaller/incident/PendingList.java336
-rw-r--r--test/Android.mk2
-rw-r--r--test/instrumentation/Android.mk44
-rw-r--r--test/instrumentation/AndroidManifest.xml25
-rw-r--r--test/instrumentation/AndroidTest.xml30
-rw-r--r--test/instrumentation/res/values/strings.xml20
-rw-r--r--test/instrumentation/src/com/android/permissioncontrollertesthelper/incident/RequestConfirmationTest.java283
17 files changed, 1189 insertions, 51 deletions
diff --git a/Android.mk b/Android.mk
index dd9fd148..cf7250d4 100644
--- a/Android.mk
+++ b/Android.mk
@@ -1,50 +1,6 @@
LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_SRC_FILES := \
- $(call all-java-files-under, src)
-
-LOCAL_STATIC_ANDROID_LIBRARIES += \
- iconloader \
- androidx.car_car \
- androidx.design_design \
- androidx.transition_transition \
- androidx.core_core \
- androidx.media_media \
- androidx.legacy_legacy-support-core-utils \
- androidx.legacy_legacy-support-core-ui \
- androidx.fragment_fragment \
- androidx.appcompat_appcompat \
- androidx.preference_preference \
- androidx.recyclerview_recyclerview \
- androidx.legacy_legacy-preference-v14 \
- androidx.leanback_leanback \
- androidx.leanback_leanback-preference \
- androidx.lifecycle_lifecycle-extensions \
- androidx.lifecycle_lifecycle-common-java8 \
- SettingsLibHelpUtils \
- SettingsLibRestrictedLockUtils \
- SettingsLibAppPreference \
- SettingsLibSearchWidget \
- SettingsLibSettingsSpinner \
- SettingsLibLayoutPreference \
- SettingsLibActionButtonsPreference \
- SettingsLibBarChartPreference \
- SettingsLibEntityHeaderWidgets
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- androidx.annotation_annotation
-
-LOCAL_PACKAGE_NAME := PermissionController
-LOCAL_SDK_VERSION := system_current
-LOCAL_MIN_SDK_VERSION := 28
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_CERTIFICATE := platform
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-
-include $(BUILD_PACKAGE)
+# In order to build the apk and tests for both AOSP and other builds
+# via inherit-package, the makefile for PermissionController itself must
+# not include the subdir makefiles, so it is split into its own makefile.
+include $(LOCAL_PATH)/PermissionController.mk $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index aec523df..af11ed5e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -29,8 +29,9 @@
<uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS" />
<uses-permission android:name="android.permission.SET_PREFERRED_APPLICATIONS" />
<uses-permission android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER" />
-
<uses-permission android:name="android.permission.ACCESS_INSTANT_APPS" />
+ <uses-permission android:name="android.permission.REQUEST_INCIDENT_REPORT_APPROVAL" />
+ <uses-permission android:name="android.permission.APPROVE_INCIDENT_REPORTS" />
<uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28" />
@@ -205,6 +206,24 @@
<action android:name="android.rolecontrollerservice.RoleControllerService"/>
</intent-filter>
</service>
+
+ <!-- Debug report authorization (bugreport and incident report) -->
+ <receiver android:name="com.android.packageinstaller.incident.ConfirmationReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.PENDING_INCIDENT_REPORTS_CHANGED" />
+ </intent-filter>
+ </receiver>
+
+ <activity android:name="com.android.packageinstaller.incident.ConfirmationActivity"
+ android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar"
+ android:exported="false"
+ android:excludeFromRecents="true"
+ android:noHistory="true" />
+
+ <receiver android:name="com.android.packageinstaller.incident.ApprovalReceiver"
+ android:exported="false" />
+
</application>
</manifest>
diff --git a/PermissionController.mk b/PermissionController.mk
new file mode 100644
index 00000000..dd9fd148
--- /dev/null
+++ b/PermissionController.mk
@@ -0,0 +1,50 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src)
+
+LOCAL_STATIC_ANDROID_LIBRARIES += \
+ iconloader \
+ androidx.car_car \
+ androidx.design_design \
+ androidx.transition_transition \
+ androidx.core_core \
+ androidx.media_media \
+ androidx.legacy_legacy-support-core-utils \
+ androidx.legacy_legacy-support-core-ui \
+ androidx.fragment_fragment \
+ androidx.appcompat_appcompat \
+ androidx.preference_preference \
+ androidx.recyclerview_recyclerview \
+ androidx.legacy_legacy-preference-v14 \
+ androidx.leanback_leanback \
+ androidx.leanback_leanback-preference \
+ androidx.lifecycle_lifecycle-extensions \
+ androidx.lifecycle_lifecycle-common-java8 \
+ SettingsLibHelpUtils \
+ SettingsLibRestrictedLockUtils \
+ SettingsLibAppPreference \
+ SettingsLibSearchWidget \
+ SettingsLibSettingsSpinner \
+ SettingsLibLayoutPreference \
+ SettingsLibActionButtonsPreference \
+ SettingsLibBarChartPreference \
+ SettingsLibEntityHeaderWidgets
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ androidx.annotation_annotation
+
+LOCAL_PACKAGE_NAME := PermissionController
+LOCAL_SDK_VERSION := system_current
+LOCAL_MIN_SDK_VERSION := 28
+LOCAL_PRIVILEGED_MODULE := true
+LOCAL_CERTIFICATE := platform
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+include $(BUILD_PACKAGE)
diff --git a/res/drawable/ic_bug_report_black_24dp.xml b/res/drawable/ic_bug_report_black_24dp.xml
new file mode 100644
index 00000000..fe7e443b
--- /dev/null
+++ b/res/drawable/ic_bug_report_black_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"
+ android:fillColor="@android:color/white" />
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 19c00d60..81e70595 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4,9 +4,9 @@
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.
@@ -575,4 +575,31 @@
<!-- Dialog body explaining that the app just selected by the user will not work after a reboot until the user enters their credentials, such as a PIN or password. [CHAR LIMIT=NONE] -->
<string name="encryption_unaware_confirmation_message">Note: If you restart your device and have a screen lock set, this app can\u2019t start until you unlock your device.</string>
+
+ <!-- Name for the notification channel for incident / bug report confirmation
+ [CHAR LIMIT=50] -->
+ <string name="incident_report_channel_name">Share Debugging Data</string>
+
+ <!-- Title for notification shown when the user should confirm an incident / bug report.
+ [CHAR LIMIT=50] -->
+ <string name="incident_report_notification_title">Share detailed debugging data?</string>
+
+ <!-- Content for notification shown when the user should confirm an incident / bug report.
+ [CHAR LIMIT=120] -->
+ <string name="incident_report_notification_text"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> would like to upload debugging information.</string>
+
+ <!-- Title for the incident / bug report confirmation dialog [CHAR LIMIT=50] -->
+ <string name="incident_report_dialog_title">Share Debugging Data</string>
+
+ <!-- Content for dialog shown when the user should confirm an incident / bug report.
+ [CHAR LIMIT=none] -->
+ <string name="incident_report_dialog_text">"<xliff:g id="app_name" example="Gmail">%1$s</xliff:g> is requesting to upload a bug report from this device taken on <xliff:g id="date" example="December 26, 2018">%2$s</xliff:g> at <xliff:g id="time" example="1:20 PM">%3$s</xliff:g>. Bug reports include personal information about your device or logged by apps, for example, user names, location data, device identifiers, and network information. Only share bug reports with people and apps you trust with this information.
+
+Allow <xliff:g id="app_name" example="Gmail">%4$s</xliff:g> to upload a bug report?"</string>
+
+ <!-- Label for the button to allow sharing of the report. [CHAR LIMIT=20]-->
+ <string name="incident_report_dialog_allow_label">Allow</string>
+
+ <!-- Label for the button to NOT allow sharing of the report. [CHAR LIMIT=20] -->
+ <string name="incident_report_dialog_deny_label">Deny</string>
</resources>
diff --git a/src/com/android/packageinstaller/Constants.java b/src/com/android/packageinstaller/Constants.java
index 0ff61902..f4de96e6 100644
--- a/src/com/android/packageinstaller/Constants.java
+++ b/src/com/android/packageinstaller/Constants.java
@@ -47,6 +47,21 @@ public class Constants {
public static final int LOCATION_ACCESS_CHECK_NOTIFICATION_ID = 0;
/**
+ * Key for Notification.Builder.setGroup() for the incident report approval notification.
+ */
+ public static final String INCIDENT_NOTIFICATION_GROUP_KEY = "incident confirmation";
+
+ /**
+ * Key for Notification.Builder.setChannelId() for the incident report approval notification.
+ */
+ public static final String INCIDENT_NOTIFICATION_CHANNEL_ID = "incident_confirmation";
+
+ /**
+ * ID for our notification. We always post it with a tag which is the uri in string form.
+ */
+ public static final int INCIDENT_NOTIFICATION_ID = 66900652;
+
+ /**
* Channel of the notifications shown by
* {@link com.android.packageinstaller.permission.service.LocationAccessCheck}.
*/
diff --git a/src/com/android/packageinstaller/incident/ApprovalReceiver.java b/src/com/android/packageinstaller/incident/ApprovalReceiver.java
new file mode 100644
index 00000000..60a303e9
--- /dev/null
+++ b/src/com/android/packageinstaller/incident/ApprovalReceiver.java
@@ -0,0 +1,54 @@
+/*
+ * 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.packageinstaller.incident;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.IncidentManager;
+
+/**
+ * BroadcastReceiver to handle clicking on the approval and rejection buttons
+ * in the notification.
+ */
+public class ApprovalReceiver extends BroadcastReceiver {
+ /**
+ * Action for an approval.
+ */
+ public static final String ACTION_APPROVE = "com.android.packageinstaller.incident.APPROVE";
+
+ /**
+ * Action for a denial.
+ */
+ public static final String ACTION_DENY = "com.android.packageinstaller.incident.DENY";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final Uri uri = intent.getData();
+ if (uri != null) {
+ final IncidentManager incidentManager = context.getSystemService(IncidentManager.class);
+ if (ACTION_APPROVE.equals(intent.getAction())) {
+ incidentManager.approveReport(uri);
+ } else if (ACTION_DENY.equals(intent.getAction())) {
+ incidentManager.denyReport(uri);
+ }
+ }
+ PendingList.getInstance().updateState(context, PendingList.FLAG_FROM_NOTIFICATION);
+ }
+}
+
diff --git a/src/com/android/packageinstaller/incident/ConfirmationActivity.java b/src/com/android/packageinstaller/incident/ConfirmationActivity.java
new file mode 100644
index 00000000..2dfdcec4
--- /dev/null
+++ b/src/com/android/packageinstaller/incident/ConfirmationActivity.java
@@ -0,0 +1,144 @@
+/*
+ * 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.packageinstaller.incident;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IncidentManager;
+import android.util.Log;
+
+import com.android.permissioncontroller.R;
+
+/**
+ * Confirmation dialog for approving an incident or bug report for sharing off the device.
+ */
+public class ConfirmationActivity extends Activity implements OnClickListener, OnDismissListener {
+ private static final String TAG = ConfirmationActivity.class.getSimpleName();
+
+ /**
+ * Currently displaying activity.
+ */
+ private static ConfirmationActivity sCurrentActivity;
+
+ /**
+ * Currently displaying uri.
+ */
+ private static Uri sCurrentUri;
+
+ /**
+ * If this activity is running in the current process, call finish() on it.
+ */
+ public static void finishCurrent() {
+ if (sCurrentActivity != null) {
+ sCurrentActivity.finish();
+ }
+ }
+
+ /**
+ * If the activity is in the resumed state, then record the Uri for the current
+ * one, so PendingList can skip re-showing the same one.
+ */
+ public static Uri getCurrentUri() {
+ return sCurrentUri;
+ }
+
+ /**
+ * Create the activity.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Formatting formatting = new Formatting(this);
+
+ final Uri uri = getIntent().getData();
+ if (uri == null) {
+ Log.w(TAG, "No uri in intent: " + getIntent());
+ finish();
+ return;
+ }
+
+ final IncidentManager.PendingReport report = new IncidentManager.PendingReport(uri);
+ final String appLabel = formatting.getAppLabel(report.getRequestingPackage());
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.incident_report_dialog_title)
+ .setMessage(getString(R.string.incident_report_dialog_text,
+ appLabel,
+ formatting.getDate(report.getTimestamp()),
+ formatting.getTime(report.getTimestamp()),
+ appLabel))
+ .setPositiveButton(R.string.incident_report_dialog_allow_label, this)
+ .setNegativeButton(R.string.incident_report_dialog_deny_label, this)
+ .setOnDismissListener(this)
+ .show();
+ }
+
+ /**
+ * Activity lifecycle callback. Now visible.
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ sCurrentActivity = this;
+ sCurrentUri = getIntent().getData();
+ }
+
+ /**
+ * Activity lifecycle callback. Now not visible.
+ */
+ @Override
+ protected void onStop() {
+ super.onStop();
+ sCurrentActivity = null;
+ sCurrentUri = null;
+ }
+
+ /**
+ * Dialog canceled.
+ */
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ finish();
+ }
+
+ /**
+ * Explicit button click.
+ */
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final IncidentManager incidentManager = getSystemService(IncidentManager.class);
+
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ incidentManager.approveReport(getIntent().getData());
+ PendingList.getInstance().updateState(this, 0);
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ incidentManager.denyReport(getIntent().getData());
+ PendingList.getInstance().updateState(this, 0);
+ break;
+ }
+ finish();
+ }
+}
+
diff --git a/src/com/android/packageinstaller/incident/ConfirmationReceiver.java b/src/com/android/packageinstaller/incident/ConfirmationReceiver.java
new file mode 100644
index 00000000..5c607732
--- /dev/null
+++ b/src/com/android/packageinstaller/incident/ConfirmationReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * 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.packageinstaller.incident;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * BroadcastReceiver to handle posting a notification indicating that an app would
+ * like access to a bug or incident report.
+ */
+public class ConfirmationReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ PendingList.getInstance().updateState(context, 0);
+ }
+}
+
diff --git a/src/com/android/packageinstaller/incident/Formatting.java b/src/com/android/packageinstaller/incident/Formatting.java
new file mode 100644
index 00000000..8868305d
--- /dev/null
+++ b/src/com/android/packageinstaller/incident/Formatting.java
@@ -0,0 +1,74 @@
+/*
+ * 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.packageinstaller.incident;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+import com.android.packageinstaller.permission.utils.Utils;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Utility class for formatting the incident report confirmations.
+ */
+public class Formatting {
+ private final Context mContext;
+ private final PackageManager mPm;
+ private final DateFormat mDateFormat;
+ private final DateFormat mTimeFormat;
+
+ /**
+ * Constructor. This object keeps the context.
+ */
+ Formatting(Context context) {
+ mContext = context;
+ mPm = context.getPackageManager();
+ mDateFormat = android.text.format.DateFormat.getDateFormat(context);
+ mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);
+ }
+
+ /**
+ * Get the name to show the user for an application, given the package name.
+ * If the application can't be found, returns null.
+ */
+ String getAppLabel(String pkg) {
+ ApplicationInfo app;
+ try {
+ app = mPm.getApplicationInfo(pkg, 0);
+ } catch (PackageManager.NameNotFoundException ex) {
+ return null;
+ }
+ return Utils.getAppLabel(app, mContext);
+ }
+
+ /**
+ * Format the date portion of a {@link System.currentTimeMillis} as a user-visible string.
+ */
+ String getDate(long wallTimeMs) {
+ return mDateFormat.format(new Date(wallTimeMs));
+ }
+
+ /**
+ * Format the time portion of a {@link System.currentTimeMillis} as a user-visible string.
+ */
+ String getTime(long wallTimeMs) {
+ return mTimeFormat.format(new Date(wallTimeMs));
+ }
+}
diff --git a/src/com/android/packageinstaller/incident/PendingList.java b/src/com/android/packageinstaller/incident/PendingList.java
new file mode 100644
index 00000000..5af2204c
--- /dev/null
+++ b/src/com/android/packageinstaller/incident/PendingList.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.incident;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.IncidentManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.packageinstaller.Constants;
+import com.android.permissioncontroller.R;
+
+import java.text.Collator;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Represents the current list of pending records.
+ */
+class PendingList {
+ private static final String TAG = "PermissionController.incident";
+
+ /**
+ * Flag for {@link #UpdateState} to flag whether this update is coming from the
+ * notification handling. If it is, then no dialogs will be shown.
+ */
+ static final int FLAG_FROM_NOTIFICATION = 0x1;
+
+ /**
+ * Shared preferences file name.
+ */
+ private static final String SHARED_PREFS_NAME =
+ "com.android.packageinstaller.incident.PendingList";
+
+ /**
+ * Key for the list of currently showing notifications.
+ */
+ private static final String SHARED_PREFS_KEY_NOTIFICATIONS = "notifications";
+
+ /**
+ * Singleton instance.
+ */
+ private static final PendingList sInstance = new PendingList();
+
+ /**
+ * Date format that will sort lexicographical, so we can have our notifications sorted.
+ */
+ private static final SimpleDateFormat sDateFormatter =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ /**
+ * List of currently pending records.
+ */
+ private static class Rec {
+ /**
+ * Constructor.
+ */
+ Rec(IncidentManager.PendingReport r, String l) {
+ this.report = r;
+ this.label = l;
+ }
+
+ /**
+ * The incident report to show.
+ */
+ public final IncidentManager.PendingReport report;
+
+ /**
+ * The user-visible name of the entry.
+ */
+ public final String label;
+ }
+
+ /**
+ * Class to update the state. Holds the Context, and other system services for
+ * the duration of the update.
+ */
+ private static class Updater {
+ private final Context mContext;
+ private final int mFlags;
+ private final NotificationManager mNm;
+ private final Formatting mFormatting;
+ private Collator mCollator;
+
+ /**
+ * Constructor.
+ */
+ Updater(Context context, int flags) {
+ mContext = context;
+ mFlags = flags;
+ mNm = context.getSystemService(NotificationManager.class);
+ mFormatting = new Formatting(context);
+ mCollator = Collator.getInstance(
+ context.getResources().getConfiguration().getLocales().get(0));
+ }
+
+ /**
+ * Perform the update.
+ */
+ void updateState() {
+ final IncidentManager incidentManager =
+ mContext.getSystemService(IncidentManager.class);
+ final List<IncidentManager.PendingReport> reports = incidentManager.getPendingReports();
+
+ // Load whatever we previously displayed. This may result in some spurious
+ // cancel calls across reboots... but that's not an actual problem.
+ final SharedPreferences prefs = mContext.getSharedPreferences(SHARED_PREFS_NAME,
+ Context.MODE_PRIVATE);
+ final Set<String> prevNotifications =
+ prefs.getStringSet(SHARED_PREFS_KEY_NOTIFICATIONS, null);
+ final ArraySet<String> remainingNotifications = new ArraySet<String>();
+ if (prevNotifications != null) {
+ for (final String s: prevNotifications) {
+ remainingNotifications.add(s);
+ }
+ }
+ final ArraySet<String> currentNotifications = new ArraySet<String>();
+
+ // Load everything we will need for display
+ final List<Rec> recs = new ArrayList();
+ final int recCount = reports.size();
+ for (int i = 0; i < recCount; i++) {
+ final IncidentManager.PendingReport report = reports.get(i);
+ final String label = mFormatting.getAppLabel(report.getRequestingPackage());
+ if (label == null) {
+ Log.w(TAG, "Application (or its label) could not be found. Summarily "
+ + " denying report: " + report.getRequestingPackage());
+ incidentManager.denyReport(report.getUri());
+ continue;
+ }
+
+ recs.add(new Rec(report, label));
+ }
+
+ // Sort by timestamp, then by label name (for a stable ordering, with the assumption
+ // that apps only post one at a time).
+ recs.sort((a, b) -> {
+ long val = a.report.getTimestamp() - b.report.getTimestamp();
+ if (val == 0) {
+ return mCollator.compare(a.label, b.label);
+ } else {
+ return val < 0 ? -1 : 1;
+ }
+ });
+
+ // Collect what we are going to do.
+ Rec firstDialog = null;
+ final List<Rec> notificationRecs = new ArrayList();
+ final int notificationCount = recs.size();
+ for (int i = 0; i < notificationCount; i++) {
+ final Rec rec = recs.get(i);
+ notificationRecs.add(rec);
+ final String uri = rec.report.getUri().toString();
+ remainingNotifications.remove(uri);
+ currentNotifications.add(uri);
+ if ((rec.report.getFlags() & IncidentManager.FLAG_CONFIRMATION_DIALOG) != 0) {
+ if (firstDialog == null) {
+ firstDialog = rec;
+ }
+ }
+ }
+
+ if (false) {
+ Log.d(TAG, "PermissionController pending list plan ... {");
+ Log.d(TAG, " showing {");
+ for (int i = 0; i < notificationRecs.size(); i++) {
+ Log.d(TAG, " [" + i + "] " + notificationRecs.get(i).report.getUri());
+ }
+ Log.d(TAG, " }");
+ Log.d(TAG, " canceling {");
+ for (int i = 0; i < remainingNotifications.size(); i++) {
+ Log.d(TAG, " [" + i + "] " + remainingNotifications.valueAt(i));
+ }
+ Log.d(TAG, " }");
+ Log.d(TAG, "}");
+ }
+
+ // Show the notifications
+ showNotifications(notificationRecs);
+
+ // Cancel any previously remaining notifications
+ final int remainingCount = remainingNotifications.size();
+ for (int i = 0; i < remainingCount; i++) {
+ mNm.cancel(remainingNotifications.valueAt(i), Constants.INCIDENT_NOTIFICATION_ID);
+ }
+
+ // The dialog
+ if (firstDialog != null) {
+ // Show the new dialog. The FLAG_ACTIVITY_CLEAR_TASK in the intent
+ // will remove any previously showing dialog. We check the static
+ // on ConfirmationActivity so that if the dialog is currently on
+ // top, for the same Uri, then we won't cause jank by re-showing
+ // the same one.
+ if (!firstDialog.report.getUri().equals(ConfirmationActivity.getCurrentUri())) {
+ if ((mFlags & FLAG_FROM_NOTIFICATION) == 0) {
+ mContext.startActivity(newDialogIntent(firstDialog));
+ }
+ }
+ } else {
+ // Cancel any previously showing one. The activity has the noHistory
+ // flag set in the manifest, so we know that if won't be somewhere in
+ // the background, waiting to come back.
+ ConfirmationActivity.finishCurrent();
+ }
+
+ // Save this list, so we know what we did for next time.
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putStringSet(SHARED_PREFS_KEY_NOTIFICATIONS, currentNotifications);
+ editor.apply();
+ }
+
+ /**
+ * Show the list of notifications and cancel any unneeded ones.
+ */
+ private void showNotifications(List<Rec> recs) {
+ createNotificationChannel();
+
+ final int recCount = recs.size();
+ for (int i = 0; i < recCount; i++) {
+ final Rec rec = recs.get(i);
+
+ // Intent for the confirmation dialog.
+ final PendingIntent dialog = PendingIntent.getActivity(mContext, 0,
+ newDialogIntent(rec), 0);
+
+ // Intent for the approval and denial.
+ final PendingIntent deny = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(ApprovalReceiver.ACTION_DENY, rec.report.getUri(),
+ mContext, ApprovalReceiver.class),
+ 0);
+
+ // Construct the notification
+ final Notification notification = new Notification.Builder(mContext)
+ .setStyle(new Notification.BigTextStyle())
+ .setContentTitle(
+ mContext.getString(R.string.incident_report_notification_title))
+ .setContentText(
+ mContext.getString(R.string.incident_report_notification_text,
+ rec.label))
+ .setSmallIcon(R.drawable.ic_bug_report_black_24dp)
+ .setWhen(rec.report.getTimestamp())
+ .setGroup(Constants.INCIDENT_NOTIFICATION_GROUP_KEY)
+ .setChannelId(Constants.INCIDENT_NOTIFICATION_CHANNEL_ID)
+ .setSortKey(getSortKey(rec.report.getTimestamp()))
+ .setContentIntent(dialog)
+ .setDeleteIntent(deny)
+ .setColor(mContext.getColor(
+ android.R.color.system_notification_accent_color))
+ .build();
+
+ // Show the notification
+ mNm.notify(rec.report.getUri().toString(), Constants.INCIDENT_NOTIFICATION_ID,
+ notification);
+ }
+ }
+
+ /**
+ * Create the notification channel for {@link #NOTIFICATION_CHANNEL_ID}.
+ */
+ private void createNotificationChannel() {
+ final NotificationChannel channel = new NotificationChannel(
+ Constants.INCIDENT_NOTIFICATION_CHANNEL_ID,
+ mContext.getString(R.string.incident_report_channel_name),
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ // TODO: Not in SystemApi, so we can't use it.
+ // channel.setBlockableSystem(true);
+
+ mNm.createNotificationChannel(channel);
+ }
+
+ /**
+ * Get the sort key for the order of our notifications.
+ */
+ private String getSortKey(long timestamp) {
+ return sDateFormatter.format(new Date(timestamp));
+ }
+
+ /**
+ * Create the intent to launch the dialog activity for the Rec.
+ */
+ private Intent newDialogIntent(Rec rec) {
+ final Intent result = new Intent(Intent.ACTION_MAIN, rec.report.getUri(),
+ mContext, ConfirmationActivity.class);
+ result.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ return result;
+ }
+ }
+
+ /**
+ * Get the singleton instance. Note that there is no Context associated
+ * with this object. The context should be passed in to updateState, and
+ * the assumption is that it could be a background context (i.e. the one for a
+ * BroadcastReceiver), so no direct UI can be done on it as it would be with
+ * an Activity object.
+ */
+ public static PendingList getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Constructor.
+ */
+ private PendingList() {
+ }
+
+ /**
+ * Update the notifications and dialog to reflect the current state of affairs.
+ */
+ public void updateState(Context context, int flags) {
+ (new Updater(context, flags)).updateState();
+ }
+}
diff --git a/test/Android.mk b/test/Android.mk
new file mode 100644
index 00000000..cfd03be5
--- /dev/null
+++ b/test/Android.mk
@@ -0,0 +1,2 @@
+LOCAL_PATH:= $(call my-dir)
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/test/instrumentation/Android.mk b/test/instrumentation/Android.mk
new file mode 100644
index 00000000..d09402d7
--- /dev/null
+++ b/test/instrumentation/Android.mk
@@ -0,0 +1,44 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := PermissionControllerTest
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_LIBRARIES := android.test.runner.stubs
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ ctstestrunner \
+ compatibility-device-util \
+ androidx.annotation_annotation \
+ androidx.test.runner \
+ androidx.test.rules \
+ androidx.test.uiautomator_uiautomator \
+ androidx.legacy_legacy-support-v4 \
+ platform-test-annotations
+
+LOCAL_SDK_VERSION := test_current
+
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_INSTRUMENTATION_FOR := PermissionController
+
+include $(BUILD_PACKAGE)
diff --git a/test/instrumentation/AndroidManifest.xml b/test/instrumentation/AndroidManifest.xml
new file mode 100644
index 00000000..ba4dfcff
--- /dev/null
+++ b/test/instrumentation/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.permissioncontrollertest" >
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.permissioncontroller" />
+</manifest>
diff --git a/test/instrumentation/AndroidTest.xml b/test/instrumentation/AndroidTest.xml
new file mode 100644
index 00000000..6d6d0c09
--- /dev/null
+++ b/test/instrumentation/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<!-- This test config file is auto-generated. -->
+<configuration description="Runs GooglePermissionControllerTest.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+ <option name="test-suite-tag" value="gts" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="GooglePermissionControllerTest.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.permissioncontrollertest" />
+ <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/test/instrumentation/res/values/strings.xml b/test/instrumentation/res/values/strings.xml
new file mode 100644
index 00000000..614e6289
--- /dev/null
+++ b/test/instrumentation/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name">Permission Controller Tests</string>
+</resources>
diff --git a/test/instrumentation/src/com/android/permissioncontrollertesthelper/incident/RequestConfirmationTest.java b/test/instrumentation/src/com/android/permissioncontrollertesthelper/incident/RequestConfirmationTest.java
new file mode 100644
index 00000000..b49877b7
--- /dev/null
+++ b/test/instrumentation/src/com/android/permissioncontrollertesthelper/incident/RequestConfirmationTest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2017 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.permissioncontrollertest.incident;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.ConditionVariable;
+import android.os.IncidentManager;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import com.android.permissioncontroller.R;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/** Test for incident report sharing approval. */
+public class RequestConfirmationTest {
+ private static final String TAG = "RequestConfirmationTest";
+
+ // TODO: Get this from config?
+ private static final String PERMISSION_CONTROLLER_PKG =
+ "com.google.android.permissioncontroller";
+
+ private static final int AID_SHELL = 2000;
+ private static final String PKG_SHELL = "com.android.shell";
+
+ private static final int TIMEOUT = 3000;
+
+ private enum Status {
+ PENDING,
+ APPROVED,
+ DENIED,
+ TIMED_OUT
+ }
+
+ private class AuthListener extends IncidentManager.AuthListener {
+ private Status mStatus = Status.PENDING;
+ private final ConditionVariable mCondition = new ConditionVariable();
+
+ @Override
+ public void onReportApproved() {
+ synchronized (mCondition) {
+ mStatus = Status.APPROVED;
+ mCondition.open();
+ }
+ }
+
+ @Override
+ public void onReportDenied() {
+ synchronized (mCondition) {
+ mStatus = Status.DENIED;
+ mCondition.open();
+ }
+ }
+
+ public void waitForResponse(long timeoutMs) {
+ if (!mCondition.block(timeoutMs)) {
+ mStatus = Status.TIMED_OUT;
+ }
+ }
+
+ public Status getStatus() {
+ return mStatus;
+ }
+ };
+
+ private AuthListener requestApproval(int callingUid, final String callingPackage,
+ final int flags) {
+ final Context context = InstrumentationRegistry.getContext();
+ final IncidentManager incidentManager = context.getSystemService(IncidentManager.class);
+ final AuthListener listener = new AuthListener();
+ incidentManager.requestAuthorization(callingUid, callingPackage, flags, listener);
+ return listener;
+ }
+
+ private AuthListener cancelAuthorization(AuthListener listener) {
+ final Context context = InstrumentationRegistry.getContext();
+ final IncidentManager incidentManager = context.getSystemService(IncidentManager.class);
+ incidentManager.cancelAuthorization(listener);
+ return listener;
+ }
+
+ private UiObject2 waitAndAssert(UiDevice device, BySelector selector) {
+ Assert.assertTrue(device.wait(Until.hasObject(selector), TIMEOUT));
+ final UiObject2 obj = device.findObject(selector);
+ Assert.assertNotNull(obj);
+ return obj;
+ }
+
+ private UiObject2 waitAndAssert(UiObject2 object, BySelector selector) {
+ Assert.assertTrue(object.wait(Until.hasObject(selector), TIMEOUT));
+ final UiObject2 obj = object.findObject(selector);
+ Assert.assertNotNull(obj);
+ return obj;
+ }
+
+ private UiObject2 getAndAssert(UiDevice device, BySelector selector) {
+ final UiObject2 obj = device.findObject(selector);
+ Assert.assertNotNull(obj);
+ return obj;
+ }
+
+ private UiObject2 getAndAssert(UiObject2 object, BySelector selector) {
+ final UiObject2 obj = object.findObject(selector);
+ Assert.assertNotNull(obj);
+ return obj;
+ }
+
+ /**
+ * Combined logic for the tests.
+ */
+ private void runTestFlow(boolean notification, Status expected) {
+ AuthListener listener = null;
+ try {
+ Log.d(TAG, String.format(
+ "---- BEGIN -- notification=%-5s expected=%-9s ------------------",
+ notification, expected));
+
+ final Resources res = InstrumentationRegistry.getTargetContext().getResources();
+
+ Log.d(TAG, "Initialize UiDevice instance");
+ final UiDevice device;
+ device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+ Log.d(TAG, "Request approval");
+ listener = requestApproval(AID_SHELL, PKG_SHELL,
+ notification ? 0 : IncidentManager.FLAG_CONFIRMATION_DIALOG);
+
+ if (notification) {
+ Log.d(TAG, "Open the notification panel");
+ device.openNotification();
+
+ Log.d(TAG, "Find the notification");
+ final UiObject2 notifications = getAndAssert(device,
+ By.pkg("com.android.systemui")
+ .res("com.android.systemui:id/notification_stack_scroller"));
+
+ Log.d(TAG, "Wait for the notification to appear");
+ final UiObject2 text = waitAndAssert(notifications,
+ By.text(res.getString(R.string.incident_report_notification_title)));
+
+ Log.d(TAG, "Clicking the text");
+ text.click();
+ }
+
+ Log.d(TAG, "Wait for the dialog to appear");
+ waitAndAssert(device, By.pkg(PERMISSION_CONTROLLER_PKG));
+
+ if (expected != Status.TIMED_OUT) {
+ final String buttonId = expected == Status.APPROVED ? "button1" : "button2";
+
+ Log.d(TAG, "Find the button: " + buttonId);
+ final UiObject2 button = getAndAssert(device,
+ By.res("android", buttonId));
+
+ Log.d(TAG, "Click the button");
+ button.click();
+ }
+
+ Log.d(TAG, "Wait for approval response");
+ listener.waitForResponse(TIMEOUT);
+ Assert.assertEquals(expected, listener.getStatus());
+
+ // If we didn't click, cancel the request
+ if (expected == Status.TIMED_OUT) {
+ Log.d(TAG, "Canceling the request.");
+ cancelAuthorization(listener);
+ // We don't need to cancel again after the request
+ listener = null;
+ }
+
+ if (notification) {
+ Log.d(TAG, "Open the notification panel");
+ device.openNotification();
+
+ Log.d(TAG, "Find the notification");
+ final UiObject2 notifications = getAndAssert(device,
+ By.pkg("com.android.systemui")
+ .res("com.android.systemui:id/notification_stack_scroller"));
+
+ Log.d(TAG, "Wait for the notification to be cleared");
+ Assert.assertTrue(notifications.wait(Until.gone(
+ By.text(res.getString(
+ R.string.incident_report_notification_title))),
+ TIMEOUT));
+
+ Log.d(TAG, "Close the notification panel");
+ device.pressBack();
+ }
+
+ Log.d(TAG, "Wait for the dialog to be dismissed");
+ Assert.assertTrue(device.wait(Until.gone(
+ By.pkg(PERMISSION_CONTROLLER_PKG)),
+ TIMEOUT));
+
+ Log.d(TAG, String.format(
+ "---- END ---- notification=%-5s expected=%-9s ------------------",
+ notification, expected));
+ } finally {
+ // Clean up in case we failed somewhere along the way, junit doesn't actually
+ // crash the process, so we still do need to cancel the listener.
+ if (listener != null) {
+ cancelAuthorization(listener);
+ }
+ }
+ }
+
+ /**
+ * Test that an approval request is approved via a notification.
+ */
+ @Test
+ public void testApprovedWithNotification() throws Exception {
+ runTestFlow(true, Status.APPROVED);
+ }
+
+ /**
+ * Test that an approval request is denied via a notification.
+ */
+ @Test
+ public void testDeniedWithNotification() throws Exception {
+ runTestFlow(true, Status.DENIED);
+ }
+
+ /**
+ * Test that an approval request is neither approved nor denied without
+ * taking the action (in the timeout period... we can't wait forever).
+ */
+ @Test
+ public void testTimedOutWithNotification() throws Exception {
+ runTestFlow(true, Status.TIMED_OUT);
+ }
+
+ /**
+ * Test that an approval request is approved via a notification.
+ */
+ @Test
+ public void testApprovedWithDialog() throws Exception {
+ runTestFlow(false, Status.APPROVED);
+ }
+
+ /**
+ * Test that an approval request is denied via a notification.
+ */
+ @Test
+ public void testDeniedWithDialog() throws Exception {
+ runTestFlow(false, Status.DENIED);
+ }
+
+ /**
+ * Test that an approval request is neither approved nor denied without
+ * taking the action (in the timeout period... we can't wait forever).
+ */
+ @Test
+ public void testTimedOutWithDialog() throws Exception {
+ runTestFlow(false, Status.TIMED_OUT);
+ }
+
+ public static final void main(String[] args) {
+ Log.d(TAG, "RequestConfirmationTest args=" + java.util.Arrays.toString(args));
+ System.out.println("RequestConfirmationTest args=" + java.util.Arrays.toString(args));
+ }
+}
+