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