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