PhotoViewActivity.java revision 40a26567ebaf6311bd7a102491d669abd1d68646
1/*
2 * Copyright (C) 2011 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.ex.photo;
19
20import android.app.ActionBar;
21import android.app.ActionBar.OnMenuVisibilityListener;
22import android.app.Activity;
23import android.app.ActivityManager;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.net.Uri;
29import android.os.Build;
30import android.os.Bundle;
31import android.os.Handler;
32import android.support.v4.app.Fragment;
33import android.support.v4.app.FragmentActivity;
34import android.support.v4.app.LoaderManager;
35import android.support.v4.content.Loader;
36import android.support.v4.view.ViewPager.OnPageChangeListener;
37import android.text.TextUtils;
38import android.view.MenuItem;
39import android.view.View;
40
41import com.android.ex.photo.PhotoViewPager.InterceptType;
42import com.android.ex.photo.PhotoViewPager.OnInterceptTouchListener;
43import com.android.ex.photo.adapters.PhotoPagerAdapter;
44import com.android.ex.photo.fragments.PhotoViewFragment;
45import com.android.ex.photo.loaders.PhotoPagerLoader;
46import com.android.ex.photo.provider.PhotoContract;
47
48import java.util.HashSet;
49import java.util.Set;
50
51/**
52 * Activity to view the contents of an album.
53 */
54public class PhotoViewActivity extends FragmentActivity implements
55        LoaderManager.LoaderCallbacks<Cursor>, OnPageChangeListener, OnInterceptTouchListener,
56        OnMenuVisibilityListener, PhotoViewCallbacks {
57
58    private final static String STATE_ITEM_KEY =
59            "com.google.android.apps.plus.PhotoViewFragment.ITEM";
60    private final static String STATE_FULLSCREEN_KEY =
61            "com.google.android.apps.plus.PhotoViewFragment.FULLSCREEN";
62
63    private static final int LOADER_PHOTO_LIST = 1;
64
65    /** Count used when the real photo count is unknown [but, may be determined] */
66    public static final int ALBUM_COUNT_UNKNOWN = -1;
67
68    /** Argument key for the dialog message */
69    public static final String KEY_MESSAGE = "dialog_message";
70
71    public static int sMemoryClass;
72
73    /** The URI of the photos we're viewing; may be {@code null} */
74    private String mPhotosUri;
75    /** The URI of the initial photo to display */
76    private String mInitialPhotoUri;
77    /** The index of the currently viewed photo */
78    private int mPhotoIndex;
79    /** The query projection to use; may be {@code null} */
80    private String[] mProjection;
81    /** The total number of photos; only valid if {@link #mIsEmpty} is {@code false}. */
82    private int mAlbumCount = ALBUM_COUNT_UNKNOWN;
83    /** {@code true} if the view is empty. Otherwise, {@code false}. */
84    private boolean mIsEmpty;
85    /** the main root view */
86    protected View mRootView;
87    /** The main pager; provides left/right swipe between photos */
88    protected PhotoViewPager mViewPager;
89    /** Adapter to create pager views */
90    protected PhotoPagerAdapter mAdapter;
91    /** Whether or not we're in "full screen" mode */
92    private boolean mFullScreen;
93    /** The set of listeners wanting full screen state */
94    private Set<OnScreenListener> mScreenListeners = new HashSet<OnScreenListener>();
95    /** The set of listeners wanting full screen state */
96    private Set<CursorChangedListener> mCursorListeners = new HashSet<CursorChangedListener>();
97    /** When {@code true}, restart the loader when the activity becomes active */
98    private boolean mRestartLoader;
99    /** Whether or not this activity is paused */
100    private boolean mIsPaused = true;
101    /** The maximum scale factor applied to images when they are initially displayed */
102    private float mMaxInitialScale;
103    private final Handler mHandler = new Handler();
104    // TODO Find a better way to do this. We basically want the activity to display the
105    // "loading..." progress until the fragment takes over and shows it's own "loading..."
106    // progress [located in photo_header_view.xml]. We could potentially have all status displayed
107    // by the activity, but, that gets tricky when it comes to screen rotation. For now, we
108    // track the loading by this variable which is fragile and may cause phantom "loading..."
109    // text.
110    private long mEnterFullScreenDelayTime;
111
112    protected PhotoPagerAdapter createPhotoPagerAdapter(Context context,
113            android.support.v4.app.FragmentManager fm, Cursor c, float maxScale) {
114        return new PhotoPagerAdapter(context, fm, c, maxScale);
115    }
116
117    @Override
118    protected void onCreate(Bundle savedInstanceState) {
119        super.onCreate(savedInstanceState);
120
121        final ActivityManager mgr = (ActivityManager) getApplicationContext().
122                getSystemService(Activity.ACTIVITY_SERVICE);
123        sMemoryClass = mgr.getMemoryClass();
124
125        Intent mIntent = getIntent();
126
127        int currentItem = -1;
128        if (savedInstanceState != null) {
129            currentItem = savedInstanceState.getInt(STATE_ITEM_KEY, -1);
130            mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false);
131        }
132
133        // uri of the photos to view; optional
134        if (mIntent.hasExtra(Intents.EXTRA_PHOTOS_URI)) {
135            mPhotosUri = mIntent.getStringExtra(Intents.EXTRA_PHOTOS_URI);
136        }
137
138        // projection for the query; optional
139        // I.f not set, the default projection is used.
140        // This projection must include the columns from the default projection.
141        if (mIntent.hasExtra(Intents.EXTRA_PROJECTION)) {
142            mProjection = mIntent.getStringArrayExtra(Intents.EXTRA_PROJECTION);
143        } else {
144            mProjection = null;
145        }
146
147        // Set the current item from the intent if wasn't in the saved instance
148        if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX) && currentItem < 0) {
149            currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
150        }
151        if (mIntent.hasExtra(Intents.EXTRA_INITIAL_PHOTO_URI) && currentItem < 0) {
152            mInitialPhotoUri = mIntent.getStringExtra(Intents.EXTRA_INITIAL_PHOTO_URI);
153        }
154
155        // Set the max initial scale, defaulting to 1x
156        mMaxInitialScale = mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1.0f);
157
158        mPhotoIndex = currentItem;
159
160        setContentView(R.layout.photo_activity_view);
161
162        // Create the adapter and add the view pager
163        mAdapter = createPhotoPagerAdapter(this, getSupportFragmentManager(),
164            null, mMaxInitialScale);
165        final Resources resources = getResources();
166        mRootView = findViewById(R.id.photo_activity_root_view);
167        mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
168        mViewPager.setOnPageChangeListener(this);
169        mViewPager.setOnInterceptTouchListener(this);
170        mViewPager.setPageMargin(resources.getDimensionPixelSize(R.dimen.photo_page_margin));
171
172        // Kick off the loader
173        getSupportLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
174
175        mEnterFullScreenDelayTime =
176                resources.getInteger(R.integer.reenter_fullscreen_delay_time_in_millis);
177
178        final ActionBar actionBar = getActionBar();
179        if (actionBar != null) {
180            actionBar.setDisplayHomeAsUpEnabled(true);
181            actionBar.addOnMenuVisibilityListener(this);
182            actionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
183        }
184    }
185
186    @Override
187    protected void onResume() {
188        super.onResume();
189        setFullScreen(mFullScreen, false);
190
191        mIsPaused = false;
192        if (mRestartLoader) {
193            mRestartLoader = false;
194            getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
195        }
196    }
197
198    @Override
199    protected void onPause() {
200        mIsPaused = true;
201
202        super.onPause();
203    }
204
205    @Override
206    public void onBackPressed() {
207        // If in full screen mode, toggle mode & eat the 'back'
208        if (mFullScreen) {
209            toggleFullScreen();
210        } else {
211            super.onBackPressed();
212        }
213    }
214
215    @Override
216    public void onSaveInstanceState(Bundle outState) {
217        super.onSaveInstanceState(outState);
218
219        outState.putInt(STATE_ITEM_KEY, mViewPager.getCurrentItem());
220        outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
221    }
222
223    @Override
224    public boolean onOptionsItemSelected(MenuItem item) {
225       switch (item.getItemId()) {
226          case android.R.id.home:
227             finish();
228          default:
229             return super.onOptionsItemSelected(item);
230       }
231    }
232
233    @Override
234    public void addScreenListener(OnScreenListener listener) {
235        mScreenListeners.add(listener);
236    }
237
238    @Override
239    public void removeScreenListener(OnScreenListener listener) {
240        mScreenListeners.remove(listener);
241    }
242
243    @Override
244    public synchronized void addCursorListener(CursorChangedListener listener) {
245        mCursorListeners.add(listener);
246    }
247
248    @Override
249    public synchronized void removeCursorListener(CursorChangedListener listener) {
250        mCursorListeners.remove(listener);
251    }
252
253    @Override
254    public boolean isFragmentFullScreen(Fragment fragment) {
255        if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) {
256            return mFullScreen;
257        }
258        return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment));
259    }
260
261    @Override
262    public void toggleFullScreen() {
263        setFullScreen(!mFullScreen, true);
264    }
265
266    public void onPhotoRemoved(long photoId) {
267        final Cursor data = mAdapter.getCursor();
268        if (data == null) {
269            // Huh?! How would this happen?
270            return;
271        }
272
273        final int dataCount = data.getCount();
274        if (dataCount <= 1) {
275            finish();
276            return;
277        }
278
279        getSupportLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
280    }
281
282    @Override
283    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
284        if (id == LOADER_PHOTO_LIST) {
285            return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection);
286        }
287        return null;
288    }
289
290    @Override
291    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
292        final int id = loader.getId();
293        if (id == LOADER_PHOTO_LIST) {
294            if (data == null || data.getCount() == 0) {
295                mIsEmpty = true;
296            } else {
297                mAlbumCount = data.getCount();
298
299                if (mInitialPhotoUri != null) {
300                    int index = 0;
301                    int uriIndex = data.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
302                    while (data.moveToNext()) {
303                        String uri = data.getString(uriIndex);
304                        if (TextUtils.equals(uri, mInitialPhotoUri)) {
305                            mInitialPhotoUri = null;
306                            mPhotoIndex = index;
307                            break;
308                        }
309                        index++;
310                    }
311                }
312
313                // We're paused; don't do anything now, we'll get re-invoked
314                // when the activity becomes active again
315                // TODO(pwestbro): This shouldn't be necessary, as the loader manager should
316                // restart the loader
317                if (mIsPaused) {
318                    mRestartLoader = true;
319                    return;
320                }
321                mIsEmpty = false;
322
323                mAdapter.swapCursor(data);
324                if (mViewPager.getAdapter() == null) {
325                    mViewPager.setAdapter(mAdapter);
326                }
327                notifyCursorListeners(data);
328
329                // set the selected photo
330                int itemIndex = mPhotoIndex;
331
332                // Use an index of 0 if the index wasn't specified or couldn't be found
333                if (itemIndex < 0) {
334                    itemIndex = 0;
335                }
336
337                mViewPager.setCurrentItem(itemIndex, false);
338                setViewActivated();
339            }
340            // Update the any action items
341            updateActionItems();
342        }
343    }
344
345    @Override
346    public void onLoaderReset(android.support.v4.content.Loader<Cursor> loader) {
347        // If the loader is reset, remove the reference in the adapter to this cursor
348        // TODO(pwestbro): reenable this when b/7075236 is fixed
349        // mAdapter.swapCursor(null);
350    }
351
352    protected void updateActionItems() {
353        // Do nothing, but allow extending classes to do work
354    }
355
356    private synchronized void notifyCursorListeners(Cursor data) {
357        // tell all of the objects listening for cursor changes
358        // that the cursor has changed
359        for (CursorChangedListener listener : mCursorListeners) {
360            listener.onCursorChanged(data);
361        }
362    }
363
364    @Override
365    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
366    }
367
368    @Override
369    public void onPageSelected(int position) {
370        mPhotoIndex = position;
371        setViewActivated();
372    }
373
374    @Override
375    public void onPageScrollStateChanged(int state) {
376    }
377
378    @Override
379    public boolean isFragmentActive(Fragment fragment) {
380        if (mViewPager == null || mAdapter == null) {
381            return false;
382        }
383        return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment);
384    }
385
386    @Override
387    public void onFragmentVisible(PhotoViewFragment fragment) {
388        updateActionBar(fragment);
389    }
390
391    @Override
392    public InterceptType onTouchIntercept(float origX, float origY) {
393        boolean interceptLeft = false;
394        boolean interceptRight = false;
395
396        for (OnScreenListener listener : mScreenListeners) {
397            if (!interceptLeft) {
398                interceptLeft = listener.onInterceptMoveLeft(origX, origY);
399            }
400            if (!interceptRight) {
401                interceptRight = listener.onInterceptMoveRight(origX, origY);
402            }
403            listener.onViewActivated();
404        }
405
406        if (interceptLeft) {
407            if (interceptRight) {
408                return InterceptType.BOTH;
409            }
410            return InterceptType.LEFT;
411        } else if (interceptRight) {
412            return InterceptType.RIGHT;
413        }
414        return InterceptType.NONE;
415    }
416
417    /**
418     * Updates the title bar according to the value of {@link #mFullScreen}.
419     */
420    protected void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
421        final boolean fullScreenChanged = (fullScreen != mFullScreen);
422        mFullScreen = fullScreen;
423
424        if (mFullScreen) {
425            setLightsOutMode(true);
426            cancelEnterFullScreenRunnable();
427        } else {
428            setLightsOutMode(false);
429            if (setDelayedRunnable) {
430                postEnterFullScreenRunnableWithDelay();
431            }
432        }
433
434        if (fullScreenChanged) {
435            for (OnScreenListener listener : mScreenListeners) {
436                listener.onFullScreenChanged(mFullScreen);
437            }
438        }
439    }
440
441    private void postEnterFullScreenRunnableWithDelay() {
442        mHandler.postDelayed(mEnterFullScreenRunnable, mEnterFullScreenDelayTime);
443    }
444
445    private void cancelEnterFullScreenRunnable() {
446        mHandler.removeCallbacks(mEnterFullScreenRunnable);
447    }
448
449    protected void setLightsOutMode(boolean enabled) {
450        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
451            int flags = enabled
452                    ? View.SYSTEM_UI_FLAG_LOW_PROFILE
453                    | View.SYSTEM_UI_FLAG_FULLSCREEN
454                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
455                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
456                    : View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
457                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
458
459            // using mViewPager since we have it and we need a view
460            mViewPager.setSystemUiVisibility(flags);
461        } else {
462            final ActionBar actionBar = getActionBar();
463            if (enabled) {
464                actionBar.hide();
465            } else {
466                actionBar.show();
467            }
468            int flags = enabled
469                    ? View.SYSTEM_UI_FLAG_LOW_PROFILE
470                    : View.SYSTEM_UI_FLAG_VISIBLE;
471            mViewPager.setSystemUiVisibility(flags);
472        }
473    }
474
475    private Runnable mEnterFullScreenRunnable = new Runnable() {
476        @Override
477        public void run() {
478            setFullScreen(true, true);
479        }
480    };
481
482    @Override
483    public void setViewActivated() {
484        for (OnScreenListener listener : mScreenListeners) {
485            listener.onViewActivated();
486        }
487    }
488
489    /**
490     * Adjusts the activity title and subtitle to reflect the photo name and count.
491     */
492    protected void updateActionBar(PhotoViewFragment fragment) {
493        final int position = mViewPager.getCurrentItem() + 1;
494        final String title;
495        final String subtitle;
496        final boolean hasAlbumCount = mAlbumCount >= 0;
497
498        final Cursor cursor = getCursorAtProperPosition();
499
500        if (cursor != null) {
501            final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME);
502            title = cursor.getString(photoNameIndex);
503        } else {
504            title = null;
505        }
506
507        if (mIsEmpty || !hasAlbumCount || position <= 0) {
508            subtitle = null;
509        } else {
510            subtitle = getResources().getString(R.string.photo_view_count, position, mAlbumCount);
511        }
512
513        final ActionBar actionBar = getActionBar();
514        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
515        actionBar.setTitle(title);
516        actionBar.setSubtitle(subtitle);
517    }
518
519    /**
520     * Utility method that will return the cursor that contains the data
521     * at the current position so that it refers to the current image on screen.
522     * @return the cursor at the current position or
523     * null if no cursor exists or if the {@link PhotoViewPager} is null.
524     */
525    public Cursor getCursorAtProperPosition() {
526        if (mViewPager == null) {
527            return null;
528        }
529
530        final int position = mViewPager.getCurrentItem();
531        final Cursor cursor = mAdapter.getCursor();
532
533        if (cursor == null) {
534            return null;
535        }
536
537        cursor.moveToPosition(position);
538
539        return cursor;
540    }
541
542    public Cursor getCursor() {
543        return (mAdapter == null) ? null : mAdapter.getCursor();
544    }
545
546    @Override
547    public void onMenuVisibilityChanged(boolean isVisible) {
548        if (isVisible) {
549            cancelEnterFullScreenRunnable();
550        } else {
551            postEnterFullScreenRunnableWithDelay();
552        }
553    }
554
555    public void onNewPhotoLoaded() {
556        setViewActivated();
557    }
558
559    protected boolean isFullScreen() {
560        return mFullScreen;
561    }
562
563    protected void setPhotoIndex(int index) {
564        mPhotoIndex = index;
565    }
566
567}
568