Improve Contacts' permission handling

Instead of repeatedly asking the user for the same permissions
over-and-over, instead only ask them once when they open an activity.
If they give us all the permissions we require, then allow them to continue.
If not, close the activity and show the user a toast.

Bug: 21791169
Change-Id: Idae927d525124ed8a7081bd5a8edbd24e1ba3190
diff --git a/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
index a4678f6..865f20b 100644
--- a/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
+++ b/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
@@ -1,89 +1,54 @@
+/*
+ * Copyright (C) 2015 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.contacts.common.activity;
 
 import android.Manifest.permission;
 import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.Trace;
 
 /**
  * Activity that requests permissions needed for ImportVCardActivity.
  */
-public class RequestImportVCardPermissionsActivity extends Activity {
-    public static final String PREVIOUS_ACTIVITY_INTENT = "previous_intent";
+public class RequestImportVCardPermissionsActivity extends RequestPermissionsActivityBase {
 
-    private static final int PERMISSIONS_REQUEST_ALL_PERMISSIONS = 1;
-    private static String[] sPermissions = new String[]{
+    private static final String[] REQUIRED_PERMISSIONS = new String[] {
             permission.READ_CONTACTS,
-            permission.WRITE_CONTACTS,
             permission.WRITE_EXTERNAL_STORAGE,
     };
 
-    private Intent mPreviousActivityIntent;
-
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        mPreviousActivityIntent = (Intent) getIntent().getExtras().get(PREVIOUS_ACTIVITY_INTENT);
-        requestPermissions();
+    protected String[] getRequiredPermissions() {
+        return REQUIRED_PERMISSIONS;
     }
 
+    @Override
+    protected String[] getDesiredPermissions() {
+        // Since this is used as an ostensible part of Dialer, lets be less pushy about asking for
+        // unnecessary permissions here.
+        return REQUIRED_PERMISSIONS;
+    }
+
+    /**
+     * If any permissions the Contacts app needs are missing, open an Activity
+     * to prompt the user for these permissions. Moreover, finish the current activity.
+     *
+     * This is designed to be called inside {@link android.app.Activity#onCreate}
+     */
     public static boolean startPermissionActivity(Activity activity) {
-        if (!hasPermissions(activity)) {
-            final Intent intent = new Intent(activity,
-                    RequestImportVCardPermissionsActivity.class);
-            intent.putExtra(PREVIOUS_ACTIVITY_INTENT, activity.getIntent());
-            activity.startActivity(intent);
-            activity.finish();
-            return true;
-        }
-        return false;
-    }
-
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, String permissions[],
-            int[] grantResults) {
-        if (isAllGranted(grantResults)) {
-            mPreviousActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-            startActivity(mPreviousActivityIntent);
-            finish();
-            overridePendingTransition(0, 0);
-        } else {
-            finish();
-        }
-    }
-
-    private boolean isAllGranted(int[] grantResult) {
-        for (int result : grantResult) {
-            if (result != PackageManager.PERMISSION_GRANTED) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private void requestPermissions() {
-        Trace.beginSection("requestPermissions");
-        requestPermissions(sPermissions, PERMISSIONS_REQUEST_ALL_PERMISSIONS);
-        Trace.endSection();
-    }
-
-    public static boolean hasPermissions(Context context) {
-        Trace.beginSection("hasPermission");
-        try {
-            for (String permission : sPermissions) {
-                if (context.checkSelfPermission(permission)
-                        != PackageManager.PERMISSION_GRANTED) {
-                    return false;
-                }
-            }
-            return true;
-        } finally {
-            Trace.endSection();
-        }
-
+        return startPermissionActivity(activity, REQUIRED_PERMISSIONS,
+                RequestImportVCardPermissionsActivity.class);
     }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/common/activity/RequestPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
index 1b18e10..d1d99cd 100644
--- a/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
+++ b/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
@@ -14,112 +14,43 @@
  * limitations under the License.
  */
 
-
 package com.android.contacts.common.activity;
 
 import android.Manifest.permission;
 import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.Trace;
 
 /**
- * Repeatedly ask the user for runtime permissions, until they grant all the permissions.
- * For now this is designed for activities used in Contacts. However, the ImportVCardActivity is
- * also used in the Dialer. When Dialer begins to support runtime permissions in their app, they
- * may wish to use a more targeted list of permissions or allow the user to reject using
- * some permissions.
- *
- * At the time of writing this Activity, most permissions cause crashes when not granted.
- * So it is risky to not possess them.
- *
+ * Activity that requests permissions needed for activities exported from Contacts.
  */
-public class RequestPermissionsActivity extends Activity {
-    public static final String PREVIOUS_ACTIVITY_INTENT = "previous_intent";
+public class RequestPermissionsActivity extends RequestPermissionsActivityBase {
 
-    private static final int PERMISSIONS_REQUEST_ALL_PERMISSIONS = 1;
-    private static String[] permissions = new String[]{
-            permission.ACCESS_FINE_LOCATION, // Location Group
-            permission.READ_CONTACTS, // Contacts group
-            permission.READ_CALL_LOG, // Permission group phone
-            permission.READ_CALENDAR, // Calendar group
-            permission.READ_SMS, // SMS group
-            permission.WRITE_EXTERNAL_STORAGE, // Storage group
+    private static final String[] REQUIRED_PERMISSIONS = new String[]{
+            // "Contacts" group. Without this permission, the Contacts app is useless.
+            permission.READ_CONTACTS,
+            // "Phone" group. This is only used in a few places such as QuickContactActivity and
+            // ImportExportDialogFragment. We could work around missing this permission with a bit
+            // of work.
+            permission.READ_CALL_LOG,
     };
 
-    private Intent mPreviousActivityIntent;
-
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        mPreviousActivityIntent = (Intent) getIntent().getExtras().get(PREVIOUS_ACTIVITY_INTENT);
-
-        if (savedInstanceState == null) {
-            requestPermissions();
-        }
+    protected String[] getRequiredPermissions() {
+        return REQUIRED_PERMISSIONS;
     }
 
-    /**
-     * If any permissions the Contacts app needs are missing, open an Activity
-     * to prompt the user for these permissions. Moreover, finish the current activity.
-     *
-     * This is designed to be called inside {@link android.app.Activity#onCreate}
-     */
+    @Override
+    protected String[] getDesiredPermissions() {
+        return new String[]{
+                permission.ACCESS_FINE_LOCATION, // Location Group
+                permission.READ_CONTACTS, // Contacts group
+                permission.READ_CALL_LOG, // Permission group phone
+                permission.READ_CALENDAR, // Calendar group
+                permission.READ_SMS, // SMS group
+                permission.WRITE_EXTERNAL_STORAGE, // Storage group
+        };
+    }
     public static boolean startPermissionActivity(Activity activity) {
-        if (!RequestPermissionsActivity.hasPermissions(activity)) {
-            final Intent intent = new Intent(activity,  RequestPermissionsActivity.class);
-            intent.putExtra(PREVIOUS_ACTIVITY_INTENT, activity.getIntent());
-            activity.startActivity(intent);
-            activity.finish();
-            return true;
-        }
-        return false;
-    }
-
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, String permissions[],
-            int[] grantResults) {
-        if (isAllGranted(grantResults)) {
-            mPreviousActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-            startActivity(mPreviousActivityIntent);
-            finish();
-            overridePendingTransition(0, 0);
-        } else {
-            requestPermissions();
-        }
-    }
-
-    private boolean isAllGranted(int[] grantResult) {
-        for (int result : grantResult) {
-            if (result != PackageManager.PERMISSION_GRANTED) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private void requestPermissions() {
-        Trace.beginSection("requestPermissions");
-        requestPermissions(permissions, PERMISSIONS_REQUEST_ALL_PERMISSIONS);
-        Trace.endSection();
-    }
-
-    public static boolean hasPermissions(Context context) {
-        Trace.beginSection("hasPermission");
-        try {
-            for (String permission : permissions) {
-                if (context.checkSelfPermission(permission)
-                        != PackageManager.PERMISSION_GRANTED) {
-                    return false;
-                }
-            }
-            return true;
-        } finally {
-            Trace.endSection();
-        }
-
+        return startPermissionActivity(activity, REQUIRED_PERMISSIONS,
+                RequestPermissionsActivity.class);
     }
 }
diff --git a/src/com/android/contacts/common/activity/RequestPermissionsActivityBase.java b/src/com/android/contacts/common/activity/RequestPermissionsActivityBase.java
new file mode 100644
index 0000000..6bb61f8
--- /dev/null
+++ b/src/com/android/contacts/common/activity/RequestPermissionsActivityBase.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2015 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.contacts.common.activity;
+
+import com.android.contacts.common.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Trace;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Activity that asks the user for all {@link #getDesiredPermissions} if any of
+ * {@link #getRequiredPermissions} are missing.
+ *
+ * NOTE: As a result of b/22095159, this can behave oddly in the case where the final permission
+ * you are requesting causes an application restart.
+ */
+public abstract class RequestPermissionsActivityBase extends Activity {
+    public static final String PREVIOUS_ACTIVITY_INTENT = "previous_intent";
+    private static final int PERMISSIONS_REQUEST_ALL_PERMISSIONS = 1;
+
+    /**
+     * @return list of permissions that are needed in order for {@link #PREVIOUS_ACTIVITY_INTENT} to
+     * operate. You only need to return a single permission per permission group you care about.
+     */
+    protected abstract String[] getRequiredPermissions();
+
+    /**
+     * @return list of permissions that would be useful for {@link #PREVIOUS_ACTIVITY_INTENT} to
+     * operate. You only need to return a single permission per permission group you care about.
+     */
+    protected abstract String[] getDesiredPermissions();
+
+    private Intent mPreviousActivityIntent;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mPreviousActivityIntent = (Intent) getIntent().getExtras().get(PREVIOUS_ACTIVITY_INTENT);
+
+        // Only start a requestPermissions() flow when first starting this activity the first time.
+        // The process is likely to be restarted during the permission flow (necessary to enable
+        // permissions) so this is important to track.
+        if (savedInstanceState == null) {
+            requestPermissions();
+        }
+    }
+
+    /**
+     * If any permissions the Contacts app needs are missing, open an Activity
+     * to prompt the user for these permissions. Moreover, finish the current activity.
+     *
+     * This is designed to be called inside {@link android.app.Activity#onCreate}
+     */
+    protected static boolean startPermissionActivity(Activity activity,
+            String[] requiredPermissions, Class<?> newActivityClass) {
+        if (!RequestPermissionsActivity.hasPermissions(activity, requiredPermissions)) {
+            final Intent intent = new Intent(activity,  newActivityClass);
+            intent.putExtra(PREVIOUS_ACTIVITY_INTENT, activity.getIntent());
+            activity.startActivity(intent);
+            activity.finish();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String permissions[],
+            int[] grantResults) {
+        if (isAllGranted(permissions, grantResults)) {
+            mPreviousActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            startActivity(mPreviousActivityIntent);
+            finish();
+            overridePendingTransition(0, 0);
+        } else {
+            Toast.makeText(this, R.string.missing_required_permission, Toast.LENGTH_SHORT).show();
+            finish();
+        }
+    }
+
+    private boolean isAllGranted(String permissions[], int[] grantResult) {
+        for (int i = 0; i < permissions.length; i++) {
+            if (grantResult[i] != PackageManager.PERMISSION_GRANTED
+                    && isPermissionRequired(permissions[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isPermissionRequired(String p) {
+        return Arrays.asList(getRequiredPermissions()).contains(p);
+    }
+
+    private void requestPermissions() {
+        Trace.beginSection("requestPermissions");
+        try {
+            // Construct a list of missing permissions
+            final ArrayList<String> unsatisfiedPermissions = new ArrayList<>();
+            for (String permission : getDesiredPermissions()) {
+                if (checkSelfPermission(permission)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    unsatisfiedPermissions.add(permission);
+                }
+            }
+            if (unsatisfiedPermissions.size() == 0) {
+                throw new RuntimeException("Request permission activity was called even"
+                        + " though all permissions are satisfied.");
+            }
+            requestPermissions(
+                    unsatisfiedPermissions.toArray(new String[unsatisfiedPermissions.size()]),
+                    PERMISSIONS_REQUEST_ALL_PERMISSIONS);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    protected static boolean hasPermissions(Context context, String[] permissions) {
+        Trace.beginSection("hasPermission");
+        try {
+            for (String permission : permissions) {
+                if (context.checkSelfPermission(permission)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    return false;
+                }
+            }
+            return true;
+        } finally {
+            Trace.endSection();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportVCardActivity.java b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
index b86845e..f956c7c 100644
--- a/src/com/android/contacts/common/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 
 import com.android.contacts.common.R;
+import com.android.contacts.common.activity.RequestImportVCardPermissionsActivity;
 
 import java.io.File;
 
@@ -151,6 +152,10 @@
     protected void onCreate(Bundle bundle) {
         super.onCreate(bundle);
 
+        if (RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
+            return;
+        }
+
         // Check directory is available.
         if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
             Log.w(LOG_TAG, "External storage is in state " + Environment.getExternalStorageState() +