OSDN Git Service

Minimum viable Android M runtime permissions handling for H.
authorzafir <zafir@google.com>
Mon, 29 Jun 2015 05:08:22 +0000 (00:08 -0500)
committerzafir <zafir@google.com>
Tue, 7 Jul 2015 01:20:51 +0000 (18:20 -0700)
Creates new activity for permissions handling: both checking
for permissions and handling error condition when critical
permissions are not present. The reason for creating a
new activity is so the app does not attempt to continue
executing OnCreate, OnResume etc, which opens
the camera while the dialogs are showing. This should
not slow the app down because the permissions activity
will only run when a) the first time the app has
insufficient permissions and b) when a critical
permission is missing and the app needs to shut down.

Bug: 21273463
Change-Id: I603acfb3057ba26b9cfa7935eb4cb24b5d547cb5

AndroidManifest.xml
res/values/strings.xml
src/com/android/camera/CameraActivity.java
src/com/android/camera/PermissionsActivity.java [new file with mode: 0644]
src/com/android/camera/settings/Keys.java

index 076863d..b8816a6 100644 (file)
@@ -6,7 +6,7 @@
 
     <uses-sdk
         android:minSdkVersion="19"
-        android:targetSdkVersion="21" />
+        android:targetSdkVersion="23" />
 
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
@@ -19,7 +19,6 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
     <uses-permission android:name="android.permission.NFC" />
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
                 android:resource="@layout/keyguard_widget" />
         </activity>
 
+        <activity
+                android:name="com.android.camera.PermissionsActivity"
+                android:label="@string/app_name"
+                android:parentActivityName="com.android.camera.CameraActivity" >
+            <meta-data
+                    android:name="android.support.PARENT_ACTIVITY"
+                    android:value="com.android.camera.CameraActivity" />
+        </activity>
+
         <activity-alias
             android:name="com.android.camera.CameraLauncher"
             android:label="@string/app_name"
index 1df8803..4e98a7a 100644 (file)
     <!-- message for the dialog showing that the user's photo could not be saved [CHAR LIMIT=NONE] -->
     <string name="error_media_storage_failure">There was a problem saving your photo or video.</string>
 
+    <!-- message for the dialog showing that the app does not have sufficient permissions [CHAR LIMIT=NONE] -->
+    <string name="error_permissions">The app does not have critical permissions needed to run. Please check your permissions settings.</string>
+
     <!-- Reason used for session failure which is visible in UI [CHAR LIMIT=NONE] -->
     <string name="reason_storage_failure">Photo storage failure.</string>
 
index a2c49c5..586a66d 100644 (file)
@@ -17,6 +17,7 @@
 
 package com.android.camera;
 
+import android.Manifest;
 import android.animation.Animator;
 import android.app.ActionBar;
 import android.app.Activity;
@@ -28,6 +29,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Matrix;
@@ -190,6 +192,9 @@ public class CameraActivity extends QuickActivity
     private static final long SCREEN_DELAY_MS = 2 * 60 * 1000; // 2 mins.
     /** Load metadata for 10 items ahead of our current. */
     private static final int FILMSTRIP_PRELOAD_AHEAD_ITEMS = 10;
+    private static final int PERMISSIONS_ACTIVITY_REQUEST_CODE = 1;
+    private static final int PERMISSIONS_RESULT_CODE_OK = 0;
+    private static final int PERMISSIONS_RESULT_CODE_FAILED = 1;
 
     /** Should be used wherever a context is needed. */
     private Context mAppContext;
@@ -245,6 +250,7 @@ public class CameraActivity extends QuickActivity
     private boolean mIsUndoingDeletion = false;
     private boolean mIsActivityRunning = false;
     private FatalErrorHandler mFatalErrorHandler;
+    private boolean mHasCriticalPermissions;
 
     private final Uri[] mNfcPushUris = new Uri[1];
 
@@ -1434,7 +1440,7 @@ public class CameraActivity extends QuickActivity
         mFeatureConfig = OneCameraFeatureConfigCreator.createDefault(getContentResolver(),
                 getServices().getMemoryManager());
         mFatalErrorHandler = new FatalErrorHandlerImpl(this);
-
+        checkPermissions();
         profile.mark();
         if (!Glide.isSetup()) {
             Context context = getAndroidContext();
@@ -1602,13 +1608,6 @@ public class CameraActivity extends QuickActivity
               new PhotoDataFactory());
         mVideoItemFactory = new VideoItemFactory(mAppContext, glideManager, appContentResolver,
               new VideoDataFactory());
-        mDataAdapter = new CameraFilmstripDataAdapter(mAppContext,
-              mPhotoItemFactory, mVideoItemFactory);
-        mDataAdapter.setLocalDataListener(mFilmstripItemListener);
-
-        mPreloader = new Preloader<Integer, AsyncTask>(FILMSTRIP_PRELOAD_AHEAD_ITEMS, mDataAdapter,
-                mDataAdapter);
-
         mCameraAppUI.getFilmstripContentPanel().setFilmstripListener(mFilmstripListener);
         if (mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL,
                                         Keys.KEY_SHOULD_SHOW_REFOCUS_VIEWER_CLING)) {
@@ -1623,45 +1622,6 @@ public class CameraActivity extends QuickActivity
         mCurrentModule.init(this, isSecureCamera(), isCaptureIntent());
         profile.mark("Init CurrentModule");
 
-        if (!mSecureCamera) {
-            mFilmstripController.setDataAdapter(mDataAdapter);
-            if (!isCaptureIntent()) {
-                mDataAdapter.requestLoad(new Callback<Void>() {
-                    @Override
-                    public void onCallback(Void result) {
-                        fillTemporarySessions();
-                    }
-                });
-            }
-        } else {
-            // Put a lock placeholder as the last image by setting its date to
-            // 0.
-            ImageView v = (ImageView) getLayoutInflater().inflate(
-                    R.layout.secure_album_placeholder, null);
-            v.setTag(R.id.mediadata_tag_viewtype, FilmstripItemType.SECURE_ALBUM_PLACEHOLDER.ordinal());
-            v.setOnClickListener(new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    UsageStatistics.instance().changeScreen(NavigationChange.Mode.GALLERY,
-                            NavigationChange.InteractionCause.BUTTON);
-                    startGallery();
-                    finish();
-                }
-            });
-            v.setContentDescription(getString(R.string.accessibility_unlock_to_camera));
-            mDataAdapter = new FixedLastProxyAdapter(
-                    mAppContext,
-                    mDataAdapter,
-                    new PlaceholderItem(
-                            v,
-                            FilmstripItemType.SECURE_ALBUM_PLACEHOLDER,
-                            v.getDrawable().getIntrinsicWidth(),
-                            v.getDrawable().getIntrinsicHeight()));
-            // Flush out all the original data.
-            mDataAdapter.clear();
-            mFilmstripController.setDataAdapter(mDataAdapter);
-        }
-
         setupNfcBeamPush();
 
         mLocalImagesObserver = new FilmstripContentObserver();
@@ -1857,7 +1817,9 @@ public class CameraActivity extends QuickActivity
         mLocalImagesObserver.setForegroundChangeListener(null);
         mLocalImagesObserver.setActivityPaused(true);
         mLocalVideosObserver.setActivityPaused(true);
-        mPreloader.cancelAllLoads();
+        if (mPreloader != null) {
+            mPreloader.cancelAllLoads();
+        }
         resetScreenOn();
 
         mMotionManager.stop();
@@ -1887,7 +1849,6 @@ public class CameraActivity extends QuickActivity
     @Override
     public void onResumeTasks() {
         mPaused = false;
-
         if (!mSecureCamera) {
             // Show the dialog if necessary. The rest resume logic will be invoked
             // at the onFirstRunStateReady() callback.
@@ -1905,11 +1866,99 @@ public class CameraActivity extends QuickActivity
         }
     }
 
+    /**
+     * Checks if any of the needed Android runtime permissions are missing.
+     * If they are, then launch the permissions activity under one of the following conditions:
+     * a) The permissions dialogs have not run yet. We will ask for permission only once.
+     * b) If the missing permissions are critical to the app running, we will display a fatal error dialog.
+     * Critical permissions are: camera, microphone and storage. The app cannot run without them.
+     * Non-critical permission is location.
+     */
+    private void checkPermissions() {
+
+        if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
+                checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED &&
+                checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+            mHasCriticalPermissions = true;
+        } else {
+            mHasCriticalPermissions = false;
+        }
+
+        if ((checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+                !mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL, Keys.KEY_HAS_SEEN_PERMISSIONS_DIALOGS)) ||
+                !mHasCriticalPermissions) {
+            Intent intent = new Intent(this, PermissionsActivity.class);
+            startActivityForResult(intent, PERMISSIONS_ACTIVITY_REQUEST_CODE);
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        // Close the app if critical permissions are missing.
+        if (requestCode == PERMISSIONS_ACTIVITY_REQUEST_CODE && resultCode == PERMISSIONS_RESULT_CODE_FAILED) {
+            finish();
+        }
+    }
+
+    private void preloadFilmstripItems() {
+        if (mDataAdapter == null) {
+            mDataAdapter = new CameraFilmstripDataAdapter(mAppContext,
+                    mPhotoItemFactory, mVideoItemFactory);
+            mDataAdapter.setLocalDataListener(mFilmstripItemListener);
+            mPreloader = new Preloader<Integer, AsyncTask>(FILMSTRIP_PRELOAD_AHEAD_ITEMS, mDataAdapter,
+                    mDataAdapter);
+            if (!mSecureCamera) {
+                mFilmstripController.setDataAdapter(mDataAdapter);
+                if (!isCaptureIntent()) {
+                    mDataAdapter.requestLoad(new Callback<Void>() {
+                        @Override
+                        public void onCallback(Void result) {
+                            fillTemporarySessions();
+                        }
+                    });
+                }
+            } else {
+                // Put a lock placeholder as the last image by setting its date to
+                // 0.
+                ImageView v = (ImageView) getLayoutInflater().inflate(
+                        R.layout.secure_album_placeholder, null);
+                v.setTag(R.id.mediadata_tag_viewtype, FilmstripItemType.SECURE_ALBUM_PLACEHOLDER.ordinal());
+                v.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        UsageStatistics.instance().changeScreen(NavigationChange.Mode.GALLERY,
+                                NavigationChange.InteractionCause.BUTTON);
+                        startGallery();
+                        finish();
+                    }
+                });
+                v.setContentDescription(getString(R.string.accessibility_unlock_to_camera));
+                mDataAdapter = new FixedLastProxyAdapter(
+                        mAppContext,
+                        mDataAdapter,
+                        new PlaceholderItem(
+                                v,
+                                FilmstripItemType.SECURE_ALBUM_PLACEHOLDER,
+                                v.getDrawable().getIntrinsicWidth(),
+                                v.getDrawable().getIntrinsicHeight()));
+                // Flush out all the original data.
+                mDataAdapter.clear();
+                mFilmstripController.setDataAdapter(mDataAdapter);
+            }
+        }
+    }
+
     private void resume() {
         Profile profile = mProfiler.create("CameraActivity.resume").start();
         CameraPerformanceTracker.onEvent(CameraPerformanceTracker.ACTIVITY_RESUME);
         Log.v(TAG, "Build info: " + Build.DISPLAY);
-
+        if (!mHasCriticalPermissions) {
+            Log.v(TAG, "Missing critical permissions.");
+            return;
+        }
+        preloadFilmstripItems();
         updateStorageSpaceAndHint(null);
 
         mLastLayoutOrientation = getResources().getConfiguration().orientation;
diff --git a/src/com/android/camera/PermissionsActivity.java b/src/com/android/camera/PermissionsActivity.java
new file mode 100644 (file)
index 0000000..af223d5
--- /dev/null
@@ -0,0 +1,183 @@
+package com.android.camera;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import com.android.camera.app.CameraServicesImpl;
+import com.android.camera.debug.Log;
+import com.android.camera.settings.Keys;
+import com.android.camera.settings.SettingsManager;
+import com.android.camera2.R;
+
+/**
+ * Activity that shows permissions request dialogs and handles lack of critical permissions.
+ */
+public class PermissionsActivity extends Activity {
+    private static final Log.Tag TAG = new Log.Tag("PermissionsActivity");
+
+    private static int PERMISSION_REQUEST_CODE = 1;
+    private static int RESULT_CODE_OK = 0;
+    private static int RESULT_CODE_FAILED = 1;
+
+    private int mIndexPermissionRequestCamera;
+    private int mIndexPermissionRequestMicrophone;
+    private int mIndexPermissionRequestLocation;
+    private int mIndexPermissionRequestStorage;
+    private boolean mShouldRequestCameraPermission;
+    private boolean mShouldRequestMicrophonePermission;
+    private boolean mShouldRequestLocationPermission;
+    private boolean mShouldRequestStoragePermission;
+    private int mNumPermissionsToRequest;
+    private boolean mFlagHasCameraPermission;
+    private boolean mFlagHasMicrophonePermission;
+    private boolean mFlagHasStoragePermission;
+    private SettingsManager mSettingsManager;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mSettingsManager = CameraServicesImpl.instance().getSettingsManager();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mNumPermissionsToRequest = 0;
+        checkPermissions();
+    }
+
+    private void checkPermissions() {
+        if (checkSelfPermission(Manifest.permission.CAMERA)
+                != PackageManager.PERMISSION_GRANTED) {
+            mNumPermissionsToRequest++;
+            mShouldRequestCameraPermission = true;
+        } else {
+            mFlagHasCameraPermission = true;
+        }
+
+        if (checkSelfPermission(Manifest.permission.RECORD_AUDIO)
+                != PackageManager.PERMISSION_GRANTED) {
+            mNumPermissionsToRequest++;
+            mShouldRequestMicrophonePermission = true;
+        } else {
+            mFlagHasMicrophonePermission = true;
+        }
+
+        if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+                != PackageManager.PERMISSION_GRANTED) {
+            mNumPermissionsToRequest++;
+            mShouldRequestStoragePermission = true;
+        } else {
+            mFlagHasStoragePermission = true;
+        }
+
+        if (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
+                != PackageManager.PERMISSION_GRANTED) {
+            mNumPermissionsToRequest++;
+            mShouldRequestLocationPermission = true;
+        }
+
+        if (mNumPermissionsToRequest != 0) {
+            if (!mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL,
+                    Keys.KEY_HAS_SEEN_PERMISSIONS_DIALOGS)) {
+                buildPermissionsRequest();
+            } else {
+                //Permissions dialog has already been shown, and we're still missing permissions.
+                handlePermissionsFailure();
+            }
+        } else {
+            handlePermissionsSuccess();
+        }
+    }
+
+    private void buildPermissionsRequest() {
+        String[] permissionsToRequest = new String[mNumPermissionsToRequest];
+        int permissionsRequestIndex = 0;
+
+        if (mShouldRequestCameraPermission) {
+            permissionsToRequest[permissionsRequestIndex] = Manifest.permission.CAMERA;
+            mIndexPermissionRequestCamera = permissionsRequestIndex;
+            permissionsRequestIndex++;
+        }
+        if (mShouldRequestMicrophonePermission) {
+            permissionsToRequest[permissionsRequestIndex] = Manifest.permission.RECORD_AUDIO;
+            mIndexPermissionRequestMicrophone = permissionsRequestIndex;
+            permissionsRequestIndex++;
+        }
+        if (mShouldRequestStoragePermission) {
+            permissionsToRequest[permissionsRequestIndex] = Manifest.permission.READ_EXTERNAL_STORAGE;
+            mIndexPermissionRequestStorage = permissionsRequestIndex;
+            permissionsRequestIndex++;
+        }
+        if (mShouldRequestLocationPermission) {
+            permissionsToRequest[permissionsRequestIndex] = Manifest.permission.ACCESS_COARSE_LOCATION;
+            mIndexPermissionRequestLocation = permissionsRequestIndex;
+        }
+
+        requestPermissions(permissionsToRequest, PERMISSION_REQUEST_CODE);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode,
+                                           String permissions[], int[] grantResults) {
+        mSettingsManager.set(
+                SettingsManager.SCOPE_GLOBAL,
+                Keys.KEY_HAS_SEEN_PERMISSIONS_DIALOGS,
+                true);
+
+        if (mShouldRequestCameraPermission) {
+            if (grantResults[mIndexPermissionRequestCamera] == PackageManager.PERMISSION_GRANTED) {
+                mFlagHasCameraPermission = true;
+            } else {
+                handlePermissionsFailure();
+            }
+        }
+        if (mShouldRequestMicrophonePermission) {
+            if (grantResults[mIndexPermissionRequestMicrophone] == PackageManager.PERMISSION_GRANTED) {
+                mFlagHasMicrophonePermission = true;
+            } else {
+                handlePermissionsFailure();
+            }
+        }
+        if (mShouldRequestStoragePermission) {
+            if (grantResults[mIndexPermissionRequestStorage] == PackageManager.PERMISSION_GRANTED) {
+                mFlagHasStoragePermission = true;
+            } else {
+                handlePermissionsFailure();
+            }
+        }
+
+        if (mShouldRequestLocationPermission) {
+            if (grantResults[mIndexPermissionRequestLocation] == PackageManager.PERMISSION_GRANTED) {
+                // Do nothing
+            } else {
+                // Do nothing
+            }
+        }
+
+        if (mFlagHasCameraPermission && mFlagHasMicrophonePermission && mFlagHasStoragePermission) {
+            handlePermissionsSuccess();
+        }
+    }
+
+    private void handlePermissionsSuccess() {
+        setResult(RESULT_CODE_OK, null);
+        finish();
+    }
+
+    private void handlePermissionsFailure() {
+        new AlertDialog.Builder(this).setTitle(getResources().getString(R.string.camera_error_title))
+                .setMessage(getResources().getString(R.string.error_permissions))
+                .setPositiveButton(getResources().getString(R.string.dialog_dismiss), new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        setResult(RESULT_CODE_FAILED, null);
+                        finish();
+                    }
+                })
+                .show();
+    }
+}
index 8712d4e..0339ea6 100644 (file)
@@ -80,6 +80,7 @@ public class Keys {
     public static final String KEY_HDR_PLUS_FLASH_MODE = "pref_hdr_plus_flash_mode";
     public static final String KEY_SHOULD_SHOW_SETTINGS_BUTTON_CLING =
             "pref_should_show_settings_button_cling";
+    public static final String KEY_HAS_SEEN_PERMISSIONS_DIALOGS = "pref_has_seen_permissions_dialogs";
 
     /**
      * Set some number of defaults for the defined keys.