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