PhotoViewActivity.java revision 502e7156de49b62863ef0824cfd8b5a0be65dbed
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    private Handler mActionBarHideHandler;
138    // TODO Find a better way to do this. We basically want the activity to display the
139    // "loading..." progress until the fragment takes over and shows it's own "loading..."
140    // progress [located in photo_header_view.xml]. We could potentially have all status displayed
141    // by the activity, but, that gets tricky when it comes to screen rotation. For now, we
142    // track the loading by this variable which is fragile and may cause phantom "loading..."
143    // text.
144    private long mActionBarHideDelayTime;
145
146    @Override
147    protected void onCreate(Bundle savedInstanceState) {
148        super.onCreate(savedInstanceState);
149
150        final ActivityManager mgr = (ActivityManager) getApplicationContext().
151                getSystemService(Activity.ACTIVITY_SERVICE);
152        sMemoryClass = mgr.getMemoryClass();
153
154        Intent mIntent = getIntent();
155
156        int currentItem = -1;
157        if (savedInstanceState != null) {
158            currentItem = savedInstanceState.getInt(STATE_ITEM_KEY, -1);
159            mFullScreen = savedInstanceState.getBoolean(STATE_FULLSCREEN_KEY, false);
160        }
161
162        // uri of the photos to view; optional
163        if (mIntent.hasExtra(Intents.EXTRA_PHOTOS_URI)) {
164            mPhotosUri = mIntent.getStringExtra(Intents.EXTRA_PHOTOS_URI);
165        }
166
167        // projection for the query; optional
168        // I.f not set, the default projection is used.
169        // This projection must include the columns from the default projection.
170        if (mIntent.hasExtra(Intents.EXTRA_PROJECTION)) {
171            mProjection = mIntent.getStringArrayExtra(Intents.EXTRA_PROJECTION);
172        } else {
173            mProjection = null;
174        }
175
176        // Set the current item from the intent if wasn't in the saved instance
177        if (mIntent.hasExtra(Intents.EXTRA_PHOTO_INDEX) && currentItem < 0) {
178            currentItem = mIntent.getIntExtra(Intents.EXTRA_PHOTO_INDEX, -1);
179        }
180        mPhotoIndex = currentItem;
181
182        setContentView(R.layout.photo_activity_view);
183
184        // Create the adapter and add the view pager
185        mAdapter = new PhotoPagerAdapter(this, getFragmentManager(), null);
186
187        mViewPager = (PhotoViewPager) findViewById(R.id.photo_view_pager);
188        mViewPager.setAdapter(mAdapter);
189        mViewPager.setOnPageChangeListener(this);
190        mViewPager.setOnInterceptTouchListener(this);
191
192        // Kick off the loader
193        getLoaderManager().initLoader(LOADER_PHOTO_LIST, null, this);
194
195        final ActionBar actionBar = getActionBar();
196        actionBar.setDisplayHomeAsUpEnabled(true);
197        mActionBarHideDelayTime = getResources().getInteger(
198                R.integer.action_bar_delay_time_in_millis);
199        actionBar.addOnMenuVisibilityListener(this);
200        actionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
201        mActionBarHideHandler = new Handler();
202    }
203
204    @Override
205    protected void onResume() {
206        super.onResume();
207        setFullScreen(mFullScreen, false);
208
209        mIsPaused = false;
210        if (mRestartLoader) {
211            mRestartLoader = false;
212            getLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
213        }
214    }
215
216    @Override
217    protected void onPause() {
218        mIsPaused = true;
219
220        super.onPause();
221    }
222
223    @Override
224    public void onBackPressed() {
225        // If in full screen mode, toggle mode & eat the 'back'
226        if (mFullScreen) {
227            toggleFullScreen();
228        } else {
229            super.onBackPressed();
230        }
231    }
232
233    @Override
234    public void onSaveInstanceState(Bundle outState) {
235        super.onSaveInstanceState(outState);
236
237        outState.putInt(STATE_ITEM_KEY, mViewPager.getCurrentItem());
238        outState.putBoolean(STATE_FULLSCREEN_KEY, mFullScreen);
239    }
240
241    @Override
242    public boolean onOptionsItemSelected(MenuItem item) {
243       switch (item.getItemId()) {
244          case android.R.id.home:
245             finish();
246          default:
247             return super.onOptionsItemSelected(item);
248       }
249    }
250
251    public void addScreenListener(OnScreenListener listener) {
252        mScreenListeners.add(listener);
253    }
254
255    public void removeScreenListener(OnScreenListener listener) {
256        mScreenListeners.remove(listener);
257    }
258
259    public synchronized void addCursorListener(CursorChangedListener listener) {
260        mCursorListeners.add(listener);
261    }
262
263    public synchronized void removeCursorListener(CursorChangedListener listener) {
264        mCursorListeners.remove(listener);
265    }
266
267    public boolean isFragmentFullScreen(Fragment fragment) {
268        if (mViewPager == null || mAdapter == null || mAdapter.getCount() == 0) {
269            return mFullScreen;
270        }
271        return mFullScreen || (mViewPager.getCurrentItem() != mAdapter.getItemPosition(fragment));
272    }
273
274    public void toggleFullScreen() {
275        setFullScreen(!mFullScreen, true);
276    }
277
278    public void onPhotoRemoved(long photoId) {
279        final Cursor data = mAdapter.getCursor();
280        if (data == null) {
281            // Huh?! How would this happen?
282            return;
283        }
284
285        final int dataCount = data.getCount();
286        if (dataCount <= 1) {
287            finish();
288            return;
289        }
290
291        getLoaderManager().restartLoader(LOADER_PHOTO_LIST, null, this);
292    }
293
294    @Override
295    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
296        if (id == LOADER_PHOTO_LIST) {
297            return new PhotoPagerLoader(this, Uri.parse(mPhotosUri), mProjection);
298        }
299        return null;
300    }
301
302    @Override
303    public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
304        final int id = loader.getId();
305        if (id == LOADER_PHOTO_LIST) {
306            if (data == null || data.getCount() == 0) {
307                mIsEmpty = true;
308            } else {
309                mAlbumCount = data.getCount();
310
311                // Cannot do this directly; need to be out of the loader
312                // TODO(pwestbro): Fix this as this could cause a problem as the cursor may be
313                // closed before the runnable runs.
314                new Handler().post(new Runnable() {
315                    @Override
316                    public void run() {
317                        // If the cursor is closed, we can leave, as another runnable will have
318                        // been posted (this means our Cursor is already out-of-date)
319                        if (data.isClosed()) return;
320
321                        // We're paused; don't do anything now, we'll get re-invoked
322                        // when the activity becomes active again
323                        if (mIsPaused) {
324                            mRestartLoader = true;
325                            return;
326                        }
327                        mIsEmpty = false;
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                        mAdapter.swapCursor(data);
338                        notifyCursorListeners(data);
339
340                        mViewPager.setCurrentItem(itemIndex, false);
341                        setViewActivated();
342                    }
343                });
344            }
345        }
346    }
347
348    private synchronized void notifyCursorListeners(Cursor data) {
349        // tell all of the objects listening for cursor changes
350        // that the cursor has changed
351        for (CursorChangedListener listener : mCursorListeners) {
352            listener.onCursorChanged(data);
353        }
354    }
355
356    @Override
357    public void onLoaderReset(Loader<Cursor> loader) {
358    }
359
360    @Override
361    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
362    }
363
364    @Override
365    public void onPageSelected(int position) {
366        mPhotoIndex = position;
367        setViewActivated();
368    }
369
370    @Override
371    public void onPageScrollStateChanged(int state) {
372    }
373
374    public boolean isFragmentActive(Fragment fragment) {
375        if (mViewPager == null || mAdapter == null) {
376            return false;
377        }
378        return mViewPager.getCurrentItem() == mAdapter.getItemPosition(fragment);
379    }
380
381    public void onFragmentVisible(PhotoViewFragment fragment) {
382        updateActionBar(fragment);
383    }
384
385    @Override
386    public InterceptType onTouchIntercept(float origX, float origY) {
387        boolean interceptLeft = false;
388        boolean interceptRight = false;
389
390        for (OnScreenListener listener : mScreenListeners) {
391            if (!interceptLeft) {
392                interceptLeft = listener.onInterceptMoveLeft(origX, origY);
393            }
394            if (!interceptRight) {
395                interceptRight = listener.onInterceptMoveRight(origX, origY);
396            }
397            listener.onViewActivated();
398        }
399
400        if (interceptLeft) {
401            if (interceptRight) {
402                return InterceptType.BOTH;
403            }
404            return InterceptType.LEFT;
405        } else if (interceptRight) {
406            return InterceptType.RIGHT;
407        }
408        return InterceptType.NONE;
409    }
410
411    /**
412     * Updates the title bar according to the value of {@link #mFullScreen}.
413     */
414    private void setFullScreen(boolean fullScreen, boolean setDelayedRunnable) {
415        final boolean fullScreenChanged = (fullScreen != mFullScreen);
416        mFullScreen = fullScreen;
417
418        if (mFullScreen) {
419            setLightsOutMode(true);
420            cancelActionBarHideRunnable();
421        } else {
422            setLightsOutMode(false);
423            if (setDelayedRunnable) {
424                postActionBarHideRunnableWithDelay();
425            }
426        }
427
428        if (fullScreenChanged) {
429            for (OnScreenListener listener : mScreenListeners) {
430                listener.onFullScreenChanged(mFullScreen);
431            }
432        }
433    }
434
435    private void postActionBarHideRunnableWithDelay() {
436        mActionBarHideHandler.postDelayed(mActionBarHideRunnable,
437                mActionBarHideDelayTime);
438    }
439
440    private void cancelActionBarHideRunnable() {
441        mActionBarHideHandler.removeCallbacks(mActionBarHideRunnable);
442    }
443
444    private void setLightsOutMode(boolean enabled) {
445        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
446            int flags = enabled
447                    ? View.SYSTEM_UI_FLAG_LOW_PROFILE
448                    | View.SYSTEM_UI_FLAG_FULLSCREEN
449                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
450                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
451                    : View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
452                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
453
454            // using mViewPager since we have it and we need a view
455            mViewPager.setSystemUiVisibility(flags);
456        } else {
457            final ActionBar actionBar = getActionBar();
458            if (enabled) {
459                actionBar.hide();
460            } else {
461                actionBar.show();
462            }
463            int flags = enabled
464                    ? View.SYSTEM_UI_FLAG_LOW_PROFILE
465                    : View.SYSTEM_UI_FLAG_VISIBLE;
466            mViewPager.setSystemUiVisibility(flags);
467        }
468    }
469
470    private Runnable mActionBarHideRunnable = new Runnable() {
471        @Override
472        public void run() {
473            PhotoViewActivity.this.setLightsOutMode(true);
474        }
475    };
476
477    public void setViewActivated() {
478        for (OnScreenListener listener : mScreenListeners) {
479            listener.onViewActivated();
480        }
481    }
482
483    /**
484     * Adjusts the activity title and subtitle to reflect the photo name and count.
485     */
486    protected void updateActionBar(PhotoViewFragment fragment) {
487        final int position = mViewPager.getCurrentItem() + 1;
488        final String title;
489        final String subtitle;
490        final boolean hasAlbumCount = mAlbumCount >= 0;
491
492        final Cursor cursor = getCursorAtProperPosition();
493
494        if (cursor != null) {
495            final int photoNameIndex = cursor.getColumnIndex(PhotoContract.PhotoViewColumns.NAME);
496            title = cursor.getString(photoNameIndex);
497        } else {
498            title = null;
499        }
500
501        if (mIsEmpty || !hasAlbumCount || position <= 0) {
502            subtitle = null;
503        } else {
504            subtitle = getResources().getString(R.string.photo_view_count, position, mAlbumCount);
505        }
506
507        final ActionBar actionBar = getActionBar();
508        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
509        actionBar.setTitle(title);
510        actionBar.setSubtitle(subtitle);
511    }
512
513    /**
514     * Utility method that will return the cursor that contains the data
515     * at the current position so that it refers to the current image on screen.
516     * @return the cursor at the current position or
517     * null if no cursor exists or if the {@link PhotoViewPager} is null.
518     */
519    public Cursor getCursorAtProperPosition() {
520        if (mViewPager == null) {
521            return null;
522        }
523
524        final int position = mViewPager.getCurrentItem();
525        final Cursor cursor = mAdapter.getCursor();
526
527        if (cursor == null) {
528            return null;
529        }
530
531        cursor.moveToPosition(position);
532
533        return cursor;
534    }
535
536    public Cursor getCursor() {
537        return (mAdapter == null) ? null : mAdapter.getCursor();
538    }
539
540    @Override
541    public void onMenuVisibilityChanged(boolean isVisible) {
542        if (isVisible) {
543            cancelActionBarHideRunnable();
544        } else {
545            postActionBarHideRunnableWithDelay();
546        }
547    }
548}
549