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