From ad44cda82fe6ec5ee090115129223c6314f9e1bb Mon Sep 17 00:00:00 2001 From: zafir Date: Mon, 29 Jun 2015 00:08:22 -0500 Subject: Minimum viable Android M runtime permissions handling for H. 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 | 12 +- res/values/strings.xml | 3 + src/com/android/camera/CameraActivity.java | 149 ++++++++++++------- src/com/android/camera/PermissionsActivity.java | 183 ++++++++++++++++++++++++ src/com/android/camera/settings/Keys.java | 1 + 5 files changed, 296 insertions(+), 52 deletions(-) create mode 100644 src/com/android/camera/PermissionsActivity.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 076863de2..b8816a616 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -6,7 +6,7 @@ + android:targetSdkVersion="23" /> @@ -19,7 +19,6 @@ - @@ -71,6 +70,15 @@ android:resource="@layout/keyguard_widget" /> + + + + There was a problem saving your photo or video. + + The app does not have critical permissions needed to run. Please check your permissions settings. + Photo storage failure. diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java index a2c49c50b..586a66dd5 100644 --- a/src/com/android/camera/CameraActivity.java +++ b/src/com/android/camera/CameraActivity.java @@ -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(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() { - @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(FILMSTRIP_PRELOAD_AHEAD_ITEMS, mDataAdapter, + mDataAdapter); + if (!mSecureCamera) { + mFilmstripController.setDataAdapter(mDataAdapter); + if (!isCaptureIntent()) { + mDataAdapter.requestLoad(new Callback() { + @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 index 000000000..af223d53f --- /dev/null +++ b/src/com/android/camera/PermissionsActivity.java @@ -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(); + } +} diff --git a/src/com/android/camera/settings/Keys.java b/src/com/android/camera/settings/Keys.java index 8712d4ef7..0339ea6c7 100644 --- a/src/com/android/camera/settings/Keys.java +++ b/src/com/android/camera/settings/Keys.java @@ -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. -- cgit v1.2.3