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