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