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.
+}