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