Check permissions before creating dynamic shortcuts.
Dynamic shortcuts is initialized before permissions may have been granted so
it needs to check permissions before running queries.
Test
Ran the following:
$ adb shell pm revoke com.android.contacts android.permission.READ_CONTACTS
$ adb shell pm revoke com.android.contacts android.permission.WRITE_CONTACTS
$ adb shell pm revoke com.android.contacts android.permission.GET_ACCOUNTS
$ adb shell pm revoke com.android.contacts android.permission.READ_PHONE_STATE
$ adb shell pm revoke com.android.contacts android.permission.READ_CALL_LOG
$ adb shell pm revoke com.android.contacts android.permission.CALL_PHONE
$ adb shell am instrument -w \
com.google.android.contacts.tests/android.support.test.runner.AndroidJUnitRunner \
-e class com.android.contacts.NoPermissionsLaunchSmokeTest
Bug 30189449
Change-Id: I3e7f865559d142c12f3b026a9d6aa2d7e1a1e5f9
diff --git a/src/com/android/contacts/DynamicShortcuts.java b/src/com/android/contacts/DynamicShortcuts.java
index c21f3a0..a7357b3 100644
--- a/src/com/android/contacts/DynamicShortcuts.java
+++ b/src/com/android/contacts/DynamicShortcuts.java
@@ -20,10 +20,13 @@
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
@@ -41,13 +44,16 @@
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.Experiments;
+import com.android.contacts.common.activity.RequestPermissionsActivity;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.common.util.PermissionsUtil;
import com.android.contactsbind.experiments.Flags;
import java.io.IOException;
@@ -131,6 +137,10 @@
@VisibleForTesting
void refresh() {
+ // Guard here in addition to initialize because this could be run by the JobScheduler
+ // after permissions are revoked (maybe)
+ if (!hasRequiredPermissions()) return;
+
final List<ShortcutInfo> shortcuts = getStrequentShortcuts();
mShortcutManager.setDynamicShortcuts(shortcuts);
if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -351,6 +361,11 @@
@VisibleForTesting
void handleFlagDisabled() {
+ removeAllShortcuts();
+ mJobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
+ }
+
+ private void removeAllShortcuts() {
mShortcutManager.removeAllDynamicShortcuts();
final List<ShortcutInfo> pinned = mShortcutManager.getPinnedShortcuts();
@@ -363,8 +378,6 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "DynamicShortcuts have been removed.");
}
-
- mJobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
}
@VisibleForTesting
@@ -385,6 +398,10 @@
mJobScheduler.schedule(job);
}
+ void updateInBackground() {
+ new ShortcutUpdateTask(this).execute();
+ }
+
public synchronized static void initialize(Context context) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
final Flags flags = Flags.getInstance(context);
@@ -402,10 +419,16 @@
if (!CompatUtils.isLauncherShortcutCompatible()) return;
final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
+
if (!Flags.getInstance(context).getBoolean(Experiments.DYNAMIC_SHORTCUTS)) {
// Clear dynamic shortcuts if the flag is not enabled. This prevents shortcuts from
// staying around if it is enabled then later disabled (due to bugs for instance).
shortcuts.handleFlagDisabled();
+ } else if (!shortcuts.hasRequiredPermissions()) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(RequestPermissionsActivity.BROADCAST_PERMISSIONS_GRANTED);
+ LocalBroadcastManager.getInstance(shortcuts.mContext).registerReceiver(
+ new PermissionsGrantedReceiver(), filter);
} else if (!isJobScheduled(context)) {
// Update the shortcuts. If the job is already scheduled then either the app is being
// launched to run the job in which case the shortcuts will get updated when it runs or
@@ -417,6 +440,23 @@
}
}
+ @VisibleForTesting
+ public static void reset(Context context) {
+ final JobScheduler jobScheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ jobScheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
+
+ if (!CompatUtils.isLauncherShortcutCompatible()) {
+ return;
+ }
+ new DynamicShortcuts(context).removeAllShortcuts();
+ }
+
+ @VisibleForTesting
+ boolean hasRequiredPermissions() {
+ return PermissionsUtil.hasContactsPermissions(mContext);
+ }
+
public static void updateFromJob(final JobService service, final JobParameters jobParams) {
new ShortcutUpdateTask(new DynamicShortcuts(service)) {
@Override
@@ -465,4 +505,13 @@
mDynamicShortcuts.scheduleUpdateJob();
}
}
+
+ private static class PermissionsGrantedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Clear the receiver.
+ LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
+ DynamicShortcuts.initialize(context);
+ }
+ }
}
diff --git a/src/com/android/contacts/common/activity/RequestPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
index 126cd64..4bbac73 100644
--- a/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
+++ b/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
@@ -22,6 +22,7 @@
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
import java.util.ArrayList;
@@ -32,6 +33,8 @@
*/
public class RequestPermissionsActivity extends RequestPermissionsActivityBase {
+ public static final String BROADCAST_PERMISSIONS_GRANTED = "broadcastPermissionsGranted";
+
private static String[] sRequiredPermissions;
@Override
@@ -76,6 +79,9 @@
startActivity(mPreviousActivityIntent);
finish();
overridePendingTransition(0, 0);
+
+ LocalBroadcastManager.getInstance(this).sendBroadcast(
+ new Intent(BROADCAST_PERMISSIONS_GRANTED));
} else {
Toast.makeText(this, R.string.missing_required_permission, Toast.LENGTH_SHORT).show();
finish();
diff --git a/tests/Android.mk b/tests/Android.mk
index 6b31593..46ab201 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -21,7 +21,9 @@
LOCAL_STATIC_JAVA_LIBRARIES += \
hamcrest-library \
- mockito-target-minus-junit4
+ mockito-target-minus-junit4 \
+ ub-uiautomator
+
LOCAL_AAPT_FLAGS := \
--auto-add-overlay \
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 65be28a..0c2cda7 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -93,7 +93,7 @@
<service android:name=".PhoneNumberTestService" />
</application>
- <instrumentation android:name="android.test.InstrumentationTestRunner"
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.contacts"
android:label="Contacts app tests">
</instrumentation>
diff --git a/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
new file mode 100644
index 0000000..a196ffa
--- /dev/null
+++ b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
@@ -0,0 +1,88 @@
+package com.android.contacts;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static com.android.contacts.common.util.PermissionsUtil.hasPermission;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Make sure the app doesn't crash when it is started without permissions. Note: this won't
+ * run in most environments because permissions will already have been granted.
+ *
+ * To exercise this run:
+ *
+ * $ adb shell pm revoke com.android.contacts android.permission.READ_CONTACTS
+ * $ adb shell pm revoke com.android.contacts android.permission.WRITE_CONTACTS
+ * $ adb shell pm revoke com.android.contacts android.permission.GET_ACCOUNTS
+ * $ adb shell pm revoke com.android.contacts android.permission.READ_PHONE_STATE
+ * $ adb shell pm revoke com.android.contacts android.permission.READ_CALL_LOG
+ * $ adb shell pm revoke com.android.contacts android.permission.CALL_PHONE
+ * $ adb shell am instrument -w \
+ * com.google.android.contacts.tests/android.support.test.runner.AndroidJUnitRunner \
+ * -e class com.android.contacts.NoPermissionsLaunchSmokeTest
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class NoPermissionsLaunchSmokeTest {
+ private static final long TIMEOUT = 5000;
+
+ private Context mTargetContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ assumeTrue(!hasPermission(mTargetContext, Manifest.permission.READ_CONTACTS));
+ assumeTrue(!hasPermission(mTargetContext, Manifest.permission.WRITE_CONTACTS));
+ assumeTrue(!hasPermission(mTargetContext, Manifest.permission.GET_ACCOUNTS));
+ assumeTrue(!hasPermission(mTargetContext, Manifest.permission.READ_PHONE_STATE));
+ assumeTrue(!hasPermission(mTargetContext, Manifest.permission.READ_CALL_LOG));
+ assumeTrue(!hasPermission(mTargetContext, Manifest.permission.CALL_PHONE));
+
+ // remove state that might exist outside of the app
+ // (e.g. launcher shortcuts and scheduled jobs)
+ DynamicShortcuts.reset(mTargetContext);
+ }
+
+ @Test
+ public void launchingMainActivityDoesntCrash() throws Exception {
+ final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+ // Launch the main activity
+ InstrumentationRegistry.getContext().startActivity(
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_DEFAULT)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()));
+
+ device.waitForIdle();
+
+ device.wait(Until.hasObject(By.textStartsWith("Allow Contacts")), TIMEOUT);
+ final UiObject2 grantContactsPermissionButton = device.findObject(By.text("ALLOW"));
+
+ grantContactsPermissionButton.click();
+
+ device.wait(Until.hasObject(By.textEndsWith("make and manage phone calls?")), TIMEOUT);
+
+ final UiObject2 grantPhonePermissionButton = device.findObject(By.text("ALLOW"));
+
+ grantPhonePermissionButton.clickAndWait(Until.newWindow(), TIMEOUT);
+
+ // Not sure if this actually waits for the load to complete or not.
+ device.waitForIdle();
+ }
+
+ // TODO: it would be good to have similar tests for other entry points that might be reached
+ // without required permissions.
+}