CameraActivity.java revision 0e57fc1baa471ea2acdd6b5bf8b961aa43b1a4a8
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import android.animation.Animator;
20import android.annotation.TargetApi;
21import android.app.ActionBar;
22import android.app.Activity;
23import android.content.ActivityNotFoundException;
24import android.content.BroadcastReceiver;
25import android.content.ComponentName;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.ServiceConnection;
31import android.content.SharedPreferences;
32import android.content.pm.ActivityInfo;
33import android.content.res.Configuration;
34import android.graphics.Bitmap;
35import android.graphics.SurfaceTexture;
36import android.graphics.drawable.ColorDrawable;
37import android.net.Uri;
38import android.nfc.NfcAdapter;
39import android.nfc.NfcAdapter.CreateBeamUrisCallback;
40import android.nfc.NfcEvent;
41import android.os.AsyncTask;
42import android.os.Build;
43import android.os.Bundle;
44import android.os.Handler;
45import android.os.IBinder;
46import android.os.Looper;
47import android.os.Message;
48import android.preference.PreferenceManager;
49import android.provider.MediaStore;
50import android.provider.Settings;
51import android.util.Log;
52import android.view.KeyEvent;
53import android.view.LayoutInflater;
54import android.view.Menu;
55import android.view.MenuInflater;
56import android.view.MenuItem;
57import android.view.MotionEvent;
58import android.view.View;
59import android.view.ViewGroup;
60import android.view.Window;
61import android.view.WindowManager;
62import android.widget.FrameLayout;
63import android.widget.ImageView;
64import android.widget.ProgressBar;
65import android.widget.ShareActionProvider;
66
67import com.android.camera.app.AppController;
68import com.android.camera.app.AppManagerFactory;
69import com.android.camera.app.CameraController;
70import com.android.camera.app.CameraManager;
71import com.android.camera.app.CameraManagerFactory;
72import com.android.camera.app.CameraProvider;
73import com.android.camera.app.ImageTaskManager;
74import com.android.camera.app.MediaSaver;
75import com.android.camera.app.ModuleManagerImpl;
76import com.android.camera.app.OrientationManager;
77import com.android.camera.app.OrientationManagerImpl;
78import com.android.camera.app.PanoramaStitchingManager;
79import com.android.camera.app.PlaceholderManager;
80import com.android.camera.crop.CropActivity;
81import com.android.camera.data.CameraDataAdapter;
82import com.android.camera.data.CameraPreviewData;
83import com.android.camera.data.FixedFirstDataAdapter;
84import com.android.camera.data.FixedLastDataAdapter;
85import com.android.camera.data.InProgressDataWrapper;
86import com.android.camera.data.LocalData;
87import com.android.camera.data.LocalDataAdapter;
88import com.android.camera.data.LocalMediaObserver;
89import com.android.camera.data.MediaDetails;
90import com.android.camera.data.SimpleViewData;
91import com.android.camera.filmstrip.FilmstripController;
92import com.android.camera.filmstrip.FilmstripImageData;
93import com.android.camera.filmstrip.FilmstripListener;
94import com.android.camera.module.ModuleController;
95import com.android.camera.tinyplanet.TinyPlanetFragment;
96import com.android.camera.ui.DetailsDialog;
97import com.android.camera.ui.FilmstripView;
98import com.android.camera.ui.ModeListView;
99import com.android.camera.util.ApiHelper;
100import com.android.camera.util.CameraUtil;
101import com.android.camera.util.GcamHelper;
102import com.android.camera.util.IntentHelper;
103import com.android.camera.util.PhotoSphereHelper;
104import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper;
105import com.android.camera.util.RefocusHelper;
106import com.android.camera.util.UsageStatistics;
107import com.android.camera2.R;
108
109import java.io.File;
110
111public class CameraActivity extends Activity
112        implements AppController, ModeListView.ModeSwitchListener, CameraManager.CameraOpenCallback,
113        ActionBar.OnMenuVisibilityListener, ShareActionProvider.OnShareTargetSelectedListener,
114        OrientationManager.OnOrientationChangeListener {
115
116    private static final String TAG = "CAM_Activity";
117
118    private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE =
119            "android.media.action.STILL_IMAGE_CAMERA_SECURE";
120    public static final String ACTION_IMAGE_CAPTURE_SECURE =
121            "android.media.action.IMAGE_CAPTURE_SECURE";
122    public static final String ACTION_TRIM_VIDEO =
123            "com.android.camera.action.TRIM";
124    public static final String MEDIA_ITEM_PATH = "media-item-path";
125
126    // The intent extra for camera from secure lock screen. True if the gallery
127    // should only show newly captured pictures. sSecureAlbumId does not
128    // increment. This is used when switching between camera, camcorder, and
129    // panorama. If the extra is not set, it is in the normal camera mode.
130    public static final String SECURE_CAMERA_EXTRA = "secure_camera";
131
132    /**
133     * Request code from an activity we started that indicated that we do not want
134     * to reset the view to the preview in onResume.
135     */
136    public static final int REQ_CODE_DONT_SWITCH_TO_PREVIEW = 142;
137
138    public static final int REQ_CODE_GCAM_DEBUG_POSTCAPTURE = 999;
139
140    private static final int HIDE_ACTION_BAR = 1;
141    private static final long SHOW_ACTION_BAR_TIMEOUT_MS = 3000;
142
143    /**
144     * Whether onResume should reset the view to the preview.
145     */
146    private boolean mResetToPreviewOnResume = true;
147
148    // Supported operations at FilmStripView. Different data has different
149    // set of supported operations.
150    private static final int SUPPORT_DELETE = 1 << 0;
151    private static final int SUPPORT_ROTATE = 1 << 1;
152    private static final int SUPPORT_INFO = 1 << 2;
153    private static final int SUPPORT_CROP = 1 << 3;
154    private static final int SUPPORT_SETAS = 1 << 4;
155    private static final int SUPPORT_EDIT = 1 << 5;
156    private static final int SUPPORT_TRIM = 1 << 6;
157    private static final int SUPPORT_SHARE = 1 << 7;
158    private static final int SUPPORT_SHARE_PANORAMA360 = 1 << 8;
159    private static final int SUPPORT_SHOW_ON_MAP = 1 << 9;
160    private static final int SUPPORT_ALL = 0xffffffff;
161
162    /**
163     * This data adapter is used by FilmStripView.
164     */
165    private LocalDataAdapter mDataAdapter;
166    /**
167     * This data adapter represents the real local camera data.
168     */
169    private LocalDataAdapter mWrappedDataAdapter;
170
171    private PanoramaStitchingManager mPanoramaManager;
172    private PlaceholderManager mPlaceholderManager;
173    private ModeListView mModeListView;
174    private int mCurrentModuleIndex;
175    private CameraModule mCurrentModule;
176    private ModuleController mCurrentModule2;
177    private ModuleManagerImpl mModuleManager;
178    private FrameLayout mAboveFilmstripControlLayout;
179    private FrameLayout mCameraModuleRootView;
180    private FilmstripController mFilmstripController;
181    private ProgressBar mBottomProgress;
182    private View mPanoStitchingPanel;
183    private int mResultCodeForTesting;
184    private Intent mResultDataForTesting;
185    private OnScreenHint mStorageHint;
186    private long mStorageSpaceBytes = Storage.LOW_STORAGE_THRESHOLD_BYTES;
187    private boolean mAutoRotateScreen;
188    private boolean mSecureCamera;
189    // This is a hack to speed up the start of SecureCamera.
190    private static boolean sFirstStartAfterScreenOn = true;
191    private int mLastRawOrientation;
192    private OrientationManagerImpl mOrientationManager;
193    private Handler mMainHandler;
194    private PanoramaViewHelper mPanoramaViewHelper;
195    private CameraPreviewData mCameraPreviewData;
196    private ActionBar mActionBar;
197    private OnActionBarVisibilityListener mOnActionBarVisibilityListener = null;
198    private Menu mActionBarMenu;
199    private ViewGroup mUndoDeletionBar;
200    private boolean mIsUndoingDeletion = false;
201
202    private Uri[] mNfcPushUris = new Uri[1];
203
204    private ShareActionProvider mStandardShareActionProvider;
205    private Intent mStandardShareIntent;
206    private ShareActionProvider mPanoramaShareActionProvider;
207    private Intent mPanoramaShareIntent;
208    private LocalMediaObserver mLocalImagesObserver;
209    private LocalMediaObserver mLocalVideosObserver;
210
211    private final int DEFAULT_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
212            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
213    private boolean mPendingDeletion = false;
214
215    private Intent mVideoShareIntent;
216    private Intent mImageShareIntent;
217
218    private CameraController mCameraController;
219
220    private MediaSaver mMediaSaver;
221    private ServiceConnection mConnection = new ServiceConnection() {
222        @Override
223        public void onServiceConnected(ComponentName className, IBinder b) {
224            mMediaSaver = ((MediaSaveService.LocalBinder) b).getService();
225            mCurrentModule.onMediaSaverAvailable(mMediaSaver);
226        }
227
228        @Override
229        public void onServiceDisconnected(ComponentName className) {
230            if (mMediaSaver != null) {
231                mMediaSaver.setQueueListener(null);
232                mMediaSaver = null;
233            }
234        }
235    };
236
237
238    // close activity when screen turns off
239    private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
240        @Override
241        public void onReceive(Context context, Intent intent) {
242            finish();
243        }
244    };
245
246    private static BroadcastReceiver sScreenOffReceiver;
247
248    @Override
249    public void onCameraOpened(CameraManager.CameraProxy camera) {
250        if (!mModuleManager.getModuleAgent(mCurrentModuleIndex).requestAppForCamera()) {
251            // We shouldn't be here. Just close the camera and leave.
252            camera.release(false);
253            throw new IllegalStateException("Camera opened but the module shouldn't be " +
254                    "requesting");
255        }
256        if (mCurrentModule2 != null) {
257            mCurrentModule2.onCameraAvailable(camera);
258        }
259    }
260
261    @Override
262    public void onCameraDisabled(int cameraId) {
263        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, UsageStatistics.ACTION_OPEN_FAIL,
264                "security");
265
266        CameraUtil.showErrorAndFinish(this, R.string.camera_disabled);
267    }
268
269    @Override
270    public void onDeviceOpenFailure(int cameraId) {
271        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
272                UsageStatistics.ACTION_OPEN_FAIL, "open");
273
274        CameraUtil.showErrorAndFinish(this, R.string.cannot_connect_camera);
275    }
276
277    @Override
278    public void onReconnectionFailure(CameraManager mgr) {
279        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
280                UsageStatistics.ACTION_OPEN_FAIL, "reconnect");
281
282        CameraUtil.showErrorAndFinish(this, R.string.cannot_connect_camera);
283    }
284
285    private static class ScreenOffReceiver extends BroadcastReceiver {
286        @Override
287        public void onReceive(Context context, Intent intent) {
288            sFirstStartAfterScreenOn = true;
289        }
290    }
291
292    private class MainHandler extends Handler {
293        public MainHandler(Looper looper) {
294            super(looper);
295        }
296
297        @Override
298        public void handleMessage(Message msg) {
299            if (msg.what == HIDE_ACTION_BAR) {
300                removeMessages(HIDE_ACTION_BAR);
301                CameraActivity.this.setSystemBarsVisibility(false);
302            }
303        }
304    }
305
306    public interface OnActionBarVisibilityListener {
307        public void onActionBarVisibilityChanged(boolean isVisible);
308    }
309
310    public void setOnActionBarVisibilityListener(OnActionBarVisibilityListener listener) {
311        mOnActionBarVisibilityListener = listener;
312    }
313
314    public static boolean isFirstStartAfterScreenOn() {
315        return sFirstStartAfterScreenOn;
316    }
317
318    public static void resetFirstStartAfterScreenOn() {
319        sFirstStartAfterScreenOn = false;
320    }
321
322    private String fileNameFromDataID(int dataID) {
323        final LocalData localData = mDataAdapter.getLocalData(dataID);
324
325        File localFile = new File(localData.getPath());
326        return localFile.getName();
327    }
328
329    private FilmstripListener mFilmStripListener =
330            new FilmstripListener() {
331                @Override
332                public void onDataPromoted(int dataID) {
333                    UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
334                            UsageStatistics.ACTION_DELETE, "promoted", 0,
335                            UsageStatistics.hashFileName(fileNameFromDataID(dataID)));
336
337                    removeData(dataID);
338                }
339
340                @Override
341                public void onDataDemoted(int dataID) {
342                    UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
343                            UsageStatistics.ACTION_DELETE, "demoted", 0,
344                            UsageStatistics.hashFileName(fileNameFromDataID(dataID)));
345
346                    removeData(dataID);
347                }
348
349                @Override
350                public void onDataFullScreenChange(int dataID, boolean full) {
351                    boolean isCameraID = isCameraPreview(dataID);
352                    if (!isCameraID) {
353                        if (!full) {
354                            // Always show action bar in filmstrip mode
355                            CameraActivity.this.setSystemBarsVisibility(true, false);
356                        } else if (mActionBar.isShowing()) {
357                            // Hide action bar after time out in full screen mode
358                            mMainHandler.sendEmptyMessageDelayed(HIDE_ACTION_BAR,
359                                    SHOW_ACTION_BAR_TIMEOUT_MS);
360                        }
361                    }
362                }
363
364                /**
365                 * Check if the local data corresponding to dataID is the camera
366                 * preview.
367                 *
368                 * @param dataID the ID of the local data
369                 * @return true if the local data is not null and it is the
370                 *         camera preview.
371                 */
372                private boolean isCameraPreview(int dataID) {
373                    LocalData localData = mDataAdapter.getLocalData(dataID);
374                    if (localData == null) {
375                        Log.w(TAG, "Current data ID not found.");
376                        return false;
377                    }
378                    return localData.getLocalDataType() == LocalData.LOCAL_CAMERA_PREVIEW;
379                }
380
381                @Override
382                public void onDataReloaded() {
383                    setPreviewControlsVisibility(true);
384                    CameraActivity.this.setSystemBarsVisibility(false);
385                }
386
387                @Override
388                public void onCurrentDataCentered(int dataID) {
389                    if (dataID != 0 && !mFilmstripController.isCameraPreview()) {
390                        // For now, We ignore all items that are not the camera preview.
391                        return;
392                    }
393
394                    if (!arePreviewControlsVisible()) {
395                        setPreviewControlsVisibility(true);
396                        CameraActivity.this.setSystemBarsVisibility(false);
397                    }
398                }
399
400                @Override
401                public void onCurrentDataOffCentered(int dataID) {
402                    if (dataID != 0 && !mFilmstripController.isCameraPreview()) {
403                        // For now, We ignore all items that are not the camera preview.
404                        return;
405                    }
406
407                    if (arePreviewControlsVisible()) {
408                        setPreviewControlsVisibility(false);
409                    }
410                }
411
412                @Override
413                public void onDataFocusChanged(final int dataID, final boolean focused) {
414                    // Delay hiding action bar if there is any user interaction
415                    if (mMainHandler.hasMessages(HIDE_ACTION_BAR)) {
416                        mMainHandler.removeMessages(HIDE_ACTION_BAR);
417                        mMainHandler.sendEmptyMessageDelayed(HIDE_ACTION_BAR,
418                                SHOW_ACTION_BAR_TIMEOUT_MS);
419                    }
420                    // TODO: This callback is UI event callback, should always
421                    // happen on UI thread. Find the reason for this
422                    // runOnUiThread() and fix it.
423                    runOnUiThread(new Runnable() {
424                        @Override
425                        public void run() {
426                            LocalData currentData = mDataAdapter.getLocalData(dataID);
427                            if (currentData == null) {
428                                Log.w(TAG, "Current data ID not found.");
429                                hidePanoStitchingProgress();
430                                return;
431                            }
432                            boolean isCameraID = currentData.getLocalDataType() ==
433                                    LocalData.LOCAL_CAMERA_PREVIEW;
434                            if (!focused) {
435                                if (isCameraID) {
436                                    mCurrentModule.onPreviewFocusChanged(false);
437                                    CameraActivity.this.setSystemBarsVisibility(true);
438                                }
439                                hidePanoStitchingProgress();
440                            } else {
441                                if (isCameraID) {
442                                    // Don't show the action bar in Camera
443                                    // preview.
444                                    CameraActivity.this.setSystemBarsVisibility(false);
445
446                                    if (mPendingDeletion) {
447                                        performDeletion();
448                                    }
449                                } else {
450                                    updateActionBarMenu(dataID);
451                                }
452
453                                Uri contentUri = currentData.getContentUri();
454                                if (contentUri == null) {
455                                    hidePanoStitchingProgress();
456                                    return;
457                                }
458                                int panoStitchingProgress = mPanoramaManager.getTaskProgress(contentUri);
459                                if (panoStitchingProgress < 0) {
460                                    hidePanoStitchingProgress();
461                                    return;
462                                }
463                                showPanoStitchingProgress();
464                                updateStitchingProgress(panoStitchingProgress);
465                            }
466                        }
467                    });
468                }
469
470                @Override
471                public void onToggleSystemDecorsVisibility(int dataID) {
472                    // If action bar is showing, hide it immediately, otherwise
473                    // show action bar and hide it later
474                    if (mActionBar.isShowing()) {
475                        CameraActivity.this.setSystemBarsVisibility(false);
476                    } else {
477                        // Don't show the action bar if that is the camera preview.
478                        boolean isCameraID = isCameraPreview(dataID);
479                        if (!isCameraID) {
480                            CameraActivity.this.setSystemBarsVisibility(true, true);
481                        }
482                    }
483                }
484
485                @Override
486                public void setSystemDecorsVisibility(boolean visible) {
487                    CameraActivity.this.setSystemBarsVisibility(visible);
488                }
489            };
490
491    public void gotoGallery() {
492        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, UsageStatistics.ACTION_FILMSTRIP,
493                "thumbnailTap");
494
495        mFilmstripController.goToNextItem();
496    }
497
498    /**
499     * If {@param visible} is false, this hides the action bar and switches the system UI
500     * to lights-out mode.
501     */
502    // TODO: This should not be called outside of the activity.
503    public void setSystemBarsVisibility(boolean visible) {
504        setSystemBarsVisibility(visible, false);
505    }
506
507    /**
508     * If {@param visible} is false, this hides the action bar and switches the
509     * system UI to lights-out mode. If {@param hideLater} is true, a delayed message
510     * will be sent after a timeout to hide the action bar.
511     */
512    private void setSystemBarsVisibility(boolean visible, boolean hideLater) {
513        mMainHandler.removeMessages(HIDE_ACTION_BAR);
514
515        int currentSystemUIVisibility = mAboveFilmstripControlLayout.getSystemUiVisibility();
516        int newSystemUIVisibility = DEFAULT_SYSTEM_UI_VISIBILITY |
517                (visible ? View.SYSTEM_UI_FLAG_VISIBLE :
518                        View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN);
519        if (newSystemUIVisibility != currentSystemUIVisibility) {
520            mAboveFilmstripControlLayout.setSystemUiVisibility(newSystemUIVisibility);
521        }
522
523        boolean currentActionBarVisibility = mActionBar.isShowing();
524        if (visible != currentActionBarVisibility) {
525            if (visible) {
526                mActionBar.show();
527            } else {
528                mActionBar.hide();
529            }
530            if (mOnActionBarVisibilityListener != null) {
531                mOnActionBarVisibilityListener.onActionBarVisibilityChanged(visible);
532            }
533        }
534
535        // Now delay hiding the bars
536        if (visible && hideLater) {
537            mMainHandler.sendEmptyMessageDelayed(HIDE_ACTION_BAR, SHOW_ACTION_BAR_TIMEOUT_MS);
538        }
539    }
540
541    private void hidePanoStitchingProgress() {
542        mPanoStitchingPanel.setVisibility(View.GONE);
543    }
544
545    private void showPanoStitchingProgress() {
546        mPanoStitchingPanel.setVisibility(View.VISIBLE);
547    }
548
549    private void updateStitchingProgress(int progress) {
550        mBottomProgress.setProgress(progress);
551    }
552
553    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
554    private void setupNfcBeamPush() {
555        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(CameraActivity.this);
556        if (adapter == null) {
557            return;
558        }
559
560        if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
561            // Disable beaming
562            adapter.setNdefPushMessage(null, CameraActivity.this);
563            return;
564        }
565
566        adapter.setBeamPushUris(null, CameraActivity.this);
567        adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
568            @Override
569            public Uri[] createBeamUris(NfcEvent event) {
570                return mNfcPushUris;
571            }
572        }, CameraActivity.this);
573    }
574
575    private void setNfcBeamPushUri(Uri uri) {
576        mNfcPushUris[0] = uri;
577    }
578
579    private void setStandardShareIntent(Uri contentUri, String mimeType) {
580        mStandardShareIntent = getShareIntentFromType(mimeType);
581        if (mStandardShareIntent != null) {
582            mStandardShareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
583            mStandardShareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
584            if (mStandardShareActionProvider != null) {
585                mStandardShareActionProvider.setShareIntent(mStandardShareIntent);
586            }
587        }
588    }
589
590    /**
591     * Get the share intent according to the mimeType
592     *
593     * @param mimeType The mimeType of current data.
594     * @return the video/image's ShareIntent or null if mimeType is invalid.
595     */
596    private Intent getShareIntentFromType(String mimeType) {
597        // Lazily create the intent object.
598        if (mimeType.startsWith("video/")) {
599            if (mVideoShareIntent == null) {
600                mVideoShareIntent = new Intent(Intent.ACTION_SEND);
601                mVideoShareIntent.setType("video/*");
602            }
603            return mVideoShareIntent;
604        } else if (mimeType.startsWith("image/")) {
605            if (mImageShareIntent == null) {
606                mImageShareIntent = new Intent(Intent.ACTION_SEND);
607                mImageShareIntent.setType("image/*");
608            }
609            return mImageShareIntent;
610        }
611        Log.w(TAG, "unsupported mimeType " + mimeType);
612        return null;
613    }
614
615    private void setPanoramaShareIntent(Uri contentUri) {
616        if (mPanoramaShareIntent == null) {
617            mPanoramaShareIntent = new Intent(Intent.ACTION_SEND);
618        }
619        mPanoramaShareIntent.setType("application/vnd.google.panorama360+jpg");
620        mPanoramaShareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
621        if (mPanoramaShareActionProvider != null) {
622            mPanoramaShareActionProvider.setShareIntent(mPanoramaShareIntent);
623        }
624    }
625
626    @Override
627    public void onMenuVisibilityChanged(boolean isVisible) {
628        // If menu is showing, we need to make sure action bar does not go away.
629        mMainHandler.removeMessages(HIDE_ACTION_BAR);
630        if (!isVisible) {
631            mMainHandler.sendEmptyMessageDelayed(HIDE_ACTION_BAR, SHOW_ACTION_BAR_TIMEOUT_MS);
632        }
633    }
634
635    @Override
636    public boolean onShareTargetSelected(ShareActionProvider shareActionProvider, Intent intent) {
637        int currentDataId = mFilmstripController.getCurrentId();
638        if (currentDataId < 0) {
639            return false;
640        }
641        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, UsageStatistics.ACTION_SHARE,
642                intent.getComponent().getPackageName(), 0,
643                UsageStatistics.hashFileName(fileNameFromDataID(currentDataId)));
644        return true;
645    }
646
647    /**
648     * According to the data type, make the menu items for supported operations
649     * visible.
650     *
651     * @param dataID the data ID of the current item.
652     */
653    private void updateActionBarMenu(int dataID) {
654        LocalData currentData = mDataAdapter.getLocalData(dataID);
655        if (currentData == null) {
656            return;
657        }
658        int type = currentData.getLocalDataType();
659
660        if (mActionBarMenu == null) {
661            return;
662        }
663
664        int supported = 0;
665
666        switch (type) {
667            case LocalData.LOCAL_IMAGE:
668                supported |= SUPPORT_DELETE | SUPPORT_ROTATE | SUPPORT_INFO
669                        | SUPPORT_CROP | SUPPORT_SETAS | SUPPORT_EDIT
670                        | SUPPORT_SHARE | SUPPORT_SHOW_ON_MAP;
671                break;
672            case LocalData.LOCAL_VIDEO:
673                supported |= SUPPORT_DELETE | SUPPORT_INFO | SUPPORT_TRIM
674                        | SUPPORT_SHARE;
675                break;
676            case LocalData.LOCAL_PHOTO_SPHERE:
677                supported |= SUPPORT_DELETE | SUPPORT_ROTATE | SUPPORT_INFO
678                        | SUPPORT_CROP | SUPPORT_SETAS | SUPPORT_EDIT
679                        | SUPPORT_SHARE | SUPPORT_SHOW_ON_MAP;
680                break;
681            case LocalData.LOCAL_360_PHOTO_SPHERE:
682                supported |= SUPPORT_DELETE | SUPPORT_ROTATE | SUPPORT_INFO
683                        | SUPPORT_CROP | SUPPORT_SETAS | SUPPORT_EDIT
684                        | SUPPORT_SHARE | SUPPORT_SHARE_PANORAMA360
685                        | SUPPORT_SHOW_ON_MAP;
686                break;
687            default:
688                break;
689        }
690
691        // In secure camera mode, we only support delete operation.
692        if (isSecureCamera()) {
693            supported &= SUPPORT_DELETE;
694        }
695
696        setMenuItemVisible(mActionBarMenu, R.id.action_delete,
697                (supported & SUPPORT_DELETE) != 0);
698        setMenuItemVisible(mActionBarMenu, R.id.action_rotate_ccw,
699                (supported & SUPPORT_ROTATE) != 0);
700        setMenuItemVisible(mActionBarMenu, R.id.action_rotate_cw,
701                (supported & SUPPORT_ROTATE) != 0);
702        setMenuItemVisible(mActionBarMenu, R.id.action_details,
703                (supported & SUPPORT_INFO) != 0);
704        setMenuItemVisible(mActionBarMenu, R.id.action_crop,
705                (supported & SUPPORT_CROP) != 0);
706        setMenuItemVisible(mActionBarMenu, R.id.action_setas,
707                (supported & SUPPORT_SETAS) != 0);
708        setMenuItemVisible(mActionBarMenu, R.id.action_edit,
709                (supported & SUPPORT_EDIT) != 0);
710        setMenuItemVisible(mActionBarMenu, R.id.action_trim,
711                (supported & SUPPORT_TRIM) != 0);
712
713        boolean standardShare = (supported & SUPPORT_SHARE) != 0;
714        boolean panoramaShare = (supported & SUPPORT_SHARE_PANORAMA360) != 0;
715        setMenuItemVisible(mActionBarMenu, R.id.action_share, standardShare);
716        setMenuItemVisible(mActionBarMenu, R.id.action_share_panorama, panoramaShare);
717
718        if (panoramaShare) {
719            // For 360 PhotoSphere, relegate standard share to the overflow menu
720            MenuItem item = mActionBarMenu.findItem(R.id.action_share);
721            if (item != null) {
722                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
723                item.setTitle(getResources().getString(R.string.share_as_photo));
724            }
725            // And, promote "share as panorama" to action bar
726            item = mActionBarMenu.findItem(R.id.action_share_panorama);
727            if (item != null) {
728                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
729            }
730            setPanoramaShareIntent(currentData.getContentUri());
731        }
732        if (standardShare) {
733            if (!panoramaShare) {
734                MenuItem item = mActionBarMenu.findItem(R.id.action_share);
735                if (item != null) {
736                    item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
737                    item.setTitle(getResources().getString(R.string.share));
738                }
739            }
740            setStandardShareIntent(currentData.getContentUri(), currentData.getMimeType());
741            setNfcBeamPushUri(currentData.getContentUri());
742        }
743
744        boolean itemHasLocation = currentData.getLatLong() != null;
745        setMenuItemVisible(mActionBarMenu, R.id.action_show_on_map,
746                itemHasLocation && (supported & SUPPORT_SHOW_ON_MAP) != 0);
747    }
748
749    private void setMenuItemVisible(Menu menu, int itemId, boolean visible) {
750        MenuItem item = menu.findItem(itemId);
751        if (item != null) {
752            item.setVisible(visible);
753        }
754    }
755
756    private ImageTaskManager.TaskListener mPlaceholderListener =
757            new ImageTaskManager.TaskListener() {
758
759                @Override
760                public void onTaskQueued(String filePath, final Uri imageUri) {
761                    mMainHandler.post(new Runnable() {
762                        @Override
763                        public void run() {
764                            notifyNewMedia(imageUri);
765                            int dataID = mDataAdapter.findDataByContentUri(imageUri);
766                            if (dataID != -1) {
767                                LocalData d = mDataAdapter.getLocalData(dataID);
768                                InProgressDataWrapper newData = new InProgressDataWrapper(d, true);
769                                mDataAdapter.updateData(dataID, newData);
770                            }
771                        }
772                    });
773                }
774
775                @Override
776                public void onTaskDone(String filePath, final Uri imageUri) {
777                    mMainHandler.post(new Runnable() {
778                        @Override
779                        public void run() {
780                            mDataAdapter.refresh(getContentResolver(), imageUri);
781                        }
782                    });
783                }
784
785                @Override
786                public void onTaskProgress(String filePath, Uri imageUri, int progress) {
787                    // Do nothing
788                }
789            };
790
791    private ImageTaskManager.TaskListener mStitchingListener =
792            new ImageTaskManager.TaskListener() {
793                @Override
794                public void onTaskQueued(String filePath, final Uri imageUri) {
795                    mMainHandler.post(new Runnable() {
796                        @Override
797                        public void run() {
798                            notifyNewMedia(imageUri);
799                            int dataID = mDataAdapter.findDataByContentUri(imageUri);
800                            if (dataID != -1) {
801                                // Don't allow special UI actions (swipe to
802                                // delete, for example) on in-progress data.
803                                LocalData d = mDataAdapter.getLocalData(dataID);
804                                InProgressDataWrapper newData = new InProgressDataWrapper(d);
805                                mDataAdapter.updateData(dataID, newData);
806                            }
807                        }
808                    });
809                }
810
811                @Override
812                public void onTaskDone(String filePath, final Uri imageUri) {
813                    Log.v(TAG, "onTaskDone:" + filePath);
814                    mMainHandler.post(new Runnable() {
815                        @Override
816                        public void run() {
817                            int doneID = mDataAdapter.findDataByContentUri(imageUri);
818                            int currentDataId = mFilmstripController.getCurrentId();
819
820                            if (currentDataId == doneID) {
821                                hidePanoStitchingProgress();
822                                updateStitchingProgress(0);
823                            }
824
825                            mDataAdapter.refresh(getContentResolver(), imageUri);
826                        }
827                    });
828                }
829
830                @Override
831                public void onTaskProgress(
832                        String filePath, final Uri imageUri, final int progress) {
833                    mMainHandler.post(new Runnable() {
834                        @Override
835                        public void run() {
836                            int currentDataId = mFilmstripController.getCurrentId();
837                            if (currentDataId == -1) {
838                                return;
839                            }
840                            if (imageUri.equals(
841                                    mDataAdapter.getLocalData(currentDataId).getContentUri())) {
842                                updateStitchingProgress(progress);
843                            }
844                        }
845                    });
846                }
847            };
848
849    @Override
850    public Context getAndroidContext() {
851        return this;
852    }
853
854    @Override
855    public SurfaceTexture getPreviewBuffer() {
856        // TODO: implement this
857        return null;
858    }
859
860    @Override
861    public FrameLayout getModuleLayoutRoot() {
862        return mCameraModuleRootView;
863    }
864
865    @Override
866    public void setShutterEventsListener(ShutterEventsListener listener) {
867        // TODO: implement this
868    }
869
870    @Override
871    public void setShutterEnabled(boolean enabled) {
872        // TODO: implement this
873    }
874
875    @Override
876    public boolean isShutterEnabled() {
877        // TODO: implement this
878        return false;
879    }
880
881    @Override
882    public void startPreCaptureAnimation() {
883        // TODO: implement this
884    }
885
886    @Override
887    public void cancelPreCaptureAnimation() {
888        // TODO: implement this
889    }
890
891    @Override
892    public void startPostCaptureAnimation() {
893        // TODO: implement this
894    }
895
896    @Override
897    public void startPostCaptureAnimation(Bitmap thumbnail) {
898        // TODO: implement this
899    }
900
901    @Override
902    public void cancelPostCaptureAnimation() {
903        // TODO: implement this
904    }
905
906    @Override
907    public MediaSaver getMediaSaver() {
908        return mMediaSaver;
909    }
910
911    @Override
912    public OrientationManager getOrientationManager() {
913        return mOrientationManager;
914    }
915
916    @Override
917    public LocationManager getLocationManager() {
918        // TODO: implement this
919        return null;
920    }
921
922    @Override
923    public void lockOrientation() {
924        mOrientationManager.lockOrientation();
925    }
926
927    @Override
928    public void unlockOrientation() {
929        mOrientationManager.unlockOrientation();
930    }
931
932    @Override
933    public void notifyNewMedia(Uri uri) {
934        ContentResolver cr = getContentResolver();
935        String mimeType = cr.getType(uri);
936        if (mimeType.startsWith("video/")) {
937            sendBroadcast(new Intent(CameraUtil.ACTION_NEW_VIDEO, uri));
938            mDataAdapter.addNewVideo(cr, uri);
939        } else if (mimeType.startsWith("image/")) {
940            CameraUtil.broadcastNewPicture(this, uri);
941            mDataAdapter.addNewPhoto(cr, uri);
942        } else if (mimeType.startsWith("application/stitching-preview")) {
943            mDataAdapter.addNewPhoto(cr, uri);
944        } else if (mimeType.startsWith(PlaceholderManager.PLACEHOLDER_MIME_TYPE)) {
945            mDataAdapter.addNewPhoto(cr, uri);
946        } else {
947            android.util.Log.w(TAG, "Unknown new media with MIME type:"
948                    + mimeType + ", uri:" + uri);
949        }
950    }
951
952    @Override
953    public CameraProvider getCameraProvider() {
954        return mCameraController;
955    }
956
957    private void removeData(int dataID) {
958        mDataAdapter.removeData(CameraActivity.this, dataID);
959        if (mDataAdapter.getTotalNumber() > 1) {
960            showUndoDeletionBar();
961        } else {
962            // If camera preview is the only view left in filmstrip,
963            // no need to show undo bar.
964            mPendingDeletion = true;
965            performDeletion();
966        }
967    }
968
969    private void bindMediaSaveService() {
970        Intent intent = new Intent(this, MediaSaveService.class);
971        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
972    }
973
974    private void unbindMediaSaveService() {
975        if (mConnection != null) {
976            unbindService(mConnection);
977        }
978    }
979
980    @Override
981    public boolean onCreateOptionsMenu(Menu menu) {
982        // Inflate the menu items for use in the action bar
983        MenuInflater inflater = getMenuInflater();
984        inflater.inflate(R.menu.operations, menu);
985        mActionBarMenu = menu;
986
987        // Configure the standard share action provider
988        MenuItem item = menu.findItem(R.id.action_share);
989        mStandardShareActionProvider = (ShareActionProvider) item.getActionProvider();
990        mStandardShareActionProvider.setShareHistoryFileName("standard_share_history.xml");
991        if (mStandardShareIntent != null) {
992            mStandardShareActionProvider.setShareIntent(mStandardShareIntent);
993        }
994
995        // Configure the panorama share action provider
996        item = menu.findItem(R.id.action_share_panorama);
997        mPanoramaShareActionProvider = (ShareActionProvider) item.getActionProvider();
998        mPanoramaShareActionProvider.setShareHistoryFileName("panorama_share_history.xml");
999        if (mPanoramaShareIntent != null) {
1000            mPanoramaShareActionProvider.setShareIntent(mPanoramaShareIntent);
1001        }
1002
1003        mStandardShareActionProvider.setOnShareTargetSelectedListener(this);
1004        mPanoramaShareActionProvider.setOnShareTargetSelectedListener(this);
1005
1006        return super.onCreateOptionsMenu(menu);
1007    }
1008
1009    @Override
1010    public boolean onOptionsItemSelected(MenuItem item) {
1011        int currentDataId = mFilmstripController.getCurrentId();
1012        if (currentDataId < 0) {
1013            return false;
1014        }
1015        final LocalData localData = mDataAdapter.getLocalData(currentDataId);
1016
1017        // Handle presses on the action bar items
1018        switch (item.getItemId()) {
1019            case android.R.id.home:
1020                // ActionBar's Up/Home button was clicked
1021                try {
1022                    startActivity(IntentHelper.getGalleryIntent(this));
1023                    return true;
1024                } catch (ActivityNotFoundException e) {
1025                    Log.w(TAG, "Failed to launch gallery activity, closing");
1026                    finish();
1027                }
1028            case R.id.action_delete:
1029                UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
1030                        UsageStatistics.ACTION_DELETE, null, 0,
1031                        UsageStatistics.hashFileName(fileNameFromDataID(currentDataId)));
1032                removeData(currentDataId);
1033                return true;
1034            case R.id.action_edit:
1035                UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
1036                        UsageStatistics.ACTION_EDIT, null, 0,
1037                        UsageStatistics.hashFileName(fileNameFromDataID(currentDataId)));
1038                launchEditor(localData);
1039                return true;
1040            case R.id.action_trim: {
1041                // This is going to be handled by the Gallery app.
1042                Intent intent = new Intent(ACTION_TRIM_VIDEO);
1043                LocalData currentData = mDataAdapter.getLocalData(mFilmstripController.getCurrentId());
1044                intent.setData(currentData.getContentUri());
1045                // We need the file path to wrap this into a RandomAccessFile.
1046                intent.putExtra(MEDIA_ITEM_PATH, currentData.getPath());
1047                startActivityForResult(intent, REQ_CODE_DONT_SWITCH_TO_PREVIEW);
1048                return true;
1049            }
1050            case R.id.action_rotate_ccw:
1051                localData.rotate90Degrees(this, mDataAdapter, currentDataId, false);
1052                return true;
1053            case R.id.action_rotate_cw:
1054                localData.rotate90Degrees(this, mDataAdapter, currentDataId, true);
1055                return true;
1056            case R.id.action_crop: {
1057                UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
1058                        UsageStatistics.ACTION_CROP, null, 0,
1059                        UsageStatistics.hashFileName(fileNameFromDataID(currentDataId)));
1060                Intent intent = new Intent(CropActivity.CROP_ACTION);
1061                intent.setClass(this, CropActivity.class);
1062                intent.setDataAndType(localData.getContentUri(), localData.getMimeType())
1063                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1064                startActivityForResult(intent, REQ_CODE_DONT_SWITCH_TO_PREVIEW);
1065                return true;
1066            }
1067            case R.id.action_setas: {
1068                Intent intent = new Intent(Intent.ACTION_ATTACH_DATA)
1069                        .setDataAndType(localData.getContentUri(),
1070                                localData.getMimeType())
1071                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1072                intent.putExtra("mimeType", intent.getType());
1073                startActivityForResult(Intent.createChooser(
1074                        intent, getString(R.string.set_as)), REQ_CODE_DONT_SWITCH_TO_PREVIEW);
1075                return true;
1076            }
1077            case R.id.action_details:
1078                (new AsyncTask<Void, Void, MediaDetails>() {
1079                    @Override
1080                    protected MediaDetails doInBackground(Void... params) {
1081                        return localData.getMediaDetails(CameraActivity.this);
1082                    }
1083
1084                    @Override
1085                    protected void onPostExecute(MediaDetails mediaDetails) {
1086                        if (mediaDetails != null) {
1087                            DetailsDialog.create(CameraActivity.this, mediaDetails).show();
1088                        }
1089                    }
1090                }).execute();
1091                return true;
1092            case R.id.action_show_on_map:
1093                double[] latLong = localData.getLatLong();
1094                if (latLong != null) {
1095                    CameraUtil.showOnMap(this, latLong);
1096                }
1097                return true;
1098            default:
1099                return super.onOptionsItemSelected(item);
1100        }
1101    }
1102
1103    private boolean isCaptureIntent() {
1104        if (MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction())
1105                || MediaStore.ACTION_IMAGE_CAPTURE.equals(getIntent().getAction())
1106                || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(getIntent().getAction())) {
1107            return true;
1108        } else {
1109            return false;
1110        }
1111    }
1112
1113    @Override
1114    public void onCreate(Bundle state) {
1115        super.onCreate(state);
1116        GcamHelper.init(getContentResolver());
1117
1118        getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
1119        setContentView(R.layout.activity_main);
1120        mActionBar = getActionBar();
1121        mActionBar.addOnMenuVisibilityListener(this);
1122        mMainHandler = new MainHandler(getMainLooper());
1123        mCameraController =
1124                new CameraController(this, this, mMainHandler,
1125                        CameraManagerFactory.getAndroidCameraManager());
1126        // TODO: Try to move all the resources allocation to happen as soon as
1127        // possible so we can call module.init() at the earliest time.
1128        mModuleManager = new ModuleManagerImpl();
1129        setupModules();
1130
1131        mModeListView = (ModeListView) findViewById(R.id.mode_list_layout);
1132        if (mModeListView != null) {
1133            mModeListView.setModeSwitchListener(this);
1134        } else {
1135            Log.e(TAG, "Cannot find mode list in the view hierarchy");
1136        }
1137
1138        if (ApiHelper.HAS_ROTATION_ANIMATION) {
1139            setRotationAnimation();
1140        }
1141
1142        // Check if this is in the secure camera mode.
1143        Intent intent = getIntent();
1144        String action = intent.getAction();
1145        if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action)
1146                || ACTION_IMAGE_CAPTURE_SECURE.equals(action)) {
1147            mSecureCamera = true;
1148        } else {
1149            mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false);
1150        }
1151
1152        if (mSecureCamera) {
1153            // Change the window flags so that secure camera can show when locked
1154            Window win = getWindow();
1155            WindowManager.LayoutParams params = win.getAttributes();
1156            params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
1157            win.setAttributes(params);
1158
1159            // Filter for screen off so that we can finish secure camera activity
1160            // when screen is off.
1161            IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
1162            registerReceiver(mScreenOffReceiver, filter);
1163            // TODO: This static screen off event receiver is a workaround to the
1164            // double onResume() invocation (onResume->onPause->onResume). We should
1165            // find a better solution to this.
1166            if (sScreenOffReceiver == null) {
1167                sScreenOffReceiver = new ScreenOffReceiver();
1168                registerReceiver(sScreenOffReceiver, filter);
1169            }
1170        }
1171        mAboveFilmstripControlLayout =
1172                (FrameLayout) findViewById(R.id.camera_above_filmstrip_layout);
1173        mAboveFilmstripControlLayout.setFitsSystemWindows(true);
1174        // Hide action bar first since we are in full screen mode first, and
1175        // switch the system UI to lights-out mode.
1176        this.setSystemBarsVisibility(false);
1177        mPanoramaManager = AppManagerFactory.getInstance(this)
1178                .getPanoramaStitchingManager();
1179        mPlaceholderManager = AppManagerFactory.getInstance(this)
1180                .getGcamProcessingManager();
1181        mPanoramaManager.addTaskListener(mStitchingListener);
1182        mPlaceholderManager.addTaskListener(mPlaceholderListener);
1183        LayoutInflater inflater = getLayoutInflater();
1184        View rootLayout = inflater.inflate(R.layout.camera, null, false);
1185        mCameraModuleRootView = (FrameLayout) rootLayout.findViewById(R.id.camera_app_root);
1186        mPanoStitchingPanel = findViewById(R.id.pano_stitching_progress_panel);
1187        mBottomProgress = (ProgressBar) findViewById(R.id.pano_stitching_progress_bar);
1188        mCameraPreviewData = new CameraPreviewData(rootLayout,
1189                FilmstripImageData.SIZE_FULL,
1190                FilmstripImageData.SIZE_FULL);
1191        // Put a CameraPreviewData at the first position.
1192        mWrappedDataAdapter = new FixedFirstDataAdapter(
1193                new CameraDataAdapter(new ColorDrawable(
1194                        getResources().getColor(R.color.photo_placeholder))),
1195                mCameraPreviewData);
1196        mFilmstripController = ((FilmstripView) findViewById(R.id.filmstrip_view)).getController();
1197        mFilmstripController.setViewGap(
1198                getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap));
1199        mPanoramaViewHelper = new PanoramaViewHelper(this);
1200        mPanoramaViewHelper.onCreate();
1201        mFilmstripController.setPanoramaViewHelper(mPanoramaViewHelper);
1202        // Set up the camera preview first so the preview shows up ASAP.
1203        mFilmstripController.setListener(mFilmStripListener);
1204
1205        int moduleIndex = -1;
1206        if (MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(getIntent().getAction())
1207                || MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction())) {
1208            moduleIndex = ModeListView.MODE_VIDEO;
1209        } else if (MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(getIntent().getAction())
1210                || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(getIntent()
1211                        .getAction())) {
1212            moduleIndex = ModeListView.MODE_PHOTO;
1213            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
1214            if (prefs.getInt(CameraSettings.KEY_STARTUP_MODULE_INDEX, -1)
1215                        == ModeListView.MODE_GCAM && GcamHelper.hasGcamCapture()) {
1216                moduleIndex = ModeListView.MODE_GCAM;
1217            }
1218        } else if (MediaStore.ACTION_IMAGE_CAPTURE.equals(getIntent().getAction())
1219                || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(getIntent().getAction())) {
1220            moduleIndex = ModeListView.MODE_PHOTO;
1221        } else {
1222            // If the activity has not been started using an explicit intent,
1223            // read the module index from the last time the user changed modes
1224            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
1225            moduleIndex = prefs.getInt(CameraSettings.KEY_STARTUP_MODULE_INDEX, -1);
1226            if ((moduleIndex == ModeListView.MODE_GCAM &&
1227                    !GcamHelper.hasGcamCapture()) || moduleIndex < 0) {
1228                moduleIndex = ModeListView.MODE_PHOTO;
1229            }
1230        }
1231
1232        mOrientationManager = new OrientationManagerImpl(this);
1233        mOrientationManager.addOnOrientationChangeListener(mMainHandler, this);
1234        setModuleFromIndex(moduleIndex);
1235        mCurrentModule.init(this, mCameraModuleRootView);
1236
1237        if (!mSecureCamera) {
1238            mDataAdapter = mWrappedDataAdapter;
1239            mFilmstripController.setDataAdapter(mDataAdapter);
1240            if (!isCaptureIntent()) {
1241                mDataAdapter.requestLoad(getContentResolver());
1242            }
1243        } else {
1244            // Put a lock placeholder as the last image by setting its date to
1245            // 0.
1246            ImageView v = (ImageView) getLayoutInflater().inflate(
1247                    R.layout.secure_album_placeholder, null);
1248            v.setOnClickListener(new View.OnClickListener() {
1249                @Override
1250                public void onClick(View view) {
1251                    try {
1252                        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
1253                                UsageStatistics.ACTION_GALLERY, null);
1254                        startActivity(IntentHelper.getGalleryIntent(CameraActivity.this));
1255                    } catch (ActivityNotFoundException e) {
1256                        Log.w(TAG, "Failed to launch gallery activity, closing");
1257                    }
1258                    finish();
1259                }
1260            });
1261            mDataAdapter = new FixedLastDataAdapter(
1262                    mWrappedDataAdapter,
1263                    new SimpleViewData(
1264                            v,
1265                            v.getDrawable().getIntrinsicWidth(),
1266                            v.getDrawable().getIntrinsicHeight(),
1267                            0, 0));
1268            // Flush out all the original data.
1269            mDataAdapter.flush();
1270            mFilmstripController.setDataAdapter(mDataAdapter);
1271        }
1272
1273        setupNfcBeamPush();
1274
1275        mLocalImagesObserver = new LocalMediaObserver();
1276        mLocalVideosObserver = new LocalMediaObserver();
1277
1278        getContentResolver().registerContentObserver(
1279                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true,
1280                mLocalImagesObserver);
1281        getContentResolver().registerContentObserver(
1282                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true,
1283                mLocalVideosObserver);
1284    }
1285
1286    private void setRotationAnimation() {
1287        int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
1288        rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
1289        Window win = getWindow();
1290        WindowManager.LayoutParams winParams = win.getAttributes();
1291        winParams.rotationAnimation = rotationAnimation;
1292        win.setAttributes(winParams);
1293    }
1294
1295    @Override
1296    public void onUserInteraction() {
1297        super.onUserInteraction();
1298        mCurrentModule.onUserInteraction();
1299    }
1300
1301    @Override
1302    public boolean dispatchTouchEvent(MotionEvent ev) {
1303        boolean result = super.dispatchTouchEvent(ev);
1304        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
1305            // Real deletion is postponed until the next user interaction after
1306            // the gesture that triggers deletion. Until real deletion is performed,
1307            // users can click the undo button to bring back the image that they
1308            // chose to delete.
1309            if (mPendingDeletion && !mIsUndoingDeletion) {
1310                performDeletion();
1311            }
1312        }
1313        return result;
1314    }
1315
1316    @Override
1317    public void onPause() {
1318        // Delete photos that are pending deletion
1319        performDeletion();
1320        // TODO: call mCurrentModule.pause() instead after all the modules
1321        // support pause().
1322        mCurrentModule.onPauseBeforeSuper();
1323        mOrientationManager.pause();
1324        // Close the camera and wait for the operation done.
1325        mCameraController.closeCamera();
1326        super.onPause();
1327        mCurrentModule.onPauseAfterSuper();
1328
1329        mLocalImagesObserver.setActivityPaused(true);
1330        mLocalVideosObserver.setActivityPaused(true);
1331    }
1332
1333    @Override
1334    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1335        if (requestCode == REQ_CODE_DONT_SWITCH_TO_PREVIEW) {
1336            mResetToPreviewOnResume = false;
1337        } else {
1338            super.onActivityResult(requestCode, resultCode, data);
1339        }
1340    }
1341
1342    @Override
1343    public void onResume() {
1344        // TODO: Handle this in OrientationManager.
1345        // Auto-rotate off
1346        if (Settings.System.getInt(getContentResolver(),
1347                Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {
1348            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
1349            mAutoRotateScreen = false;
1350        } else {
1351            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
1352            mAutoRotateScreen = true;
1353        }
1354
1355        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
1356                UsageStatistics.ACTION_FOREGROUNDED, this.getClass().getSimpleName());
1357
1358        mOrientationManager.resume();
1359        // TODO: call mCurrentModule.resume() instead after all the modules
1360        // support resume().
1361        mCurrentModule.onResumeBeforeSuper();
1362        super.onResume();
1363        mCurrentModule.onResumeAfterSuper();
1364
1365        setSwipingEnabled(true);
1366
1367        if (mResetToPreviewOnResume) {
1368            // Go to the preview on resume.
1369            mFilmstripController.goToFirstItem();
1370        }
1371        // Default is showing the preview, unless disabled by explicitly
1372        // starting an activity we want to return from to the filmstrip rather
1373        // than the preview.
1374        mResetToPreviewOnResume = true;
1375
1376        if (mLocalVideosObserver.isMediaDataChangedDuringPause()
1377                || mLocalImagesObserver.isMediaDataChangedDuringPause()) {
1378            if (!mSecureCamera) {
1379                // If it's secure camera, requestLoad() should not be called
1380                // as it will load all the data.
1381                mDataAdapter.requestLoad(getContentResolver());
1382            }
1383        }
1384        mLocalImagesObserver.setActivityPaused(false);
1385        mLocalVideosObserver.setActivityPaused(false);
1386
1387        mModeListView.startAccordionAnimation();
1388    }
1389
1390    @Override
1391    public void onStart() {
1392        super.onStart();
1393        bindMediaSaveService();
1394        mPanoramaViewHelper.onStart();
1395    }
1396
1397    @Override
1398    protected void onStop() {
1399        mPanoramaViewHelper.onStop();
1400        unbindMediaSaveService();
1401
1402        CameraManagerFactory.recycle();
1403        super.onStop();
1404    }
1405
1406    @Override
1407    public void onDestroy() {
1408        if (mSecureCamera) {
1409            unregisterReceiver(mScreenOffReceiver);
1410        }
1411        getContentResolver().unregisterContentObserver(mLocalImagesObserver);
1412        getContentResolver().unregisterContentObserver(mLocalVideosObserver);
1413        super.onDestroy();
1414    }
1415
1416    @Override
1417    public void onConfigurationChanged(Configuration config) {
1418        super.onConfigurationChanged(config);
1419        mCurrentModule.onConfigurationChanged(config);
1420    }
1421
1422    @Override
1423    public boolean onKeyDown(int keyCode, KeyEvent event) {
1424        if (mFilmstripController.inCameraFullscreen()) {
1425            if (mCurrentModule.onKeyDown(keyCode, event)) {
1426                return true;
1427            }
1428            // Prevent software keyboard or voice search from showing up.
1429            if (keyCode == KeyEvent.KEYCODE_SEARCH
1430                    || keyCode == KeyEvent.KEYCODE_MENU) {
1431                if (event.isLongPress()) {
1432                    return true;
1433                }
1434            }
1435        }
1436
1437        return super.onKeyDown(keyCode, event);
1438    }
1439
1440    @Override
1441    public boolean onKeyUp(int keyCode, KeyEvent event) {
1442        if (mFilmstripController.inCameraFullscreen() && mCurrentModule.onKeyUp(keyCode, event)) {
1443            return true;
1444        }
1445        return super.onKeyUp(keyCode, event);
1446    }
1447
1448    @Override
1449    public void onBackPressed() {
1450        if (!mFilmstripController.inCameraFullscreen()) {
1451            mFilmstripController.goToFirstItem();
1452        } else if (!mCurrentModule.onBackPressed()) {
1453            super.onBackPressed();
1454        }
1455    }
1456
1457    public boolean isAutoRotateScreen() {
1458        return mAutoRotateScreen;
1459    }
1460
1461    protected void updateStorageSpace() {
1462        mStorageSpaceBytes = Storage.getAvailableSpace();
1463    }
1464
1465    protected long getStorageSpaceBytes() {
1466        return mStorageSpaceBytes;
1467    }
1468
1469    protected void updateStorageSpaceAndHint() {
1470        updateStorageSpace();
1471        updateStorageHint(mStorageSpaceBytes);
1472    }
1473
1474    protected void updateStorageHint(long storageSpace) {
1475        String message = null;
1476        if (storageSpace == Storage.UNAVAILABLE) {
1477            message = getString(R.string.no_storage);
1478        } else if (storageSpace == Storage.PREPARING) {
1479            message = getString(R.string.preparing_sd);
1480        } else if (storageSpace == Storage.UNKNOWN_SIZE) {
1481            message = getString(R.string.access_sd_fail);
1482        } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD_BYTES) {
1483            message = getString(R.string.spaceIsLow_content);
1484        }
1485
1486        if (message != null) {
1487            if (mStorageHint == null) {
1488                mStorageHint = OnScreenHint.makeText(this, message);
1489            } else {
1490                mStorageHint.setText(message);
1491            }
1492            mStorageHint.show();
1493        } else if (mStorageHint != null) {
1494            mStorageHint.cancel();
1495            mStorageHint = null;
1496        }
1497    }
1498
1499    protected void setResultEx(int resultCode) {
1500        mResultCodeForTesting = resultCode;
1501        setResult(resultCode);
1502    }
1503
1504    protected void setResultEx(int resultCode, Intent data) {
1505        mResultCodeForTesting = resultCode;
1506        mResultDataForTesting = data;
1507        setResult(resultCode, data);
1508    }
1509
1510    public int getResultCode() {
1511        return mResultCodeForTesting;
1512    }
1513
1514    public Intent getResultData() {
1515        return mResultDataForTesting;
1516    }
1517
1518    public boolean isSecureCamera() {
1519        return mSecureCamera;
1520    }
1521
1522    @Override
1523    public void onModeSelected(int moduleIndex) {
1524        if (mCurrentModuleIndex == moduleIndex) {
1525            return;
1526        }
1527
1528        closeModule(mCurrentModule);
1529        setModuleFromIndex(moduleIndex);
1530
1531        openModule(mCurrentModule);
1532        mCurrentModule.onOrientationChanged(mLastRawOrientation);
1533        if (mMediaSaver != null) {
1534            mCurrentModule.onMediaSaverAvailable(mMediaSaver);
1535        }
1536
1537        // Store the module index so we can use it the next time the Camera
1538        // starts up.
1539        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
1540        prefs.edit().putInt(CameraSettings.KEY_STARTUP_MODULE_INDEX, moduleIndex).apply();
1541    }
1542
1543    /**
1544     * Sets the mCurrentModuleIndex, creates a new module instance for the given
1545     * index an sets it as mCurrentModule.
1546     */
1547    private void setModuleFromIndex(int moduleIndex) {
1548        ModuleManagerImpl.ModuleAgent agent = mModuleManager.getModuleAgent(moduleIndex);
1549        if (agent == null) {
1550            return;
1551        }
1552        if (!agent.requestAppForCamera()) {
1553            mCameraController.closeCamera();
1554        }
1555        mCurrentModuleIndex = agent.getModuleId();
1556        mCurrentModule2 = agent.createModule();
1557        mCurrentModule = (CameraModule) mCurrentModule2;
1558    }
1559
1560    /**
1561     * Launches an ACTION_EDIT intent for the given local data item.
1562     */
1563    public void launchEditor(LocalData data) {
1564        Intent intent = new Intent(Intent.ACTION_EDIT)
1565                .setDataAndType(data.getContentUri(), data.getMimeType())
1566                .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1567        try {
1568            startActivityForResult(intent, REQ_CODE_DONT_SWITCH_TO_PREVIEW);
1569        } catch (ActivityNotFoundException e) {
1570            startActivityForResult(Intent.createChooser(intent, null),
1571                    REQ_CODE_DONT_SWITCH_TO_PREVIEW);
1572        }
1573    }
1574
1575    /**
1576     * Launch the tiny planet editor.
1577     *
1578     * @param data The data must be a 360 degree stereographically mapped
1579     *             panoramic image. It will not be modified, instead a new item
1580     *             with the result will be added to the filmstrip.
1581     */
1582    public void launchTinyPlanetEditor(LocalData data) {
1583        TinyPlanetFragment fragment = new TinyPlanetFragment();
1584        Bundle bundle = new Bundle();
1585        bundle.putString(TinyPlanetFragment.ARGUMENT_URI, data.getContentUri().toString());
1586        bundle.putString(TinyPlanetFragment.ARGUMENT_TITLE, data.getTitle());
1587        fragment.setArguments(bundle);
1588        fragment.show(getFragmentManager(), "tiny_planet");
1589    }
1590
1591    private void openModule(CameraModule module) {
1592        module.init(this, mCameraModuleRootView);
1593        // TODO: call mCurrentModule.resume() instead after all the modules
1594        // support resume().
1595        module.onResumeBeforeSuper();
1596        module.onResumeAfterSuper();
1597    }
1598
1599    private void closeModule(CameraModule module) {
1600        // TODO: call mCurrentModule.pause() instead after all the modules
1601        // support pause().
1602        module.onPauseBeforeSuper();
1603        module.onPauseAfterSuper();
1604        ((ViewGroup) mCameraModuleRootView).removeAllViews();
1605    }
1606
1607    private void performDeletion() {
1608        if (!mPendingDeletion) {
1609            return;
1610        }
1611        hideUndoDeletionBar(false);
1612        mDataAdapter.executeDeletion(CameraActivity.this);
1613
1614        int currentId = mFilmstripController.getCurrentId();
1615        updateActionBarMenu(currentId);
1616        mFilmStripListener.onCurrentDataCentered(currentId);
1617    }
1618
1619    public void showUndoDeletionBar() {
1620        if (mPendingDeletion) {
1621            performDeletion();
1622        }
1623        Log.v(TAG, "showing undo bar");
1624        mPendingDeletion = true;
1625        if (mUndoDeletionBar == null) {
1626            ViewGroup v = (ViewGroup) getLayoutInflater().inflate(R.layout.undo_bar,
1627                    mAboveFilmstripControlLayout, true);
1628            mUndoDeletionBar = (ViewGroup) v.findViewById(R.id.camera_undo_deletion_bar);
1629            View button = mUndoDeletionBar.findViewById(R.id.camera_undo_deletion_button);
1630            button.setOnClickListener(new View.OnClickListener() {
1631                @Override
1632                public void onClick(View view) {
1633                    mDataAdapter.undoDataRemoval();
1634                    hideUndoDeletionBar(true);
1635                }
1636            });
1637            // Setting undo bar clickable to avoid touch events going through
1638            // the bar to the buttons (eg. edit button, etc) underneath the bar.
1639            mUndoDeletionBar.setClickable(true);
1640            // When there is user interaction going on with the undo button, we
1641            // do not want to hide the undo bar.
1642            button.setOnTouchListener(new View.OnTouchListener() {
1643                @Override
1644                public boolean onTouch(View v, MotionEvent event) {
1645                    if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1646                        mIsUndoingDeletion = true;
1647                    } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1648                        mIsUndoingDeletion = false;
1649                    }
1650                    return false;
1651                }
1652            });
1653        }
1654        mUndoDeletionBar.setAlpha(0f);
1655        mUndoDeletionBar.setVisibility(View.VISIBLE);
1656        mUndoDeletionBar.animate().setDuration(200).alpha(1f).setListener(null).start();
1657    }
1658
1659    private void hideUndoDeletionBar(boolean withAnimation) {
1660        Log.v(TAG, "Hiding undo deletion bar");
1661        mPendingDeletion = false;
1662        if (mUndoDeletionBar != null) {
1663            if (withAnimation) {
1664                mUndoDeletionBar.animate().setDuration(200).alpha(0f)
1665                        .setListener(new Animator.AnimatorListener() {
1666                            @Override
1667                            public void onAnimationStart(Animator animation) {
1668                                // Do nothing.
1669                            }
1670
1671                            @Override
1672                            public void onAnimationEnd(Animator animation) {
1673                                mUndoDeletionBar.setVisibility(View.GONE);
1674                            }
1675
1676                            @Override
1677                            public void onAnimationCancel(Animator animation) {
1678                                // Do nothing.
1679                            }
1680
1681                            @Override
1682                            public void onAnimationRepeat(Animator animation) {
1683                                // Do nothing.
1684                            }
1685                        }).start();
1686            } else {
1687                mUndoDeletionBar.setVisibility(View.GONE);
1688            }
1689        }
1690    }
1691
1692    @Override
1693    public void onOrientationChanged(int orientation) {
1694        // We keep the last known orientation. So if the user first orient
1695        // the camera then point the camera to floor or sky, we still have
1696        // the correct orientation.
1697        if (orientation == OrientationManager.ORIENTATION_UNKNOWN) {
1698            return;
1699        }
1700        mLastRawOrientation = orientation;
1701        if (mCurrentModule != null) {
1702            mCurrentModule.onOrientationChanged(orientation);
1703        }
1704    }
1705
1706    /**
1707     * Enable/disable swipe-to-filmstrip. Will always disable swipe if in
1708     * capture intent.
1709     *
1710     * @param enable {@code true} to enable swipe.
1711     */
1712    public void setSwipingEnabled(boolean enable) {
1713        if (isCaptureIntent()) {
1714            mCameraPreviewData.lockPreview(true);
1715        } else {
1716            mCameraPreviewData.lockPreview(!enable);
1717        }
1718    }
1719
1720
1721    /**
1722     * Check whether camera controls are visible.
1723     *
1724     * @return whether controls are visible.
1725     */
1726    private boolean arePreviewControlsVisible() {
1727        return mCurrentModule.arePreviewControlsVisible();
1728    }
1729
1730    /**
1731     * Show or hide the {@link CameraControls} using the current module's
1732     * implementation of {@link #onPreviewFocusChanged}.
1733     *
1734     * @param showControls whether to show camera controls.
1735     */
1736    private void setPreviewControlsVisibility(boolean showControls) {
1737        mCurrentModule.onPreviewFocusChanged(showControls);
1738    }
1739
1740    // Accessor methods for getting latency times used in performance testing
1741    public long getAutoFocusTime() {
1742        return (mCurrentModule instanceof PhotoModule) ?
1743                ((PhotoModule) mCurrentModule).mAutoFocusTime : -1;
1744    }
1745
1746    public long getShutterLag() {
1747        return (mCurrentModule instanceof PhotoModule) ?
1748                ((PhotoModule) mCurrentModule).mShutterLag : -1;
1749    }
1750
1751    public long getShutterToPictureDisplayedTime() {
1752        return (mCurrentModule instanceof PhotoModule) ?
1753                ((PhotoModule) mCurrentModule).mShutterToPictureDisplayedTime : -1;
1754    }
1755
1756    public long getPictureDisplayedToJpegCallbackTime() {
1757        return (mCurrentModule instanceof PhotoModule) ?
1758                ((PhotoModule) mCurrentModule).mPictureDisplayedToJpegCallbackTime : -1;
1759    }
1760
1761    public long getJpegCallbackFinishTime() {
1762        return (mCurrentModule instanceof PhotoModule) ?
1763                ((PhotoModule) mCurrentModule).mJpegCallbackFinishTime : -1;
1764    }
1765
1766    public long getCaptureStartTime() {
1767        return (mCurrentModule instanceof PhotoModule) ?
1768                ((PhotoModule) mCurrentModule).mCaptureStartTime : -1;
1769    }
1770
1771    public boolean isRecording() {
1772        return (mCurrentModule instanceof VideoModule) ?
1773                ((VideoModule) mCurrentModule).isRecording() : false;
1774    }
1775
1776    public CameraManager.CameraOpenCallback getCameraOpenErrorCallback() {
1777        return mCameraController;
1778    }
1779
1780    // For debugging purposes only.
1781    public CameraModule getCurrentModule() {
1782        return mCurrentModule;
1783    }
1784
1785    private void setupModules() {
1786        // PhotoModule, the default module.
1787        mModuleManager.registerModule(new ModuleManagerImpl.ModuleAgent() {
1788            @Override
1789            public int getModuleId() {
1790                return ModeListView.MODE_PHOTO;
1791            }
1792
1793            @Override
1794            public boolean requestAppForCamera() {
1795                return true;
1796            }
1797
1798            @Override
1799            public ModuleController createModule() {
1800                return new PhotoModule();
1801            }
1802        });
1803        mModuleManager.setDefaultModuleIndex(ModeListView.MODE_PHOTO);
1804
1805        // VideoModule.
1806        mModuleManager.registerModule(new ModuleManagerImpl.ModuleAgent() {
1807            @Override
1808            public int getModuleId() {
1809                return ModeListView.MODE_VIDEO;
1810            }
1811
1812            @Override
1813            public boolean requestAppForCamera() {
1814                return true;
1815            }
1816
1817            @Override
1818            public ModuleController createModule() {
1819                return new VideoModule();
1820            }
1821        });
1822
1823        // WideAngle.
1824        mModuleManager.registerModule(new ModuleManagerImpl.ModuleAgent() {
1825            @Override
1826            public int getModuleId() {
1827                return ModeListView.MODE_WIDEANGLE;
1828            }
1829
1830            @Override
1831            public boolean requestAppForCamera() {
1832                return false;
1833            }
1834
1835            @Override
1836            public ModuleController createModule() {
1837                // TODO: remove the type casting.
1838                return new WideAnglePanoramaModule();
1839            }
1840        });
1841
1842        // PhotoSphere.
1843        if (PhotoSphereHelper.hasLightCycleCapture(this)) {
1844            mModuleManager.registerModule(new ModuleManagerImpl.ModuleAgent() {
1845                @Override
1846                public int getModuleId() {
1847                    return ModeListView.MODE_PHOTOSPHERE;
1848                }
1849
1850                @Override
1851                public boolean requestAppForCamera() {
1852                    return false;
1853                }
1854
1855                @Override
1856                public ModuleController createModule() {
1857                    // TODO: remove the type casting.
1858                    return (ModuleController) PhotoSphereHelper.createPanoramaModule();
1859                }
1860            });
1861        }
1862
1863        // Refocus.
1864        if (RefocusHelper.hasRefocusCapture(this)) {
1865            mModuleManager.registerModule(new ModuleManagerImpl.ModuleAgent() {
1866                @Override
1867                public int getModuleId() {
1868                    return ModeListView.MODE_CRAFT;
1869                }
1870
1871                @Override
1872                public boolean requestAppForCamera() {
1873                    return false;
1874                }
1875
1876                @Override
1877                public ModuleController createModule() {
1878                    // TODO: remove the type casting.
1879                    return (ModuleController) RefocusHelper.createRefocusModule();
1880                }
1881            });
1882        }
1883
1884        // Gcam for HDR+.
1885        if (GcamHelper.hasGcamCapture()) {
1886            mModuleManager.registerModule(new ModuleManagerImpl.ModuleAgent() {
1887                @Override
1888                public int getModuleId() {
1889                    return ModeListView.MODE_GCAM;
1890                }
1891
1892                @Override
1893                public boolean requestAppForCamera() {
1894                    return false;
1895                }
1896
1897                @Override
1898                public ModuleController createModule() {
1899                    return (ModuleController) GcamHelper.createGcamModule();
1900                }
1901            });
1902        }
1903    }
1904}
1905