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