/* * Copyright (C) 2012 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.camera; import android.Manifest; import android.animation.Animator; import android.app.ActionBar; import android.app.Activity; import android.app.Dialog; import android.app.KeyguardManager.KeyguardDismissCallback; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.nfc.NfcAdapter; import android.nfc.NfcAdapter.CreateBeamUrisCallback; import android.nfc.NfcEvent; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.provider.MediaStore; import android.provider.Settings; import android.text.TextUtils; import android.util.CameraPerformanceTracker; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnSystemUiVisibilityChangeListener; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ShareActionProvider; import com.android.camera.app.AppController; import com.android.camera.app.CameraAppUI; import com.android.camera.app.CameraController; import com.android.camera.app.CameraProvider; import com.android.camera.app.CameraServices; import com.android.camera.app.CameraServicesImpl; import com.android.camera.app.FirstRunDialog; import com.android.camera.app.LocationManager; import com.android.camera.app.MemoryManager; import com.android.camera.app.MemoryQuery; import com.android.camera.app.ModuleManager; import com.android.camera.app.ModuleManager.ModuleAgent; import com.android.camera.app.ModuleManagerImpl; import com.android.camera.app.MotionManager; import com.android.camera.app.OrientationManager; import com.android.camera.app.OrientationManagerImpl; import com.android.camera.data.CameraFilmstripDataAdapter; import com.android.camera.data.FilmstripContentObserver; import com.android.camera.data.FilmstripItem; import com.android.camera.data.FilmstripItemData; import com.android.camera.data.FilmstripItemType; import com.android.camera.data.FilmstripItemUtils; import com.android.camera.data.FixedLastProxyAdapter; import com.android.camera.data.GlideFilmstripManager; import com.android.camera.data.LocalFilmstripDataAdapter; import com.android.camera.data.LocalFilmstripDataAdapter.FilmstripItemListener; import com.android.camera.data.MediaDetails; import com.android.camera.data.MetadataLoader; import com.android.camera.data.PhotoDataFactory; import com.android.camera.data.PhotoItem; import com.android.camera.data.PhotoItemFactory; import com.android.camera.data.PlaceholderItem; import com.android.camera.data.SessionItem; import com.android.camera.data.VideoDataFactory; import com.android.camera.data.VideoItemFactory; import com.android.camera.debug.Log; import com.android.camera.device.ActiveCameraDeviceTracker; import com.android.camera.device.CameraId; import com.android.camera.filmstrip.FilmstripContentPanel; import com.android.camera.filmstrip.FilmstripController; import com.android.camera.module.ModuleController; import com.android.camera.module.ModulesInfo; import com.android.camera.one.OneCameraException; import com.android.camera.one.OneCameraManager; import com.android.camera.one.OneCameraModule; import com.android.camera.one.OneCameraOpener; import com.android.camera.one.config.OneCameraFeatureConfig; import com.android.camera.one.config.OneCameraFeatureConfigCreator; import com.android.camera.session.CaptureSession; import com.android.camera.session.CaptureSessionManager; import com.android.camera.session.CaptureSessionManager.SessionListener; import com.android.camera.settings.AppUpgrader; import com.android.camera.settings.CameraSettingsActivity; import com.android.camera.settings.Keys; import com.android.camera.settings.PictureSizeLoader; import com.android.camera.settings.ResolutionSetting; import com.android.camera.settings.ResolutionUtil; import com.android.camera.settings.SettingsManager; import com.android.camera.stats.UsageStatistics; import com.android.camera.stats.profiler.Profile; import com.android.camera.stats.profiler.Profiler; import com.android.camera.stats.profiler.Profilers; import com.android.camera.tinyplanet.TinyPlanetFragment; import com.android.camera.ui.AbstractTutorialOverlay; import com.android.camera.ui.DetailsDialog; import com.android.camera.ui.MainActivityLayout; import com.android.camera.ui.ModeListView; import com.android.camera.ui.ModeListView.ModeListVisibilityChangedListener; import com.android.camera.ui.PreviewStatusListener; import com.android.camera.util.ApiHelper; import com.android.camera.util.Callback; import com.android.camera.util.CameraUtil; import com.android.camera.util.GalleryHelper; import com.android.camera.util.GcamHelper; import com.android.camera.util.GoogleHelpHelper; import com.android.camera.util.IntentHelper; import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera.util.QuickActivity; import com.android.camera.util.ReleaseHelper; import com.android.camera.widget.FilmstripView; import com.android.camera.widget.Preloader; import com.android.camera2.R; import com.android.ex.camera2.portability.CameraAgent; import com.android.ex.camera2.portability.CameraAgentFactory; import com.android.ex.camera2.portability.CameraExceptionHandler; import com.android.ex.camera2.portability.CameraSettings; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.MemoryCategory; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor; import com.android.camera.exif.ExifInterface; import com.google.common.base.Optional; import com.google.common.logging.eventprotos; import com.google.common.logging.eventprotos.ForegroundEvent.ForegroundSource; import com.google.common.logging.eventprotos.MediaInteraction; import com.google.common.logging.eventprotos.NavigationChange; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class CameraActivity extends QuickActivity implements AppController, CameraAgent.CameraOpenCallback, ShareActionProvider.OnShareTargetSelectedListener { private static final Log.Tag TAG = new Log.Tag("CameraActivity"); private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE = "android.media.action.STILL_IMAGE_CAMERA_SECURE"; public static final String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE"; // The intent extra for camera from secure lock screen. True if the gallery // should only show newly captured pictures. sSecureAlbumId does not // increment. This is used when switching between camera, camcorder, and // panorama. If the extra is not set, it is in the normal camera mode. public static final String SECURE_CAMERA_EXTRA = "secure_camera"; private static final int MSG_CLEAR_SCREEN_ON_FLAG = 2; 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; /** Should be used wherever a context is needed. */ private Context mAppContext; /** * Camera fatal error handling: * 1) Present error dialog to guide users to exit the app. * 2) If users hit home button, onPause should just call finish() to exit the app. */ private boolean mCameraFatalError = false; /** * Whether onResume should reset the view to the preview. */ private boolean mResetToPreviewOnResume = true; /** * This data adapter is used by FilmStripView. */ private VideoItemFactory mVideoItemFactory; private PhotoItemFactory mPhotoItemFactory; private LocalFilmstripDataAdapter mDataAdapter; private ActiveCameraDeviceTracker mActiveCameraDeviceTracker; private OneCameraOpener mOneCameraOpener; private OneCameraManager mOneCameraManager; private SettingsManager mSettingsManager; private ResolutionSetting mResolutionSetting; private ModeListView mModeListView; private boolean mModeListVisible = false; private int mCurrentModeIndex; private CameraModule mCurrentModule; private ModuleManagerImpl mModuleManager; private FrameLayout mAboveFilmstripControlLayout; private FilmstripController mFilmstripController; private boolean mFilmstripVisible; /** Whether the filmstrip fully covers the preview. */ private boolean mFilmstripCoversPreview = false; private int mResultCodeForTesting; private Intent mResultDataForTesting; private OnScreenHint mStorageHint; private final Object mStorageSpaceLock = new Object(); private long mStorageSpaceBytes = Storage.LOW_STORAGE_THRESHOLD_BYTES; private boolean mAutoRotateScreen; private boolean mSecureCamera; private OrientationManagerImpl mOrientationManager; private LocationManager mLocationManager; private ButtonManager mButtonManager; private Handler mMainHandler; private PanoramaViewHelper mPanoramaViewHelper; private ActionBar mActionBar; private ViewGroup mUndoDeletionBar; private boolean mIsUndoingDeletion = false; private boolean mIsActivityRunning = false; private FatalErrorHandler mFatalErrorHandler; private boolean mHasCriticalPermissions; private final Uri[] mNfcPushUris = new Uri[1]; private FilmstripContentObserver mLocalImagesObserver; private FilmstripContentObserver mLocalVideosObserver; private boolean mPendingDeletion = false; private CameraController mCameraController; private boolean mPaused; private CameraAppUI mCameraAppUI; private Intent mGalleryIntent; private long mOnCreateTime; private Menu mActionBarMenu; private Preloader mPreloader; /** Can be used to play custom sounds. */ private SoundPlayer mSoundPlayer; /** Holds configuration for various OneCamera features. */ private OneCameraFeatureConfig mFeatureConfig; private static final int LIGHTS_OUT_DELAY_MS = 4000; private final int BASE_SYS_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; private final Runnable mLightsOutRunnable = new Runnable() { @Override public void run() { getWindow().getDecorView().setSystemUiVisibility( BASE_SYS_UI_VISIBILITY | View.SYSTEM_UI_FLAG_LOW_PROFILE); } }; private MemoryManager mMemoryManager; private MotionManager mMotionManager; private final Profiler mProfiler = Profilers.instance().guard(); /** First run dialog */ private FirstRunDialog mFirstRunDialog; @Override public CameraAppUI getCameraAppUI() { return mCameraAppUI; } @Override public ModuleManager getModuleManager() { return mModuleManager; } /** * Close activity when secure app passes lock screen or screen turns * off. */ private final BroadcastReceiver mShutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { finish(); } }; /** * Whether the screen is kept turned on. */ private boolean mKeepScreenOn; private int mLastLayoutOrientation; private final CameraAppUI.BottomPanel.Listener mMyFilmstripBottomControlListener = new CameraAppUI.BottomPanel.Listener() { /** * If the current photo is a photo sphere, this will launch the * Photo Sphere panorama viewer. */ @Override public void onExternalViewer() { if (mPanoramaViewHelper == null) { return; } final FilmstripItem data = getCurrentLocalData(); if (data == null) { Log.w(TAG, "Cannot open null data."); return; } final Uri contentUri = data.getData().getUri(); if (contentUri == Uri.EMPTY) { Log.w(TAG, "Cannot open empty URL."); return; } if (data.getMetadata().isUsePanoramaViewer()) { mPanoramaViewHelper.showPanorama(CameraActivity.this, contentUri); } else if (data.getMetadata().isHasRgbzData()) { mPanoramaViewHelper.showRgbz(contentUri); if (mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL, Keys.KEY_SHOULD_SHOW_REFOCUS_VIEWER_CLING)) { mSettingsManager.set(SettingsManager.SCOPE_GLOBAL, Keys.KEY_SHOULD_SHOW_REFOCUS_VIEWER_CLING, false); mCameraAppUI.clearClingForViewer( CameraAppUI.BottomPanel.VIEWER_REFOCUS); } } } @Override public void onEdit() { FilmstripItem data = getCurrentLocalData(); if (data == null) { Log.w(TAG, "Cannot edit null data."); return; } final int currentDataId = getCurrentDataId(); UsageStatistics.instance().mediaInteraction(fileNameFromAdapterAtIndex( currentDataId), MediaInteraction.InteractionType.EDIT, NavigationChange.InteractionCause.BUTTON, fileAgeFromAdapterAtIndex(currentDataId)); launchEditor(data); } @Override public void onTinyPlanet() { FilmstripItem data = getCurrentLocalData(); if (data == null) { Log.w(TAG, "Cannot edit tiny planet on null data."); return; } launchTinyPlanetEditor(data); } @Override public void onDelete() { final int currentDataId = getCurrentDataId(); UsageStatistics.instance().mediaInteraction(fileNameFromAdapterAtIndex( currentDataId), MediaInteraction.InteractionType.DELETE, NavigationChange.InteractionCause.BUTTON, fileAgeFromAdapterAtIndex(currentDataId)); removeItemAt(currentDataId); } @Override public void onShare() { final FilmstripItem data = getCurrentLocalData(); if (data == null) { Log.w(TAG, "Cannot share null data."); return; } final int currentDataId = getCurrentDataId(); UsageStatistics.instance().mediaInteraction(fileNameFromAdapterAtIndex( currentDataId), MediaInteraction.InteractionType.SHARE, NavigationChange.InteractionCause.BUTTON, fileAgeFromAdapterAtIndex(currentDataId)); // If applicable, show release information before this item // is shared. if (ReleaseHelper.shouldShowReleaseInfoDialogOnShare(data)) { ReleaseHelper.showReleaseInfoDialog(CameraActivity.this, new Callback() { @Override public void onCallback(Void result) { share(data); } }); } else { share(data); } } private void share(FilmstripItem data) { Intent shareIntent = getShareIntentByData(data); if (shareIntent != null) { try { launchActivityByIntent(shareIntent); mCameraAppUI.getFilmstripBottomControls().setShareEnabled(false); } catch (ActivityNotFoundException ex) { // Nothing. } } } private int getCurrentDataId() { return mFilmstripController.getCurrentAdapterIndex(); } private FilmstripItem getCurrentLocalData() { return mDataAdapter.getItemAt(getCurrentDataId()); } /** * Sets up the share intent and NFC properly according to the * data. * * @param item The data to be shared. */ private Intent getShareIntentByData(final FilmstripItem item) { Intent intent = null; final Uri contentUri = item.getData().getUri(); final String msgShareTo = getResources().getString(R.string.share_to); if (item.getMetadata().isPanorama360() && item.getData().getUri() != Uri.EMPTY) { intent = new Intent(Intent.ACTION_SEND); intent.setType(FilmstripItemData.MIME_TYPE_PHOTOSPHERE); intent.putExtra(Intent.EXTRA_STREAM, contentUri); } else if (item.getAttributes().canShare()) { final String mimeType = item.getData().getMimeType(); intent = getShareIntentFromType(mimeType); if (intent != null) { intent.putExtra(Intent.EXTRA_STREAM, contentUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } intent = Intent.createChooser(intent, msgShareTo); } return intent; } /** * Get the share intent according to the mimeType * * @param mimeType The mimeType of current data. * @return the video/image's ShareIntent or null if mimeType is * invalid. */ private Intent getShareIntentFromType(String mimeType) { // Lazily create the intent object. Intent intent = new Intent(Intent.ACTION_SEND); if (mimeType.startsWith("video/")) { intent.setType("video/*"); } else { if (mimeType.startsWith("image/")) { intent.setType("image/*"); } else { Log.w(TAG, "unsupported mimeType " + mimeType); } } return intent; } @Override public void onProgressErrorClicked() { FilmstripItem data = getCurrentLocalData(); getServices().getCaptureSessionManager().removeErrorMessage( data.getData().getUri()); updateBottomControlsByData(data); } }; @Override public void onCameraOpened(CameraAgent.CameraProxy camera) { Log.v(TAG, "onCameraOpened"); if (mPaused) { // We've paused, but just asynchronously opened the camera. Close it // because we should be releasing the camera when paused to allow // other apps to access it. Log.v(TAG, "received onCameraOpened but activity is paused, closing Camera"); mCameraController.closeCamera(false); return; } if (!mModuleManager.getModuleAgent(mCurrentModeIndex).requestAppForCamera()) { // We shouldn't be here. Just close the camera and leave. mCameraController.closeCamera(false); throw new IllegalStateException("Camera opened but the module shouldn't be " + "requesting"); } if (mCurrentModule != null) { resetExposureCompensationToDefault(camera); try { mCurrentModule.onCameraAvailable(camera); } catch (RuntimeException ex) { Log.e(TAG, "Error connecting to camera", ex); mFatalErrorHandler.onCameraOpenFailure(); } } else { Log.v(TAG, "mCurrentModule null, not invoking onCameraAvailable"); } Log.v(TAG, "invoking onChangeCamera"); mCameraAppUI.onChangeCamera(); } private void resetExposureCompensationToDefault(CameraAgent.CameraProxy camera) { // Reset the exposure compensation before handing the camera to module. CameraSettings cameraSettings = camera.getSettings(); cameraSettings.setExposureCompensationIndex(0); camera.applySettings(cameraSettings); } @Override public void onCameraDisabled(int cameraId) { Log.w(TAG, "Camera disabled: " + cameraId); mFatalErrorHandler.onCameraDisabledFailure(); } @Override public void onDeviceOpenFailure(int cameraId, String info) { Log.w(TAG, "Camera open failure: " + info); mFatalErrorHandler.onCameraOpenFailure(); } @Override public void onDeviceOpenedAlready(int cameraId, String info) { Log.w(TAG, "Camera open already: " + cameraId + "," + info); mFatalErrorHandler.onGenericCameraAccessFailure(); } @Override public void onReconnectionFailure(CameraAgent mgr, String info) { Log.w(TAG, "Camera reconnection failure:" + info); mFatalErrorHandler.onCameraReconnectFailure(); } private static class MainHandler extends Handler { final WeakReference mActivity; public MainHandler(CameraActivity activity, Looper looper) { super(looper); mActivity = new WeakReference(activity); } @Override public void handleMessage(Message msg) { CameraActivity activity = mActivity.get(); if (activity == null) { return; } switch (msg.what) { case MSG_CLEAR_SCREEN_ON_FLAG: { if (!activity.mPaused) { activity.getWindow().clearFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } break; } } } } private String fileNameFromAdapterAtIndex(int index) { final FilmstripItem filmstripItem = mDataAdapter.getItemAt(index); if (filmstripItem == null) { return ""; } File localFile = new File(filmstripItem.getData().getFilePath()); return localFile.getName(); } private float fileAgeFromAdapterAtIndex(int index) { final FilmstripItem filmstripItem = mDataAdapter.getItemAt(index); if (filmstripItem == null) { return 0; } File localFile = new File(filmstripItem.getData().getFilePath()); return 0.001f * (System.currentTimeMillis() - localFile.lastModified()); } private final FilmstripContentPanel.Listener mFilmstripListener = new FilmstripContentPanel.Listener() { @Override public void onSwipeOut() { } @Override public void onSwipeOutBegin() { mActionBar.hide(); mCameraAppUI.hideBottomControls(); mFilmstripCoversPreview = false; updatePreviewVisibility(); } @Override public void onFilmstripHidden() { mFilmstripVisible = false; UsageStatistics.instance().changeScreen(currentUserInterfaceMode(), NavigationChange.InteractionCause.SWIPE_RIGHT); CameraActivity.this.setFilmstripUiVisibility(false); // When the user hide the filmstrip (either swipe out or // tap on back key) we move to the first item so next time // when the user swipe in the filmstrip, the most recent // one is shown. mFilmstripController.goToFirstItem(); } @Override public void onFilmstripShown() { mFilmstripVisible = true; mCameraAppUI.hideCaptureIndicator(); UsageStatistics.instance().changeScreen(currentUserInterfaceMode(), NavigationChange.InteractionCause.SWIPE_LEFT); updateUiByData(mFilmstripController.getCurrentAdapterIndex()); } @Override public void onFocusedDataLongPressed(int adapterIndex) { // Do nothing. } @Override public void onFocusedDataPromoted(int adapterIndex) { UsageStatistics.instance().mediaInteraction(fileNameFromAdapterAtIndex( adapterIndex), MediaInteraction.InteractionType.DELETE, NavigationChange.InteractionCause.SWIPE_UP, fileAgeFromAdapterAtIndex( adapterIndex)); removeItemAt(adapterIndex); } @Override public void onFocusedDataDemoted(int adapterIndex) { UsageStatistics.instance().mediaInteraction(fileNameFromAdapterAtIndex( adapterIndex), MediaInteraction.InteractionType.DELETE, NavigationChange.InteractionCause.SWIPE_DOWN, fileAgeFromAdapterAtIndex(adapterIndex)); removeItemAt(adapterIndex); } @Override public void onEnterFullScreenUiShown(int adapterIndex) { if (mFilmstripVisible) { CameraActivity.this.setFilmstripUiVisibility(true); } } @Override public void onLeaveFullScreenUiShown(int adapterIndex) { // Do nothing. } @Override public void onEnterFullScreenUiHidden(int adapterIndex) { if (mFilmstripVisible) { CameraActivity.this.setFilmstripUiVisibility(false); } } @Override public void onLeaveFullScreenUiHidden(int adapterIndex) { // Do nothing. } @Override public void onEnterFilmstrip(int adapterIndex) { if (mFilmstripVisible) { CameraActivity.this.setFilmstripUiVisibility(true); } } @Override public void onLeaveFilmstrip(int adapterIndex) { // Do nothing. } @Override public void onDataReloaded() { if (!mFilmstripVisible) { return; } updateUiByData(mFilmstripController.getCurrentAdapterIndex()); } @Override public void onDataUpdated(int adapterIndex) { if (!mFilmstripVisible) { return; } updateUiByData(mFilmstripController.getCurrentAdapterIndex()); } @Override public void onEnterZoomView(int adapterIndex) { if (mFilmstripVisible) { CameraActivity.this.setFilmstripUiVisibility(false); } } @Override public void onZoomAtIndexChanged(int adapterIndex, float zoom) { final FilmstripItem filmstripItem = mDataAdapter.getItemAt(adapterIndex); long ageMillis = System.currentTimeMillis() - filmstripItem.getData().getLastModifiedDate().getTime(); // Do not log if items is to old or does not have a path (which is // being used as a key). if (TextUtils.isEmpty(filmstripItem.getData().getFilePath()) || ageMillis > UsageStatistics.VIEW_TIMEOUT_MILLIS) { return; } File localFile = new File(filmstripItem.getData().getFilePath()); UsageStatistics.instance().mediaView(localFile.getName(), filmstripItem.getData().getLastModifiedDate().getTime(), zoom); } @Override public void onDataFocusChanged(final int prevIndex, final int newIndex) { if (!mFilmstripVisible) { return; } // TODO: This callback is UI event callback, should always // happen on UI thread. Find the reason for this // runOnUiThread() and fix it. runOnUiThread(new Runnable() { @Override public void run() { updateUiByData(newIndex); } }); } @Override public void onScroll(int firstVisiblePosition, int visibleItemCount, int totalItemCount) { mPreloader.onScroll(null /*absListView*/, firstVisiblePosition, visibleItemCount, totalItemCount); } }; private final FilmstripItemListener mFilmstripItemListener = new FilmstripItemListener() { @Override public void onMetadataUpdated(List indexes) { if (mPaused) { // Callback after the activity is paused. return; } int currentIndex = mFilmstripController.getCurrentAdapterIndex(); for (Integer index : indexes) { if (index == currentIndex) { updateUiByData(index); // Currently we have only 1 data can be matched. // No need to look for more, break. break; } } } }; public void gotoGallery() { UsageStatistics.instance().changeScreen(NavigationChange.Mode.FILMSTRIP, NavigationChange.InteractionCause.BUTTON); mFilmstripController.goToNextItem(); } /** * If 'visible' is false, this hides the action bar. Also maintains * lights-out at all times. * * @param visible is false, this hides the action bar and filmstrip bottom * controls. */ private void setFilmstripUiVisibility(boolean visible) { mLightsOutRunnable.run(); mCameraAppUI.getFilmstripBottomControls().setVisible(visible); if (visible != mActionBar.isShowing()) { if (visible) { mActionBar.show(); mCameraAppUI.showBottomControls(); } else { mActionBar.hide(); mCameraAppUI.hideBottomControls(); } } mFilmstripCoversPreview = visible; updatePreviewVisibility(); } private void hideSessionProgress() { mCameraAppUI.getFilmstripBottomControls().hideProgress(); } private void showSessionProgress(int messageId) { CameraAppUI.BottomPanel controls = mCameraAppUI.getFilmstripBottomControls(); controls.setProgressText(messageId > 0 ? getString(messageId) : ""); controls.hideControls(); controls.hideProgressError(); controls.showProgress(); } private void showProcessError(int messageId) { mCameraAppUI.getFilmstripBottomControls().showProgressError( messageId > 0 ? getString(messageId) : ""); } private void updateSessionProgress(int progress) { mCameraAppUI.getFilmstripBottomControls().setProgress(progress); } private void updateSessionProgressText(int messageId) { mCameraAppUI.getFilmstripBottomControls().setProgressText( messageId > 0 ? getString(messageId) : ""); } // Candidate for deletion as Android Beam is deprecated in Android Q private void setupNfcBeamPush() { NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mAppContext); if (adapter == null) { return; } if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) { // Disable beaming adapter.setNdefPushMessage(null, CameraActivity.this); return; } adapter.setBeamPushUris(null, CameraActivity.this); adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() { @Override public Uri[] createBeamUris(NfcEvent event) { return mNfcPushUris; } }, CameraActivity.this); } @Override public boolean onShareTargetSelected(ShareActionProvider shareActionProvider, Intent intent) { int currentIndex = mFilmstripController.getCurrentAdapterIndex(); if (currentIndex < 0) { return false; } UsageStatistics.instance().mediaInteraction(fileNameFromAdapterAtIndex(currentIndex), MediaInteraction.InteractionType.SHARE, NavigationChange.InteractionCause.BUTTON, fileAgeFromAdapterAtIndex(currentIndex)); // TODO add intent.getComponent().getPackageName() return true; } // Note: All callbacks come back on the main thread. private final SessionListener mSessionListener = new SessionListener() { @Override public void onSessionQueued(final Uri uri) { Log.v(TAG, "onSessionQueued: " + uri); if (!Storage.instance().isSessionUri(uri)) { return; } Optional newData = SessionItem.create(getApplicationContext(), uri); if (newData.isPresent()) { mDataAdapter.addOrUpdate(newData.get()); } } @Override public void onSessionUpdated(Uri uri) { Log.v(TAG, "onSessionUpdated: " + uri); mDataAdapter.refresh(uri); } @Override public void onSessionDone(final Uri sessionUri) { Log.v(TAG, "onSessionDone:" + sessionUri); Uri contentUri = Storage.instance().getContentUriForSessionUri(sessionUri); if (contentUri == null) { mDataAdapter.refresh(sessionUri); return; } PhotoItem newData = mPhotoItemFactory.queryContentUri(contentUri); // This can be null if e.g. a session is canceled (e.g. // through discard panorama). It might be worth adding // onSessionCanceled or the like this interface. if (newData == null) { Log.i(TAG, "onSessionDone: Could not find LocalData for URI: " + contentUri); return; } final int pos = mDataAdapter.findByContentUri(sessionUri); if (pos == -1) { // We do not have a placeholder for this image, perhaps // due to the activity crashing or being killed. mDataAdapter.addOrUpdate(newData); } else { // Make the PhotoItem aware of the session placeholder, to // allow it to make a smooth transition to its content if it // the session item is currently visible. FilmstripItem oldSessionData = mDataAdapter.getFilmstripItemAt(pos); if (mCameraAppUI.getFilmstripVisibility() == View.VISIBLE && mFilmstripController.isVisible(oldSessionData)) { Log.v(TAG, "session item visible, setting transition placeholder"); newData.setSessionPlaceholderBitmap( Storage.instance().getPlaceholderForSession(sessionUri)); } mDataAdapter.updateItemAt(pos, newData); } } @Override public void onSessionProgress(final Uri uri, final int progress) { if (progress < 0) { // Do nothing, there is no task for this URI. return; } int currentIndex = mFilmstripController.getCurrentAdapterIndex(); if (currentIndex == -1) { return; } if (uri.equals( mDataAdapter.getItemAt(currentIndex).getData().getUri())) { updateSessionProgress(progress); } } @Override public void onSessionProgressText(final Uri uri, final int messageId) { int currentIndex = mFilmstripController.getCurrentAdapterIndex(); if (currentIndex == -1) { return; } if (uri.equals( mDataAdapter.getItemAt(currentIndex).getData().getUri())) { updateSessionProgressText(messageId); } } @Override public void onSessionCaptureIndicatorUpdate(Bitmap indicator, int rotationDegrees) { // Don't show capture indicator in Photo Sphere. final int photosphereModuleId = getApplicationContext().getResources() .getInteger( R.integer.camera_mode_photosphere); if (mCurrentModeIndex == photosphereModuleId) { return; } indicateCapture(indicator, rotationDegrees); } @Override public void onSessionFailed(Uri uri, int failureMessageId, boolean removeFromFilmstrip) { Log.v(TAG, "onSessionFailed:" + uri); int failedIndex = mDataAdapter.findByContentUri(uri); int currentIndex = mFilmstripController.getCurrentAdapterIndex(); if (currentIndex == failedIndex) { updateSessionProgress(0); showProcessError(failureMessageId); mDataAdapter.refresh(uri); } if (removeFromFilmstrip) { mFatalErrorHandler.onMediaStorageFailure(); mDataAdapter.removeAt(failedIndex); } } @Override public void onSessionCanceled(Uri uri) { Log.v(TAG, "onSessionCanceled:" + uri); int failedIndex = mDataAdapter.findByContentUri(uri); mDataAdapter.removeAt(failedIndex); } @Override public void onSessionThumbnailUpdate(Bitmap bitmap) { } @Override public void onSessionPictureDataUpdate(byte[] pictureData, int orientation) { } }; @Override public Context getAndroidContext() { return mAppContext; } @Override public OneCameraFeatureConfig getCameraFeatureConfig() { return mFeatureConfig; } @Override public Dialog createDialog() { return new Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen); } @Override public void launchActivityByIntent(Intent intent) { // Starting from L, we prefer not to start edit activity within camera's task. mResetToPreviewOnResume = false; intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); startActivity(intent); } @Override public int getCurrentModuleIndex() { return mCurrentModeIndex; } @Override public String getModuleScope() { ModuleAgent agent = mModuleManager.getModuleAgent(mCurrentModeIndex); return SettingsManager.getModuleSettingScope(agent.getScopeNamespace()); } @Override public String getCameraScope() { // if an unopen camera i.e. negative ID is returned, which we've observed in // some automated scenarios, just return it as a valid separate scope // this could cause user issues, so log a stack trace noting the call path // which resulted in this scenario. CameraId cameraId = mCameraController.getCurrentCameraId(); if(cameraId == null) { Log.e(TAG, "Retrieving Camera Setting Scope with -1"); return SettingsManager.getCameraSettingScope("-1"); } return SettingsManager.getCameraSettingScope(cameraId.getValue()); } @Override public ModuleController getCurrentModuleController() { return mCurrentModule; } @Override public int getQuickSwitchToModuleId(int currentModuleIndex) { return mModuleManager.getQuickSwitchToModuleId(currentModuleIndex, mSettingsManager, mAppContext); } @Override public SurfaceTexture getPreviewBuffer() { // TODO: implement this return null; } @Override public void onPreviewReadyToStart() { mCameraAppUI.onPreviewReadyToStart(); } @Override public void onPreviewStarted() { mCameraAppUI.onPreviewStarted(); } @Override public void addPreviewAreaSizeChangedListener( PreviewStatusListener.PreviewAreaChangedListener listener) { mCameraAppUI.addPreviewAreaChangedListener(listener); } @Override public void removePreviewAreaSizeChangedListener( PreviewStatusListener.PreviewAreaChangedListener listener) { mCameraAppUI.removePreviewAreaChangedListener(listener); } @Override public void setupOneShotPreviewListener() { mCameraController.setOneShotPreviewCallback(mMainHandler, new CameraAgent.CameraPreviewDataCallback() { @Override public void onPreviewFrame(byte[] data, CameraAgent.CameraProxy camera) { mCurrentModule.onPreviewInitialDataReceived(); mCameraAppUI.onNewPreviewFrame(); } } ); } @Override public void updatePreviewAspectRatio(float aspectRatio) { mCameraAppUI.updatePreviewAspectRatio(aspectRatio); } @Override public void updatePreviewTransformFullscreen(Matrix matrix, float aspectRatio) { mCameraAppUI.updatePreviewTransformFullscreen(matrix, aspectRatio); } @Override public RectF getFullscreenRect() { return mCameraAppUI.getFullscreenRect(); } @Override public void updatePreviewTransform(Matrix matrix) { mCameraAppUI.updatePreviewTransform(matrix); } @Override public void setPreviewStatusListener(PreviewStatusListener previewStatusListener) { mCameraAppUI.setPreviewStatusListener(previewStatusListener); } @Override public FrameLayout getModuleLayoutRoot() { return mCameraAppUI.getModuleRootView(); } @Override public void setShutterEventsListener(ShutterEventsListener listener) { // TODO: implement this } @Override public void setShutterEnabled(boolean enabled) { mCameraAppUI.setShutterButtonEnabled(enabled); } @Override public boolean isShutterEnabled() { return mCameraAppUI.isShutterButtonEnabled(); } @Override public void startFlashAnimation(boolean shortFlash) { mCameraAppUI.startFlashAnimation(shortFlash); } @Override public void startPreCaptureAnimation() { // TODO: implement this } @Override public void cancelPreCaptureAnimation() { // TODO: implement this } @Override public void startPostCaptureAnimation() { // TODO: implement this } @Override public void startPostCaptureAnimation(Bitmap thumbnail) { // TODO: implement this } @Override public void cancelPostCaptureAnimation() { // TODO: implement this } @Override public OrientationManager getOrientationManager() { return mOrientationManager; } @Override public LocationManager getLocationManager() { return mLocationManager; } @Override public void lockOrientation() { if (mOrientationManager != null) { mOrientationManager.lockOrientation(); } } @Override public void unlockOrientation() { if (mOrientationManager != null) { mOrientationManager.unlockOrientation(); } } /** * If not in filmstrip, this shows the capture indicator. */ private void indicateCapture(final Bitmap indicator, final int rotationDegrees) { if (mFilmstripVisible) { return; } // Don't show capture indicator in Photo Sphere. // TODO: Don't reach into resources to figure out the current mode. final int photosphereModuleId = getApplicationContext().getResources().getInteger( R.integer.camera_mode_photosphere); if (mCurrentModeIndex == photosphereModuleId) { return; } mMainHandler.post(new Runnable() { @Override public void run() { mCameraAppUI.startCaptureIndicatorRevealAnimation(mCurrentModule .getPeekAccessibilityString()); mCameraAppUI.updateCaptureIndicatorThumbnail(indicator, rotationDegrees); } }); } @Override public void notifyNewMedia(Uri uri) { // TODO: This method is running on the main thread. Also we should get // rid of that AsyncTask. updateStorageSpaceAndHint(null); ContentResolver cr = getContentResolver(); String mimeType = cr.getType(uri); FilmstripItem newData = null; if (FilmstripItemUtils.isMimeTypeVideo(mimeType)) { sendBroadcast(new Intent(CameraUtil.ACTION_NEW_VIDEO, uri)); newData = mVideoItemFactory.queryContentUri(uri); if (newData == null) { Log.e(TAG, "Can't find video data in content resolver:" + uri); return; } } else if (FilmstripItemUtils.isMimeTypeImage(mimeType)) { CameraUtil.broadcastNewPicture(mAppContext, uri); newData = mPhotoItemFactory.queryContentUri(uri); if (newData == null) { Log.e(TAG, "Can't find photo data in content resolver:" + uri); return; } } else { Log.w(TAG, "Unknown new media with MIME type:" + mimeType + ", uri:" + uri); return; } // We are preloading the metadata for new video since we need the // rotation info for the thumbnail. new AsyncTask() { @Override protected FilmstripItem doInBackground(FilmstripItem... params) { FilmstripItem data = params[0]; MetadataLoader.loadMetadata(getAndroidContext(), data); return data; } @Override protected void onPostExecute(final FilmstripItem data) { // TODO: Figure out why sometimes the data is aleady there. mDataAdapter.addOrUpdate(data); // Legacy modules don't use CaptureSession, so we show the capture indicator when // the item was safed. if (mCurrentModule instanceof PhotoModule || mCurrentModule instanceof VideoModule) { AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { @Override public void run() { final Optional bitmap = data.generateThumbnail( mAboveFilmstripControlLayout.getWidth(), mAboveFilmstripControlLayout.getMeasuredHeight()); if (bitmap.isPresent()) { indicateCapture(bitmap.get(), 0); } } }); } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, newData); } @Override public void enableKeepScreenOn(boolean enabled) { if (mPaused) { return; } mKeepScreenOn = enabled; if (mKeepScreenOn) { mMainHandler.removeMessages(MSG_CLEAR_SCREEN_ON_FLAG); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { keepScreenOnForAWhile(); } } @Override public CameraProvider getCameraProvider() { return mCameraController; } @Override public OneCameraOpener getCameraOpener() { return mOneCameraOpener; } private void removeItemAt(int index) { mDataAdapter.removeAt(index); if (mDataAdapter.getTotalNumber() > 0) { showUndoDeletionBar(); } else { // If camera preview is the only view left in filmstrip, // no need to show undo bar. mPendingDeletion = true; performDeletion(); if (mFilmstripVisible) { mCameraAppUI.getFilmstripContentPanel().animateHide(); } } } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle presses on the action bar items switch (item.getItemId()) { case android.R.id.home: onBackPressed(); return true; case R.id.action_details: showDetailsDialog(mFilmstripController.getCurrentAdapterIndex()); return true; case R.id.action_help_and_feedback: mResetToPreviewOnResume = false; new GoogleHelpHelper(this).launchGoogleHelp(); return true; default: return super.onOptionsItemSelected(item); } } private boolean isCaptureIntent() { if (MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction()) || MediaStore.ACTION_IMAGE_CAPTURE.equals(getIntent().getAction()) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(getIntent().getAction())) { return true; } else { return false; } } /** * Note: Make sure this callback is unregistered properly when the activity * is destroyed since we're otherwise leaking the Activity reference. */ private final CameraExceptionHandler.CameraExceptionCallback mCameraExceptionCallback = new CameraExceptionHandler.CameraExceptionCallback() { @Override public void onCameraError(int errorCode) { // Not a fatal error. only do Log.e(). Log.e(TAG, "Camera error callback. error=" + errorCode); } @Override public void onCameraException( RuntimeException ex, String commandHistory, int action, int state) { Log.e(TAG, "Camera Exception", ex); UsageStatistics.instance().cameraFailure( eventprotos.CameraFailure.FailureReason.API_RUNTIME_EXCEPTION, commandHistory, action, state); onFatalError(); } @Override public void onDispatchThreadException(RuntimeException ex) { Log.e(TAG, "DispatchThread Exception", ex); UsageStatistics.instance().cameraFailure( eventprotos.CameraFailure.FailureReason.API_TIMEOUT, null, UsageStatistics.NONE, UsageStatistics.NONE); onFatalError(); } private void onFatalError() { if (mCameraFatalError) { return; } mCameraFatalError = true; // If the activity receives exception during onPause, just exit the app. if (mPaused && !isFinishing()) { Log.e(TAG, "Fatal error during onPause, call Activity.finish()"); finish(); } else { mFatalErrorHandler.handleFatalError(FatalErrorHandler.Reason.CANNOT_CONNECT_TO_CAMERA); } } }; @Override public void onNewIntentTasks(Intent intent) { onModeSelected(getModeIndex()); } @Override public void onCreateTasks(Bundle state) { Profile profile = mProfiler.create("CameraActivity.onCreateTasks").start(); CameraPerformanceTracker.onEvent(CameraPerformanceTracker.ACTIVITY_START); mOnCreateTime = System.currentTimeMillis(); mAppContext = getApplicationContext(); mMainHandler = new MainHandler(this, getMainLooper()); mLocationManager = new LocationManager(mAppContext, shouldUseNoOpLocation()); mOrientationManager = new OrientationManagerImpl(this, mMainHandler); mSettingsManager = getServices().getSettingsManager(); mSoundPlayer = new SoundPlayer(mAppContext); mFeatureConfig = OneCameraFeatureConfigCreator.createDefault(getContentResolver(), getServices().getMemoryManager()); mFatalErrorHandler = new FatalErrorHandlerImpl(this); checkPermissions(); if (!mHasCriticalPermissions) { Log.v(TAG, "onCreate: Missing critical permissions."); finish(); return; } profile.mark(); if (!Glide.isSetup()) { Context context = getAndroidContext(); Glide.setup(new GlideBuilder(context) .setDecodeFormat(DecodeFormat.ALWAYS_ARGB_8888) .setResizeService(new FifoPriorityThreadPoolExecutor(2))); Glide glide = Glide.get(context); // As a camera we will use a large amount of memory // for displaying images. glide.setMemoryCategory(MemoryCategory.HIGH); } profile.mark("Glide.setup"); mActiveCameraDeviceTracker = ActiveCameraDeviceTracker.instance(); try { mOneCameraOpener = OneCameraModule.provideOneCameraOpener( mFeatureConfig, mAppContext, mActiveCameraDeviceTracker, ResolutionUtil.getDisplayMetrics(this)); mOneCameraManager = OneCameraModule.provideOneCameraManager(); } catch (OneCameraException e) { // Log error and continue start process while showing error dialog.. Log.e(TAG, "Creating camera manager failed.", e); mFatalErrorHandler.onGenericCameraAccessFailure(); } profile.mark("OneCameraManager.get"); try { mCameraController = new CameraController(mAppContext, this, mMainHandler, CameraAgentFactory.getAndroidCameraAgent(mAppContext, CameraAgentFactory.CameraApi.API_1), CameraAgentFactory.getAndroidCameraAgent(mAppContext, CameraAgentFactory.CameraApi.AUTO), mActiveCameraDeviceTracker); mCameraController.setCameraExceptionHandler( new CameraExceptionHandler(mCameraExceptionCallback, mMainHandler)); } catch (AssertionError e) { Log.e(TAG, "Creating camera controller failed.", e); mFatalErrorHandler.onGenericCameraAccessFailure(); } // TODO: Try to move all the resources allocation to happen as soon as // possible so we can call module.init() at the earliest time. mModuleManager = new ModuleManagerImpl(); ModulesInfo.setupModules(mAppContext, mModuleManager, mFeatureConfig); AppUpgrader appUpgrader = new AppUpgrader(this); appUpgrader.upgrade(mSettingsManager); // Make sure the picture sizes are correctly cached for the current OS // version. profile.mark(); try { PictureSizeLoader pictureSizeLoader = new PictureSizeLoader(mAppContext); pictureSizeLoader.computePictureSizes(); pictureSizeLoader.release(); } catch (AssertionError e) { Log.e(TAG, "Creating camera controller failed.", e); mFatalErrorHandler.onGenericCameraAccessFailure(); } profile.mark("computePictureSizes"); Keys.setDefaults(mSettingsManager, mAppContext); mResolutionSetting = new ResolutionSetting(mSettingsManager, mOneCameraManager, getContentResolver()); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); // We suppress this flag via theme when drawing the system preview // background, but once we create activity here, reactivate to the // default value. The default is important for L, we don't want to // change app behavior, just starting background drawable layout. if (ApiHelper.isLOrHigher()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } profile.mark(); setContentView(R.layout.activity_main); profile.mark("setContentView()"); // A window background is set in styles.xml for the system to show a // drawable background with gray color and camera icon before the // activity is created. We set the background to null here to prevent // overdraw, all views must take care of drawing backgrounds if // necessary. This call to setBackgroundDrawable must occur after // setContentView, otherwise a background may be set again from the // style. getWindow().setBackgroundDrawable(null); mActionBar = getActionBar(); // set actionbar background to 100% or 50% transparent if (ApiHelper.isLOrHigher()) { mActionBar.setBackgroundDrawable(new ColorDrawable(0x00000000)); } else { mActionBar.setBackgroundDrawable(new ColorDrawable(0x80000000)); } mModeListView = (ModeListView) findViewById(R.id.mode_list_layout); mModeListView.init(mModuleManager.getSupportedModeIndexList()); if (ApiHelper.HAS_ROTATION_ANIMATION) { setRotationAnimation(); } mModeListView.setVisibilityChangedListener(new ModeListVisibilityChangedListener() { @Override public void onVisibilityChanged(boolean visible) { mModeListVisible = visible; mCameraAppUI.setShutterButtonImportantToA11y(!visible); updatePreviewVisibility(); } }); // Check if this is in the secure camera mode. Intent intent = getIntent(); String action = intent.getAction(); if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action) || ACTION_IMAGE_CAPTURE_SECURE.equals(action)) { mSecureCamera = true; } else { mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false); } if (mSecureCamera) { // Change the window flags so that secure camera can show when // locked Window win = getWindow(); WindowManager.LayoutParams params = win.getAttributes(); params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; win.setAttributes(params); // Filter for screen off so that we can finish secure camera // activity when screen is off. IntentFilter filter_screen_off = new IntentFilter(Intent.ACTION_SCREEN_OFF); registerReceiver(mShutdownReceiver, filter_screen_off); // Filter for phone unlock so that we can finish secure camera // via this UI path: // 1. from secure lock screen, user starts secure camera // 2. user presses home button // 3. user unlocks phone IntentFilter filter_user_unlock = new IntentFilter(Intent.ACTION_USER_PRESENT); registerReceiver(mShutdownReceiver, filter_user_unlock); } mCameraAppUI = new CameraAppUI(this, (MainActivityLayout) findViewById(R.id.activity_root_view), isCaptureIntent()); mCameraAppUI.setFilmstripBottomControlsListener(mMyFilmstripBottomControlListener); mAboveFilmstripControlLayout = (FrameLayout) findViewById(R.id.camera_filmstrip_content_layout); // Add the session listener so we can track the session progress // updates. getServices().getCaptureSessionManager().addSessionListener(mSessionListener); mFilmstripController = ((FilmstripView) findViewById(R.id.filmstrip_view)).getController(); mFilmstripController.setImageGap( getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap)); profile.mark("Configure Camera UI"); mPanoramaViewHelper = new PanoramaViewHelper(this); mPanoramaViewHelper.onCreate(); ContentResolver appContentResolver = mAppContext.getContentResolver(); GlideFilmstripManager glideManager = new GlideFilmstripManager(mAppContext); mPhotoItemFactory = new PhotoItemFactory(mAppContext, glideManager, appContentResolver, new PhotoDataFactory()); mVideoItemFactory = new VideoItemFactory(mAppContext, glideManager, appContentResolver, new VideoDataFactory()); mCameraAppUI.getFilmstripContentPanel().setFilmstripListener(mFilmstripListener); if (mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL, Keys.KEY_SHOULD_SHOW_REFOCUS_VIEWER_CLING)) { mCameraAppUI.setupClingForViewer(CameraAppUI.BottomPanel.VIEWER_REFOCUS); } setModuleFromModeIndex(getModeIndex()); profile.mark(); mCameraAppUI.prepareModuleUI(); profile.mark("Init Current Module UI"); mCurrentModule.init(this, isSecureCamera(), isCaptureIntent()); profile.mark("Init CurrentModule"); preloadFilmstripItems(); // Candidate for deletion as Android Beam is deprecated in Android Q setupNfcBeamPush(); mLocalImagesObserver = new FilmstripContentObserver(); mLocalVideosObserver = new FilmstripContentObserver(); getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, mLocalImagesObserver); getContentResolver().registerContentObserver( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, mLocalVideosObserver); mMemoryManager = getServices().getMemoryManager(); AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { @Override public void run() { HashMap memoryData = mMemoryManager.queryMemory(); UsageStatistics.instance().reportMemoryConsumed(memoryData, MemoryQuery.REPORT_LABEL_LAUNCH); } }); mMotionManager = getServices().getMotionManager(); mFirstRunDialog = new FirstRunDialog(this, this /* as context */, mResolutionSetting, mSettingsManager, mOneCameraManager, new FirstRunDialog.FirstRunDialogListener() { @Override public void onFirstRunStateReady() { // Run normal resume tasks. resume(); } @Override public void onFirstRunDialogCancelled() { // App isn't functional until users finish first run dialog. // We need to finish here since users hit back button during // first run dialog (b/19593942). finish(); } @Override public void onCameraAccessException() { mFatalErrorHandler.onGenericCameraAccessFailure(); } }); profile.stop(); } /** * Get the current mode index from the Intent or from persistent * settings. */ private int getModeIndex() { int modeIndex = -1; int photoIndex = getResources().getInteger(R.integer.camera_mode_photo); int videoIndex = getResources().getInteger(R.integer.camera_mode_video); int gcamIndex = getResources().getInteger(R.integer.camera_mode_gcam); int captureIntentIndex = getResources().getInteger(R.integer.camera_mode_capture_intent); String intentAction = getIntent().getAction(); if (MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(intentAction) || MediaStore.ACTION_VIDEO_CAPTURE.equals(intentAction)) { modeIndex = videoIndex; } else if (MediaStore.ACTION_IMAGE_CAPTURE.equals(intentAction) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(intentAction)) { // Capture intent. modeIndex = captureIntentIndex; } else if (MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(intentAction) ||MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(intentAction) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(intentAction)) { modeIndex = mSettingsManager.getInteger(SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_MODULE_LAST_USED); // For upgraders who have not seen the aspect ratio selection screen, // we need to drop them back in the photo module and have them select // aspect ratio. // TODO: Move this to SettingsManager as an upgrade procedure. if (!mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL, Keys.KEY_USER_SELECTED_ASPECT_RATIO)) { modeIndex = photoIndex; } } else { // If the activity has not been started using an explicit intent, // read the module index from the last time the user changed modes modeIndex = mSettingsManager.getInteger(SettingsManager.SCOPE_GLOBAL, Keys.KEY_STARTUP_MODULE_INDEX); if ((modeIndex == gcamIndex && !GcamHelper.hasGcamAsSeparateModule(mFeatureConfig)) || modeIndex < 0) { modeIndex = photoIndex; } } return modeIndex; } /** * Incase the calling package doesn't have ACCESS_FINE_LOCATION permissions, we should not pass * it valid location information in exif. */ private boolean shouldUseNoOpLocation () { String callingPackage = getCallingPackage(); if (callingPackage == null) { // Activity not started through startActivityForResult. return false; } PackageInfo packageInfo = null; try { packageInfo = getPackageManager().getPackageInfo(callingPackage, PackageManager.GET_PERMISSIONS); } catch (Exception e) { Log.w(TAG, "Unable to get PackageInfo for callingPackage " + callingPackage); } if (packageInfo != null) { if (packageInfo.requestedPermissions == null) { // No-permissions at all, were requested by the calling app. return true; } for (int i = 0; i < packageInfo.requestedPermissions.length; i++) { if (packageInfo.requestedPermissions[i].equals( Manifest.permission.ACCESS_FINE_LOCATION) && (packageInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { return false; } } } return true; } /** * Call this whenever the mode drawer or filmstrip change the visibility * state. */ private void updatePreviewVisibility() { if (mCurrentModule == null) { return; } int visibility = getPreviewVisibility(); mCameraAppUI.onPreviewVisiblityChanged(visibility); updatePreviewRendering(visibility); mCurrentModule.onPreviewVisibilityChanged(visibility); } private void updatePreviewRendering(int visibility) { if (visibility == ModuleController.VISIBILITY_HIDDEN) { mCameraAppUI.pausePreviewRendering(); } else { mCameraAppUI.resumePreviewRendering(); } } private int getPreviewVisibility() { if (mFilmstripCoversPreview) { return ModuleController.VISIBILITY_HIDDEN; } else if (mModeListVisible){ return ModuleController.VISIBILITY_COVERED; } else { return ModuleController.VISIBILITY_VISIBLE; } } private void setRotationAnimation() { int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; Window win = getWindow(); WindowManager.LayoutParams winParams = win.getAttributes(); winParams.rotationAnimation = rotationAnimation; win.setAttributes(winParams); } @Override public void onUserInteraction() { super.onUserInteraction(); if (!isFinishing()) { keepScreenOnForAWhile(); } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = super.dispatchTouchEvent(ev); if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { // Real deletion is postponed until the next user interaction after // the gesture that triggers deletion. Until real deletion is // performed, users can click the undo button to bring back the // image that they chose to delete. if (mPendingDeletion && !mIsUndoingDeletion) { performDeletion(); } } return result; } @Override public void onPauseTasks() { CameraPerformanceTracker.onEvent(CameraPerformanceTracker.ACTIVITY_PAUSE); Profile profile = mProfiler.create("CameraActivity.onPause").start(); /* * Save the last module index after all secure camera and icon launches, * not just on mode switches. * * Right now we exclude capture intents from this logic, because we also * ignore the cross-Activity recovery logic in onStart for capture intents. */ if (!isCaptureIntent()) { mSettingsManager.set(SettingsManager.SCOPE_GLOBAL, Keys.KEY_STARTUP_MODULE_INDEX, mCurrentModeIndex); } mPaused = true; mCameraAppUI.hideCaptureIndicator(); mFirstRunDialog.dismiss(); // Delete photos that are pending deletion performDeletion(); mCurrentModule.pause(); mOrientationManager.pause(); mPanoramaViewHelper.onPause(); mLocalImagesObserver.setForegroundChangeListener(null); mLocalImagesObserver.setActivityPaused(true); mLocalVideosObserver.setActivityPaused(true); if (mPreloader != null) { mPreloader.cancelAllLoads(); } resetScreenOn(); mMotionManager.stop(); // Always stop recording location when paused. Resume will start // location recording again if the location setting is on. mLocationManager.recordLocation(false); UsageStatistics.instance().backgrounded(); // Camera is in fatal state. A fatal dialog is presented to users, but users just hit home // button. Let's just kill the process. if (mCameraFatalError && !isFinishing()) { Log.v(TAG, "onPause when camera is in fatal state, call Activity.finish()"); finish(); } else { // Close the camera and wait for the operation done. Log.v(TAG, "onPause closing camera"); if (mCameraController != null) { mCameraController.closeCamera(true); } } profile.stop(); } @Override public void onResumeTasks() { mPaused = false; checkPermissions(); if (!mHasCriticalPermissions) { Log.v(TAG, "onResume: Missing critical permissions."); finish(); return; } if (!isSecureCamera() && !isCaptureIntent()) { // Show the dialog if necessary. The rest resume logic will be invoked // at the onFirstRunStateReady() callback. try { mFirstRunDialog.showIfNecessary(); } catch (AssertionError e) { Log.e(TAG, "Creating camera controller failed.", e); mFatalErrorHandler.onGenericCameraAccessFailure(); } } else { // In secure mode from lockscreen, we go straight to camera and will // show first run dialog next time user enters launcher. Log.v(TAG, "in secure mode, skipping first run dialog check"); resume(); } } /** * 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 (!ApiHelper.isMOrHigher()) { Log.v(TAG, "not running on M, skipping permission checks"); mHasCriticalPermissions = true; return; } if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { mHasCriticalPermissions = true; } else { mHasCriticalPermissions = false; } if (!mHasCriticalPermissions || (mSettingsManager.getBoolean( SettingsManager.SCOPE_GLOBAL, Keys.KEY_RECORD_LOCATION) && (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) && !mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL, Keys.KEY_HAS_SEEN_PERMISSIONS_DIALOGS))) { // TODO: Convert PermissionsActivity into a dialog so we // don't lose the state of CameraActivity. Intent intent = new Intent(this, PermissionsActivity.class); startActivity(intent); 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); updateStorageSpaceAndHint(null); mLastLayoutOrientation = getResources().getConfiguration().orientation; // TODO: Handle this in OrientationManager. // Auto-rotate off if (Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); mAutoRotateScreen = false; } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); mAutoRotateScreen = true; } // Foreground event logging. ACTION_STILL_IMAGE_CAMERA and // INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE are double logged due to // lockscreen onResume->onPause->onResume sequence. int source; String action = getIntent().getAction(); if (action == null) { source = ForegroundSource.UNKNOWN_SOURCE; } else { switch (action) { case MediaStore.ACTION_IMAGE_CAPTURE: source = ForegroundSource.ACTION_IMAGE_CAPTURE; break; case MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA: // was UNKNOWN_SOURCE in Fishlake. source = ForegroundSource.ACTION_STILL_IMAGE_CAMERA; break; case MediaStore.INTENT_ACTION_VIDEO_CAMERA: // was UNKNOWN_SOURCE in Fishlake. source = ForegroundSource.ACTION_VIDEO_CAMERA; break; case MediaStore.ACTION_VIDEO_CAPTURE: source = ForegroundSource.ACTION_VIDEO_CAPTURE; break; case MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE: // was ACTION_IMAGE_CAPTURE_SECURE in Fishlake. source = ForegroundSource.ACTION_STILL_IMAGE_CAMERA_SECURE; break; case MediaStore.ACTION_IMAGE_CAPTURE_SECURE: source = ForegroundSource.ACTION_IMAGE_CAPTURE_SECURE; break; case Intent.ACTION_MAIN: source = ForegroundSource.ACTION_MAIN; break; default: source = ForegroundSource.UNKNOWN_SOURCE; break; } } UsageStatistics.instance().foregrounded(source, currentUserInterfaceMode(), isKeyguardSecure(), isKeyguardLocked(), mStartupOnCreate, mExecutionStartNanoTime); mGalleryIntent = IntentHelper.getGalleryIntent(mAppContext); if (ApiHelper.isLOrHigher()) { // hide the up affordance for L devices, it's not very Materially mActionBar.setDisplayShowHomeEnabled(false); } mOrientationManager.resume(); mCurrentModule.hardResetSettings(mSettingsManager); profile.mark(); mCurrentModule.resume(); UsageStatistics.instance().changeScreen(currentUserInterfaceMode(), NavigationChange.InteractionCause.BUTTON); setSwipingEnabled(true); profile.mark("mCurrentModule.resume"); if (!mResetToPreviewOnResume) { FilmstripItem item = mDataAdapter.getItemAt( mFilmstripController.getCurrentAdapterIndex()); if (item != null) { mDataAdapter.refresh(item.getData().getUri()); } } // The share button might be disabled to avoid double tapping. mCameraAppUI.getFilmstripBottomControls().setShareEnabled(true); // Default is showing the preview, unless disabled by explicitly // starting an activity we want to return from to the filmstrip rather // than the preview. mResetToPreviewOnResume = true; if (mLocalVideosObserver.isMediaDataChangedDuringPause() || mLocalImagesObserver.isMediaDataChangedDuringPause()) { if (!mSecureCamera) { // If it's secure camera, requestLoad() should not be called // as it will load all the data. if (!mFilmstripVisible) { mDataAdapter.requestLoad(new Callback() { @Override public void onCallback(Void result) { fillTemporarySessions(); } }); } else { mDataAdapter.requestLoadNewPhotos(); } } } mLocalImagesObserver.setActivityPaused(false); mLocalVideosObserver.setActivityPaused(false); if (!mSecureCamera) { mLocalImagesObserver.setForegroundChangeListener( new FilmstripContentObserver.ChangeListener() { @Override public void onChange() { mDataAdapter.requestLoadNewPhotos(); } }); } keepScreenOnForAWhile(); // Lights-out mode at all times. final View rootView = findViewById(R.id.activity_root_view); mLightsOutRunnable.run(); getWindow().getDecorView().setOnSystemUiVisibilityChangeListener( new OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { mMainHandler.removeCallbacks(mLightsOutRunnable); mMainHandler.postDelayed(mLightsOutRunnable, LIGHTS_OUT_DELAY_MS); } }); profile.mark(); mPanoramaViewHelper.onResume(); profile.mark("mPanoramaViewHelper.onResume()"); ReleaseHelper.showReleaseInfoDialogOnStart(this, mSettingsManager); // Enable location recording if the setting is on. final boolean locationRecordingEnabled = mSettingsManager.getBoolean(SettingsManager.SCOPE_GLOBAL, Keys.KEY_RECORD_LOCATION); mLocationManager.recordLocation(locationRecordingEnabled); final int previewVisibility = getPreviewVisibility(); updatePreviewRendering(previewVisibility); mMotionManager.start(); profile.stop(); } private void fillTemporarySessions() { if (mSecureCamera) { return; } // There might be sessions still in flight (processed by our service). // Make sure they're added to the filmstrip. getServices().getCaptureSessionManager().fillTemporarySession(mSessionListener); } @Override public void onStartTasks() { mIsActivityRunning = true; mPanoramaViewHelper.onStart(); /* * If we're starting after launching a different Activity (lockscreen), * we need to use the last mode used in the other Activity, and * not the old one from this Activity. * * This needs to happen before CameraAppUI.resume() in order to set the * mode cover icon to the actual last mode used. * * Right now we exclude capture intents from this logic. */ int modeIndex = getModeIndex(); if (!isCaptureIntent() && mCurrentModeIndex != modeIndex) { onModeSelected(modeIndex); } if (mResetToPreviewOnResume) { mCameraAppUI.resume(); mResetToPreviewOnResume = false; } } @Override protected void onStopTasks() { mIsActivityRunning = false; mPanoramaViewHelper.onStop(); mLocationManager.disconnect(); } @Override public void onDestroyTasks() { if (mSecureCamera) { unregisterReceiver(mShutdownReceiver); } // Ensure anything that checks for "isPaused" returns true. mPaused = true; mSettingsManager.removeAllListeners(); if (mCameraController != null) { mCameraController.removeCallbackReceiver(); mCameraController.setCameraExceptionHandler(null); } if (mLocalImagesObserver != null) { getContentResolver().unregisterContentObserver(mLocalImagesObserver); } if (mLocalVideosObserver != null) { getContentResolver().unregisterContentObserver(mLocalVideosObserver); } getServices().getCaptureSessionManager().removeSessionListener(mSessionListener); if (mCameraAppUI != null) { mCameraAppUI.onDestroy(); } if (mModeListView != null) { mModeListView.setVisibilityChangedListener(null); } mCameraController = null; mSettingsManager = null; mOrientationManager = null; mButtonManager = null; if (mSoundPlayer != null) { mSoundPlayer.release(); } if (mFirstRunDialog != null) { mFirstRunDialog.dismiss(); } CameraAgentFactory.recycle(CameraAgentFactory.CameraApi.API_1); CameraAgentFactory.recycle(CameraAgentFactory.CameraApi.AUTO); } @Override public void onConfigurationChanged(Configuration config) { super.onConfigurationChanged(config); Log.v(TAG, "onConfigurationChanged"); if (config.orientation == Configuration.ORIENTATION_UNDEFINED) { return; } if (mLastLayoutOrientation != config.orientation) { mLastLayoutOrientation = config.orientation; mCurrentModule.onLayoutOrientationChanged( mLastLayoutOrientation == Configuration.ORIENTATION_LANDSCAPE); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (!mFilmstripVisible) { if (mCurrentModule.onKeyDown(keyCode, event)) { return true; } // Prevent software keyboard or voice search from showing up. if (keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_MENU) { if (event.isLongPress()) { return true; } } } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (!mFilmstripVisible) { // If a module is in the middle of capture, it should // consume the key event. if (mCurrentModule.onKeyUp(keyCode, event)) { return true; } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { // Let the mode list view consume the event. mCameraAppUI.openModeList(); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mCameraAppUI.showFilmstrip(); return true; } } else { if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mFilmstripController.goToNextItem(); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { boolean wentToPrevious = mFilmstripController.goToPreviousItem(); if (!wentToPrevious) { // at beginning of filmstrip, hide and go back to preview mCameraAppUI.hideFilmstrip(); } return true; } } return super.onKeyUp(keyCode, event); } @Override public void onBackPressed() { if (!mCameraAppUI.onBackPressed()) { if (!mCurrentModule.onBackPressed()) { super.onBackPressed(); } } } @Override public boolean isAutoRotateScreen() { // TODO: Move to OrientationManager. return mAutoRotateScreen; } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.filmstrip_menu, menu); mActionBarMenu = menu; // add a button for launching the gallery if (mGalleryIntent != null) { CharSequence appName = IntentHelper.getGalleryAppName(mAppContext, mGalleryIntent); if (appName != null) { MenuItem menuItem = menu.add(appName); menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); menuItem.setIntent(mGalleryIntent); Drawable galleryLogo = IntentHelper.getGalleryIcon(mAppContext, mGalleryIntent); if (galleryLogo != null) { menuItem.setIcon(galleryLogo); } } } return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { if (isSecureCamera() && !ApiHelper.isLOrHigher()) { // Compatibility pre-L: launching new activities right above // lockscreen does not reliably work, only show help if not secure menu.removeItem(R.id.action_help_and_feedback); } return super.onPrepareOptionsMenu(menu); } protected long getStorageSpaceBytes() { synchronized (mStorageSpaceLock) { return mStorageSpaceBytes; } } protected interface OnStorageUpdateDoneListener { public void onStorageUpdateDone(long bytes); } protected void updateStorageSpaceAndHint(final OnStorageUpdateDoneListener callback) { /* * We execute disk operations on a background thread in order to * free up the UI thread. Synchronizing on the lock below ensures * that when getStorageSpaceBytes is called, the main thread waits * until this method has completed. * * However, .execute() does not ensure this execution block will be * run right away (.execute() schedules this AsyncTask for sometime * in the future. executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) * tries to execute the task in parellel with other AsyncTasks, but * there's still no guarantee). * e.g. don't call this then immediately call getStorageSpaceBytes(). * Instead, pass in an OnStorageUpdateDoneListener. */ (new AsyncTask() { @Override protected Long doInBackground(Void ... arg) { synchronized (mStorageSpaceLock) { mStorageSpaceBytes = Storage.instance().getAvailableSpace(); return mStorageSpaceBytes; } } @Override protected void onPostExecute(Long bytes) { updateStorageHint(bytes); // This callback returns after I/O to check disk, so we could be // pausing and shutting down. If so, don't bother invoking. if (callback != null && !mPaused) { callback.onStorageUpdateDone(bytes); } else { Log.v(TAG, "ignoring storage callback after activity pause"); } } }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } protected void updateStorageHint(long storageSpace) { if (!mIsActivityRunning) { return; } String message = null; if (storageSpace == Storage.UNAVAILABLE) { message = getString(R.string.no_storage); } else if (storageSpace == Storage.PREPARING) { message = getString(R.string.preparing_sd); } else if (storageSpace == Storage.UNKNOWN_SIZE) { message = getString(R.string.access_sd_fail); } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD_BYTES) { message = getString(R.string.spaceIsLow_content); } if (message != null) { Log.w(TAG, "Storage warning: " + message); if (mStorageHint == null) { mStorageHint = OnScreenHint.makeText(CameraActivity.this, message); } else { mStorageHint.setText(message); } mStorageHint.show(); UsageStatistics.instance().storageWarning(storageSpace); // Disable all user interactions, mCameraAppUI.setDisableAllUserInteractions(true); } else if (mStorageHint != null) { mStorageHint.cancel(); mStorageHint = null; // Re-enable all user interactions. mCameraAppUI.setDisableAllUserInteractions(false); } } protected void setResultEx(int resultCode) { mResultCodeForTesting = resultCode; setResult(resultCode); } protected void setResultEx(int resultCode, Intent data) { mResultCodeForTesting = resultCode; mResultDataForTesting = data; setResult(resultCode, data); } public int getResultCode() { return mResultCodeForTesting; } public Intent getResultData() { return mResultDataForTesting; } public boolean isSecureCamera() { return mSecureCamera; } @Override public boolean isPaused() { return mPaused; } @Override public int getPreferredChildModeIndex(int modeIndex) { if (modeIndex == getResources().getInteger(R.integer.camera_mode_photo)) { boolean hdrPlusOn = Keys.isHdrPlusOn(mSettingsManager); if (hdrPlusOn && GcamHelper.hasGcamAsSeparateModule(mFeatureConfig)) { modeIndex = getResources().getInteger(R.integer.camera_mode_gcam); } } return modeIndex; } @Override public void onModeSelected(int modeIndex) { if (mCurrentModeIndex == modeIndex) { return; } CameraPerformanceTracker.onEvent(CameraPerformanceTracker.MODE_SWITCH_START); // Record last used camera mode for quick switching if (modeIndex == getResources().getInteger(R.integer.camera_mode_photo) || modeIndex == getResources().getInteger(R.integer.camera_mode_gcam)) { mSettingsManager.set(SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_MODULE_LAST_USED, modeIndex); } closeModule(mCurrentModule); // Select the correct module index from the mode switcher index. modeIndex = getPreferredChildModeIndex(modeIndex); setModuleFromModeIndex(modeIndex); mCameraAppUI.resetBottomControls(mCurrentModule, modeIndex); mCameraAppUI.addShutterListener(mCurrentModule); openModule(mCurrentModule); // Store the module index so we can use it the next time the Camera // starts up. mSettingsManager.set(SettingsManager.SCOPE_GLOBAL, Keys.KEY_STARTUP_MODULE_INDEX, modeIndex); } /** * Shows the settings dialog. */ @Override public void onSettingsSelected() { UsageStatistics.instance().controlUsed( eventprotos.ControlEvent.ControlType.OVERALL_SETTINGS); Intent intent = new Intent(this, CameraSettingsActivity.class); if (!isKeyguardLocked()) { startActivity(intent); } else { /* Need to explicitly request keyguard dismissal for PIN/pattern * entry to show up directly. */ requestDismissKeyguard( /* requesting Activity: */ CameraActivity.this, new KeyguardDismissCallback() { @Override public void onDismissSucceeded() { /* Need to use launchActivityByIntent() so that going * back from settings after unlock leads to main * activity instead of dismissing camera entirely. */ launchActivityByIntent(intent); } @Override public void onDismissError() { Log.e(TAG, "Keyguard dismissal failed."); } @Override public void onDismissCancelled() { Log.d(TAG, "Keyguard dismissal canceled."); } } ); } } @Override public void freezeScreenUntilPreviewReady() { mCameraAppUI.freezeScreenUntilPreviewReady(); } @Override public int getModuleId(int modeIndex) { ModuleManagerImpl.ModuleAgent agent = mModuleManager.getModuleAgent(modeIndex); if (agent == null) { return -1; } return agent.getModuleId(); } /** * Sets the mCurrentModuleIndex, creates a new module instance for the given * index an sets it as mCurrentModule. */ private void setModuleFromModeIndex(int modeIndex) { ModuleManagerImpl.ModuleAgent agent = mModuleManager.getModuleAgent(modeIndex); if (agent == null) { return; } if (!agent.requestAppForCamera()) { mCameraController.closeCamera(true); } mCurrentModeIndex = agent.getModuleId(); mCurrentModule = (CameraModule) agent.createModule(this, getIntent()); } @Override public SettingsManager getSettingsManager() { return mSettingsManager; } @Override public ResolutionSetting getResolutionSetting() { return mResolutionSetting; } @Override public CameraServices getServices() { return CameraServicesImpl.instance(); } @Override public FatalErrorHandler getFatalErrorHandler() { return mFatalErrorHandler; } public List getSupportedModeNames() { List indices = mModuleManager.getSupportedModeIndexList(); List supported = new ArrayList(); for (Integer modeIndex : indices) { String name = CameraUtil.getCameraModeText(modeIndex, mAppContext); if (name != null && !name.equals("")) { supported.add(name); } } return supported; } @Override public ButtonManager getButtonManager() { if (mButtonManager == null) { mButtonManager = new ButtonManager(this); } return mButtonManager; } @Override public SoundPlayer getSoundPlayer() { return mSoundPlayer; } /** * Launches an ACTION_EDIT intent for the given local data item. If * 'withTinyPlanet' is set, this will show a disambig dialog first to let * the user start either the tiny planet editor or another photo editor. * * @param data The data item to edit. */ public void launchEditor(FilmstripItem data) { Intent intent = new Intent(Intent.ACTION_EDIT) .setDataAndType(data.getData().getUri(), data.getData().getMimeType()) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { launchActivityByIntent(intent); } catch (ActivityNotFoundException e) { final String msgEditWith = getResources().getString(R.string.edit_with); launchActivityByIntent(Intent.createChooser(intent, msgEditWith)); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.filmstrip_context_menu, menu); } @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.tiny_planet_editor: mMyFilmstripBottomControlListener.onTinyPlanet(); return true; case R.id.photo_editor: mMyFilmstripBottomControlListener.onEdit(); return true; } return false; } /** * Launch the tiny planet editor. * * @param data The data must be a 360 degree stereographically mapped * panoramic image. It will not be modified, instead a new item * with the result will be added to the filmstrip. */ public void launchTinyPlanetEditor(FilmstripItem data) { TinyPlanetFragment fragment = new TinyPlanetFragment(); Bundle bundle = new Bundle(); bundle.putString(TinyPlanetFragment.ARGUMENT_URI, data.getData().getUri().toString()); bundle.putString(TinyPlanetFragment.ARGUMENT_TITLE, data.getData().getTitle()); fragment.setArguments(bundle); fragment.show(getFragmentManager(), "tiny_planet"); } /** * Returns what UI mode (capture mode or filmstrip) we are in. * Returned number one of {@link com.google.common.logging.eventprotos.NavigationChange.Mode} */ private int currentUserInterfaceMode() { int mode = NavigationChange.Mode.UNKNOWN_MODE; if (mCurrentModeIndex == getResources().getInteger(R.integer.camera_mode_photo)) { mode = NavigationChange.Mode.PHOTO_CAPTURE; } if (mCurrentModeIndex == getResources().getInteger(R.integer.camera_mode_video)) { mode = NavigationChange.Mode.VIDEO_CAPTURE; } if (mCurrentModeIndex == getResources().getInteger(R.integer.camera_mode_refocus)) { mode = NavigationChange.Mode.LENS_BLUR; } if (mCurrentModeIndex == getResources().getInteger(R.integer.camera_mode_gcam)) { mode = NavigationChange.Mode.HDR_PLUS; } if (mCurrentModeIndex == getResources().getInteger(R.integer.camera_mode_photosphere)) { mode = NavigationChange.Mode.PHOTO_SPHERE; } if (mCurrentModeIndex == getResources().getInteger(R.integer.camera_mode_panorama)) { mode = NavigationChange.Mode.PANORAMA; } if (mFilmstripVisible) { mode = NavigationChange.Mode.FILMSTRIP; } return mode; } private void openModule(CameraModule module) { module.init(this, isSecureCamera(), isCaptureIntent()); module.hardResetSettings(mSettingsManager); // Hide accessibility zoom UI by default. Modules will enable it themselves if required. getCameraAppUI().hideAccessibilityZoomUI(); if (!mPaused) { module.resume(); UsageStatistics.instance().changeScreen(currentUserInterfaceMode(), NavigationChange.InteractionCause.BUTTON); updatePreviewVisibility(); } } private void closeModule(CameraModule module) { module.pause(); mCameraAppUI.clearModuleUI(); } private void performDeletion() { if (!mPendingDeletion) { return; } hideUndoDeletionBar(false); mDataAdapter.executeDeletion(); } public void showUndoDeletionBar() { if (mPendingDeletion) { performDeletion(); } Log.v(TAG, "showing undo bar"); mPendingDeletion = true; if (mUndoDeletionBar == null) { ViewGroup v = (ViewGroup) getLayoutInflater().inflate(R.layout.undo_bar, mAboveFilmstripControlLayout, true); mUndoDeletionBar = (ViewGroup) v.findViewById(R.id.camera_undo_deletion_bar); View button = mUndoDeletionBar.findViewById(R.id.camera_undo_deletion_button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mDataAdapter.undoDeletion(); // Fix for b/21666018: When undoing a delete in Fullscreen // mode, just flip // back to the filmstrip to force a refresh. if (mFilmstripController.inFullScreen()) { mFilmstripController.goToFilmstrip(); } hideUndoDeletionBar(true); } }); // Setting undo bar clickable to avoid touch events going through // the bar to the buttons (eg. edit button, etc) underneath the bar. mUndoDeletionBar.setClickable(true); // When there is user interaction going on with the undo button, we // do not want to hide the undo bar. button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mIsUndoingDeletion = true; } else if (event.getActionMasked() == MotionEvent.ACTION_UP) { mIsUndoingDeletion = false; } return false; } }); } mUndoDeletionBar.setAlpha(0f); mUndoDeletionBar.setVisibility(View.VISIBLE); mUndoDeletionBar.animate().setDuration(200).alpha(1f).setListener(null).start(); } private void hideUndoDeletionBar(boolean withAnimation) { Log.v(TAG, "Hiding undo deletion bar"); mPendingDeletion = false; if (mUndoDeletionBar != null) { if (withAnimation) { mUndoDeletionBar.animate().setDuration(200).alpha(0f) .setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { // Do nothing. } @Override public void onAnimationEnd(Animator animation) { mUndoDeletionBar.setVisibility(View.GONE); } @Override public void onAnimationCancel(Animator animation) { // Do nothing. } @Override public void onAnimationRepeat(Animator animation) { // Do nothing. } }).start(); } else { mUndoDeletionBar.setVisibility(View.GONE); } } } /** * Enable/disable swipe-to-filmstrip. Will always disable swipe if in * capture intent. * * @param enable {@code true} to enable swipe. */ public void setSwipingEnabled(boolean enable) { // TODO: Bring back the functionality. if (isCaptureIntent()) { // lockPreview(true); } else { // lockPreview(!enable); } } // Accessor methods for getting latency times used in performance testing public long getFirstPreviewTime() { if (mCurrentModule instanceof PhotoModule) { long coverHiddenTime = getCameraAppUI().getCoverHiddenTime(); if (coverHiddenTime != -1) { return coverHiddenTime - mOnCreateTime; } } return -1; } public long getAutoFocusTime() { return (mCurrentModule instanceof PhotoModule) ? ((PhotoModule) mCurrentModule).mAutoFocusTime : -1; } public long getShutterLag() { return (mCurrentModule instanceof PhotoModule) ? ((PhotoModule) mCurrentModule).mShutterLag : -1; } public long getShutterToPictureDisplayedTime() { return (mCurrentModule instanceof PhotoModule) ? ((PhotoModule) mCurrentModule).mShutterToPictureDisplayedTime : -1; } public long getPictureDisplayedToJpegCallbackTime() { return (mCurrentModule instanceof PhotoModule) ? ((PhotoModule) mCurrentModule).mPictureDisplayedToJpegCallbackTime : -1; } public long getJpegCallbackFinishTime() { return (mCurrentModule instanceof PhotoModule) ? ((PhotoModule) mCurrentModule).mJpegCallbackFinishTime : -1; } public long getCaptureStartTime() { return (mCurrentModule instanceof PhotoModule) ? ((PhotoModule) mCurrentModule).mCaptureStartTime : -1; } public boolean isRecording() { return (mCurrentModule instanceof VideoModule) ? ((VideoModule) mCurrentModule).isRecording() : false; } public CameraAgent.CameraOpenCallback getCameraOpenErrorCallback() { return mCameraController; } // For debugging purposes only. public CameraModule getCurrentModule() { return mCurrentModule; } @Override public void showTutorial(AbstractTutorialOverlay tutorial) { mCameraAppUI.showTutorial(tutorial, getLayoutInflater()); } @Override public void finishActivityWithIntentCompleted(Intent resultIntent) { finishActivityWithIntentResult(Activity.RESULT_OK, resultIntent); } @Override public void finishActivityWithIntentCanceled() { finishActivityWithIntentResult(Activity.RESULT_CANCELED, new Intent()); } private void finishActivityWithIntentResult(int resultCode, Intent resultIntent) { mResultCodeForTesting = resultCode; mResultDataForTesting = resultIntent; setResult(resultCode, resultIntent); finish(); } private void keepScreenOnForAWhile() { if (mKeepScreenOn) { return; } mMainHandler.removeMessages(MSG_CLEAR_SCREEN_ON_FLAG); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_ON_FLAG, SCREEN_DELAY_MS); } private void resetScreenOn() { mKeepScreenOn = false; mMainHandler.removeMessages(MSG_CLEAR_SCREEN_ON_FLAG); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } /** * @return {@code true} if the Gallery is launched successfully. */ private boolean startGallery() { if (mGalleryIntent == null) { return false; } try { UsageStatistics.instance().changeScreen(NavigationChange.Mode.GALLERY, NavigationChange.InteractionCause.BUTTON); Intent startGalleryIntent = new Intent(mGalleryIntent); int currentIndex = mFilmstripController.getCurrentAdapterIndex(); FilmstripItem currentFilmstripItem = mDataAdapter.getItemAt(currentIndex); if (currentFilmstripItem != null) { GalleryHelper.setContentUri(startGalleryIntent, currentFilmstripItem.getData().getUri()); } launchActivityByIntent(startGalleryIntent); } catch (ActivityNotFoundException e) { Log.w(TAG, "Failed to launch gallery activity, closing"); } return false; } private void setNfcBeamPushUriFromData(FilmstripItem data) { final Uri uri = data.getData().getUri(); if (uri != Uri.EMPTY) { mNfcPushUris[0] = uri; } else { mNfcPushUris[0] = null; } } /** * Updates the visibility of the filmstrip bottom controls and action bar. */ private void updateUiByData(final int index) { final FilmstripItem currentData = mDataAdapter.getItemAt(index); if (currentData == null) { Log.w(TAG, "Current data ID not found."); hideSessionProgress(); return; } updateActionBarMenu(currentData); /* Bottom controls. */ updateBottomControlsByData(currentData); if (isSecureCamera()) { // We cannot show buttons in secure camera since go to other // activities might create a security hole. mCameraAppUI.getFilmstripBottomControls().hideControls(); return; } setNfcBeamPushUriFromData(currentData); if (!mDataAdapter.isMetadataUpdatedAt(index)) { mDataAdapter.updateMetadataAt(index); } } /** * Updates the bottom controls based on the data. */ private void updateBottomControlsByData(final FilmstripItem currentData) { final CameraAppUI.BottomPanel filmstripBottomPanel = mCameraAppUI.getFilmstripBottomControls(); filmstripBottomPanel.showControls(); filmstripBottomPanel.setEditButtonVisibility( currentData.getAttributes().canEdit()); filmstripBottomPanel.setShareButtonVisibility( currentData.getAttributes().canShare()); filmstripBottomPanel.setDeleteButtonVisibility( currentData.getAttributes().canDelete()); /* Progress bar */ Uri contentUri = currentData.getData().getUri(); CaptureSessionManager sessionManager = getServices() .getCaptureSessionManager(); if (sessionManager.hasErrorMessage(contentUri)) { showProcessError(sessionManager.getErrorMessageId(contentUri)); } else { filmstripBottomPanel.hideProgressError(); hideSessionProgress(); } /* View button */ // We need to add this to a separate DB. final int viewButtonVisibility; if (currentData.getMetadata().isUsePanoramaViewer()) { viewButtonVisibility = CameraAppUI.BottomPanel.VIEWER_PHOTO_SPHERE; } else if (currentData.getMetadata().isHasRgbzData()) { viewButtonVisibility = CameraAppUI.BottomPanel.VIEWER_REFOCUS; } else { viewButtonVisibility = CameraAppUI.BottomPanel.VIEWER_NONE; } filmstripBottomPanel.setTinyPlanetEnabled( currentData.getMetadata().isPanorama360()); filmstripBottomPanel.setViewerButtonVisibility(viewButtonVisibility); } private void showDetailsDialog(int index) { final FilmstripItem data = mDataAdapter.getItemAt(index); if (data == null) { return; } Optional details = data.getMediaDetails(); if (!details.isPresent()) { return; } Dialog detailDialog = DetailsDialog.create(CameraActivity.this, details.get()); detailDialog.show(); UsageStatistics.instance().mediaInteraction( fileNameFromAdapterAtIndex(index), MediaInteraction.InteractionType.DETAILS, NavigationChange.InteractionCause.BUTTON, fileAgeFromAdapterAtIndex(index)); } /** * Show or hide action bar items depending on current data type. */ private void updateActionBarMenu(FilmstripItem data) { if (mActionBarMenu == null) { return; } MenuItem detailsMenuItem = mActionBarMenu.findItem(R.id.action_details); if (detailsMenuItem == null) { return; } boolean showDetails = data.getAttributes().hasDetailedCaptureInfo(); detailsMenuItem.setVisible(showDetails); } }