Home | History | Annotate | Download | only in ingest
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.gallery3d.ingest;
     18 
     19 import com.android.gallery3d.R;
     20 import com.android.gallery3d.ingest.adapter.CheckBroker;
     21 import com.android.gallery3d.ingest.adapter.MtpAdapter;
     22 import com.android.gallery3d.ingest.adapter.MtpPagerAdapter;
     23 import com.android.gallery3d.ingest.data.ImportTask;
     24 import com.android.gallery3d.ingest.data.IngestObjectInfo;
     25 import com.android.gallery3d.ingest.data.MtpBitmapFetch;
     26 import com.android.gallery3d.ingest.data.MtpDeviceIndex;
     27 import com.android.gallery3d.ingest.ui.DateTileView;
     28 import com.android.gallery3d.ingest.ui.IngestGridView;
     29 import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
     30 
     31 import android.annotation.TargetApi;
     32 import android.app.Activity;
     33 import android.app.ProgressDialog;
     34 import android.content.ComponentName;
     35 import android.content.Context;
     36 import android.content.Intent;
     37 import android.content.ServiceConnection;
     38 import android.content.res.Configuration;
     39 import android.database.DataSetObserver;
     40 import android.os.Build;
     41 import android.os.Bundle;
     42 import android.os.Handler;
     43 import android.os.IBinder;
     44 import android.os.Message;
     45 import android.support.v4.view.ViewPager;
     46 import android.util.SparseBooleanArray;
     47 import android.view.ActionMode;
     48 import android.view.Menu;
     49 import android.view.MenuInflater;
     50 import android.view.MenuItem;
     51 import android.view.View;
     52 import android.widget.AbsListView.MultiChoiceModeListener;
     53 import android.widget.AdapterView;
     54 import android.widget.AdapterView.OnItemClickListener;
     55 import android.widget.TextView;
     56 
     57 import java.lang.ref.WeakReference;
     58 import java.util.Collection;
     59 
     60 /**
     61  * MTP importer, main activity.
     62  */
     63 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
     64 public class IngestActivity extends Activity implements
     65     MtpDeviceIndex.ProgressListener, ImportTask.Listener {
     66 
     67   private IngestService mHelperService;
     68   private boolean mActive = false;
     69   private IngestGridView mGridView;
     70   private MtpAdapter mAdapter;
     71   private Handler mHandler;
     72   private ProgressDialog mProgressDialog;
     73   private ActionMode mActiveActionMode;
     74 
     75   private View mWarningView;
     76   private TextView mWarningText;
     77   private int mLastCheckedPosition = 0;
     78 
     79   private ViewPager mFullscreenPager;
     80   private MtpPagerAdapter mPagerAdapter;
     81   private boolean mFullscreenPagerVisible = false;
     82 
     83   private MenuItem mMenuSwitcherItem;
     84   private MenuItem mActionMenuSwitcherItem;
     85 
     86   // The MTP framework components don't give us fine-grained file copy
     87   // progress updates, so for large photos and videos, we will be stuck
     88   // with a dialog not updating for a long time. To give the user feedback,
     89   // we switch to the animated indeterminate progress bar after the timeout
     90   // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from
     91   // the framework, we switch back to the normal progress bar.
     92   private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000;
     93 
     94   @Override
     95   protected void onCreate(Bundle savedInstanceState) {
     96     super.onCreate(savedInstanceState);
     97     doBindHelperService();
     98 
     99     setContentView(R.layout.ingest_activity_item_list);
    100     mGridView = (IngestGridView) findViewById(R.id.ingest_gridview);
    101     mAdapter = new MtpAdapter(this);
    102     mAdapter.registerDataSetObserver(mMasterObserver);
    103     mGridView.setAdapter(mAdapter);
    104     mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener);
    105     mGridView.setOnItemClickListener(mOnItemClickListener);
    106     mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker);
    107 
    108     mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager);
    109 
    110     mHandler = new ItemListHandler(this);
    111 
    112     MtpBitmapFetch.configureForContext(this);
    113   }
    114 
    115   private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
    116     @Override
    117     public void onItemClick(AdapterView<?> adapterView, View itemView, int position,
    118         long arg3) {
    119       mLastCheckedPosition = position;
    120       mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position));
    121     }
    122   };
    123 
    124   private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() {
    125     private boolean mIgnoreItemCheckedStateChanges = false;
    126 
    127     private void updateSelectedTitle(ActionMode mode) {
    128       int count = mGridView.getCheckedItemCount();
    129       mode.setTitle(getResources().getQuantityString(
    130           R.plurals.ingest_number_of_items_selected, count, count));
    131     }
    132 
    133     @Override
    134     public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
    135         boolean checked) {
    136       if (mIgnoreItemCheckedStateChanges) {
    137         return;
    138       }
    139       if (mAdapter.itemAtPositionIsBucket(position)) {
    140         SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions();
    141         mIgnoreItemCheckedStateChanges = true;
    142         mGridView.setItemChecked(position, false);
    143 
    144         // Takes advantage of the fact that SectionIndexer imposes the
    145         // need to clamp to the valid range
    146         int nextSectionStart = mAdapter.getPositionForSection(
    147             mAdapter.getSectionForPosition(position) + 1);
    148         if (nextSectionStart == position) {
    149           nextSectionStart = mAdapter.getCount();
    150         }
    151 
    152         boolean rangeValue = false; // Value we want to set all of the bucket items to
    153 
    154         // Determine if all the items in the bucket are currently checked, so that we
    155         // can uncheck them, otherwise we will check all items in the bucket.
    156         for (int i = position + 1; i < nextSectionStart; i++) {
    157           if (!checkedItems.get(i)) {
    158             rangeValue = true;
    159             break;
    160           }
    161         }
    162 
    163         // Set all items in the bucket to the desired state
    164         for (int i = position + 1; i < nextSectionStart; i++) {
    165           if (checkedItems.get(i) != rangeValue) {
    166             mGridView.setItemChecked(i, rangeValue);
    167           }
    168         }
    169 
    170         mPositionMappingCheckBroker.onBulkCheckedChange();
    171         mIgnoreItemCheckedStateChanges = false;
    172       } else {
    173         mPositionMappingCheckBroker.onCheckedChange(position, checked);
    174       }
    175       mLastCheckedPosition = position;
    176       updateSelectedTitle(mode);
    177     }
    178 
    179     @Override
    180     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    181       return onOptionsItemSelected(item);
    182     }
    183 
    184     @Override
    185     public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    186       MenuInflater inflater = mode.getMenuInflater();
    187       inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
    188       updateSelectedTitle(mode);
    189       mActiveActionMode = mode;
    190       mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
    191       setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible);
    192       return true;
    193     }
    194 
    195     @Override
    196     public void onDestroyActionMode(ActionMode mode) {
    197       mActiveActionMode = null;
    198       mActionMenuSwitcherItem = null;
    199       mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE);
    200     }
    201 
    202     @Override
    203     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    204       updateSelectedTitle(mode);
    205       return false;
    206     }
    207   };
    208 
    209   @Override
    210   public boolean onOptionsItemSelected(MenuItem item) {
    211     int id = item.getItemId();
    212     if (id == R.id.ingest_import_items) {
    213       if (mActiveActionMode != null) {
    214         mHelperService.importSelectedItems(
    215             mGridView.getCheckedItemPositions(),
    216             mAdapter);
    217         mActiveActionMode.finish();
    218       }
    219       return true;
    220     } else if (id == R.id.ingest_switch_view) {
    221       setFullscreenPagerVisibility(!mFullscreenPagerVisible);
    222       return true;
    223     } else {
    224       return false;
    225     }
    226   }
    227 
    228   @Override
    229   public boolean onCreateOptionsMenu(Menu menu) {
    230     MenuInflater inflater = getMenuInflater();
    231     inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
    232     mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
    233     menu.findItem(R.id.ingest_import_items).setVisible(false);
    234     setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible);
    235     return true;
    236   }
    237 
    238   @Override
    239   protected void onDestroy() {
    240     doUnbindHelperService();
    241     super.onDestroy();
    242   }
    243 
    244   @Override
    245   protected void onResume() {
    246     DateTileView.refreshLocale();
    247     mActive = true;
    248     if (mHelperService != null) {
    249       mHelperService.setClientActivity(this);
    250     }
    251     updateWarningView();
    252     super.onResume();
    253   }
    254 
    255   @Override
    256   protected void onPause() {
    257     if (mHelperService != null) {
    258       mHelperService.setClientActivity(null);
    259     }
    260     mActive = false;
    261     cleanupProgressDialog();
    262     super.onPause();
    263   }
    264 
    265   @Override
    266   public void onConfigurationChanged(Configuration newConfig) {
    267     super.onConfigurationChanged(newConfig);
    268     MtpBitmapFetch.configureForContext(this);
    269   }
    270 
    271   private void showWarningView(int textResId) {
    272     if (mWarningView == null) {
    273       mWarningView = findViewById(R.id.ingest_warning_view);
    274       mWarningText =
    275           (TextView) mWarningView.findViewById(R.id.ingest_warning_view_text);
    276     }
    277     mWarningText.setText(textResId);
    278     mWarningView.setVisibility(View.VISIBLE);
    279     setFullscreenPagerVisibility(false);
    280     mGridView.setVisibility(View.GONE);
    281     setSwitcherMenuVisibility(false);
    282   }
    283 
    284   private void hideWarningView() {
    285     if (mWarningView != null) {
    286       mWarningView.setVisibility(View.GONE);
    287       setFullscreenPagerVisibility(false);
    288     }
    289     setSwitcherMenuVisibility(true);
    290   }
    291 
    292   private PositionMappingCheckBroker mPositionMappingCheckBroker =
    293       new PositionMappingCheckBroker();
    294 
    295   private class PositionMappingCheckBroker extends CheckBroker
    296       implements OnClearChoicesListener {
    297     private int mLastMappingPager = -1;
    298     private int mLastMappingGrid = -1;
    299 
    300     private int mapPagerToGridPosition(int position) {
    301       if (position != mLastMappingPager) {
    302         mLastMappingPager = position;
    303         mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
    304       }
    305       return mLastMappingGrid;
    306     }
    307 
    308     private int mapGridToPagerPosition(int position) {
    309       if (position != mLastMappingGrid) {
    310         mLastMappingGrid = position;
    311         mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position);
    312       }
    313       return mLastMappingPager;
    314     }
    315 
    316     @Override
    317     public void setItemChecked(int position, boolean checked) {
    318       mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
    319     }
    320 
    321     @Override
    322     public void onCheckedChange(int position, boolean checked) {
    323       if (mPagerAdapter != null) {
    324         super.onCheckedChange(mapGridToPagerPosition(position), checked);
    325       }
    326     }
    327 
    328     @Override
    329     public boolean isItemChecked(int position) {
    330       return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
    331     }
    332 
    333     @Override
    334     public void onClearChoices() {
    335       onBulkCheckedChange();
    336     }
    337   }
    338 
    339   private DataSetObserver mMasterObserver = new DataSetObserver() {
    340     @Override
    341     public void onChanged() {
    342       if (mPagerAdapter != null) {
    343         mPagerAdapter.notifyDataSetChanged();
    344       }
    345     }
    346 
    347     @Override
    348     public void onInvalidated() {
    349       if (mPagerAdapter != null) {
    350         mPagerAdapter.notifyDataSetChanged();
    351       }
    352     }
    353   };
    354 
    355   private int pickFullscreenStartingPosition() {
    356     int firstVisiblePosition = mGridView.getFirstVisiblePosition();
    357     if (mLastCheckedPosition <= firstVisiblePosition
    358         || mLastCheckedPosition > mGridView.getLastVisiblePosition()) {
    359       return firstVisiblePosition;
    360     } else {
    361       return mLastCheckedPosition;
    362     }
    363   }
    364 
    365   private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) {
    366     if (menuItem == null) {
    367       return;
    368     }
    369     if (!inFullscreenMode) {
    370       menuItem.setIcon(android.R.drawable.ic_menu_zoom);
    371       menuItem.setTitle(R.string.ingest_switch_photo_fullscreen);
    372     } else {
    373       menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
    374       menuItem.setTitle(R.string.ingest_switch_photo_grid);
    375     }
    376   }
    377 
    378   private void setFullscreenPagerVisibility(boolean visible) {
    379     mFullscreenPagerVisible = visible;
    380     if (visible) {
    381       if (mPagerAdapter == null) {
    382         mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker);
    383         mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex());
    384       }
    385       mFullscreenPager.setAdapter(mPagerAdapter);
    386       mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels(
    387           pickFullscreenStartingPosition()), false);
    388     } else if (mPagerAdapter != null) {
    389       mGridView.setSelection(mAdapter.translatePositionWithoutLabels(
    390           mFullscreenPager.getCurrentItem()));
    391       mFullscreenPager.setAdapter(null);
    392     }
    393     mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE);
    394     mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
    395     if (mActionMenuSwitcherItem != null) {
    396       setSwitcherMenuState(mActionMenuSwitcherItem, visible);
    397     }
    398     setSwitcherMenuState(mMenuSwitcherItem, visible);
    399   }
    400 
    401   private void setSwitcherMenuVisibility(boolean visible) {
    402     if (mActionMenuSwitcherItem != null) {
    403       mActionMenuSwitcherItem.setVisible(visible);
    404     }
    405     if (mMenuSwitcherItem != null) {
    406       mMenuSwitcherItem.setVisible(visible);
    407     }
    408   }
    409 
    410   private void updateWarningView() {
    411     if (!mAdapter.deviceConnected()) {
    412       showWarningView(R.string.ingest_no_device);
    413     } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) {
    414       showWarningView(R.string.ingest_empty_device);
    415     } else {
    416       hideWarningView();
    417     }
    418   }
    419 
    420   private void uiThreadNotifyIndexChanged() {
    421     mAdapter.notifyDataSetChanged();
    422     if (mActiveActionMode != null) {
    423       mActiveActionMode.finish();
    424       mActiveActionMode = null;
    425     }
    426     updateWarningView();
    427   }
    428 
    429   protected void notifyIndexChanged() {
    430     mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
    431   }
    432 
    433   private static class ProgressState {
    434     String message;
    435     String title;
    436     int current;
    437     int max;
    438 
    439     public void reset() {
    440       title = null;
    441       message = null;
    442       current = 0;
    443       max = 0;
    444     }
    445   }
    446 
    447   private ProgressState mProgressState = new ProgressState();
    448 
    449   @Override
    450   public void onObjectIndexed(IngestObjectInfo object, int numVisited) {
    451     // Not guaranteed to be called on the UI thread
    452     mProgressState.reset();
    453     mProgressState.max = 0;
    454     mProgressState.message = getResources().getQuantityString(
    455         R.plurals.ingest_number_of_items_scanned, numVisited, numVisited);
    456     mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
    457   }
    458 
    459   @Override
    460   public void onSortingStarted() {
    461     // Not guaranteed to be called on the UI thread
    462     mProgressState.reset();
    463     mProgressState.max = 0;
    464     mProgressState.message = getResources().getString(R.string.ingest_sorting);
    465     mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
    466   }
    467 
    468   @Override
    469   public void onIndexingFinished() {
    470     // Not guaranteed to be called on the UI thread
    471     mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
    472     mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
    473   }
    474 
    475   @Override
    476   public void onImportProgress(final int visitedCount, final int totalCount,
    477       String pathIfSuccessful) {
    478     // Not guaranteed to be called on the UI thread
    479     mProgressState.reset();
    480     mProgressState.max = totalCount;
    481     mProgressState.current = visitedCount;
    482     mProgressState.title = getResources().getString(R.string.ingest_importing);
    483     mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
    484     mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
    485     mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE,
    486         INDETERMINATE_SWITCH_TIMEOUT_MS);
    487   }
    488 
    489   @Override
    490   public void onImportFinish(Collection<IngestObjectInfo> objectsNotImported,
    491       int numVisited) {
    492     // Not guaranteed to be called on the UI thread
    493     mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
    494     mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
    495     // TODO(georgescu): maybe show an extra dialog listing the ones that failed
    496     // importing, if any?
    497   }
    498 
    499   private ProgressDialog getProgressDialog() {
    500     if (mProgressDialog == null || !mProgressDialog.isShowing()) {
    501       mProgressDialog = new ProgressDialog(this);
    502       mProgressDialog.setCancelable(false);
    503     }
    504     return mProgressDialog;
    505   }
    506 
    507   private void updateProgressDialog() {
    508     ProgressDialog dialog = getProgressDialog();
    509     boolean indeterminate = (mProgressState.max == 0);
    510     dialog.setIndeterminate(indeterminate);
    511     dialog.setProgressStyle(indeterminate ? ProgressDialog.STYLE_SPINNER
    512         : ProgressDialog.STYLE_HORIZONTAL);
    513     if (mProgressState.title != null) {
    514       dialog.setTitle(mProgressState.title);
    515     }
    516     if (mProgressState.message != null) {
    517       dialog.setMessage(mProgressState.message);
    518     }
    519     if (!indeterminate) {
    520       dialog.setProgress(mProgressState.current);
    521       dialog.setMax(mProgressState.max);
    522     }
    523     if (!dialog.isShowing()) {
    524       dialog.show();
    525     }
    526   }
    527 
    528   private void makeProgressDialogIndeterminate() {
    529     ProgressDialog dialog = getProgressDialog();
    530     dialog.setIndeterminate(true);
    531   }
    532 
    533   private void cleanupProgressDialog() {
    534     if (mProgressDialog != null) {
    535       mProgressDialog.dismiss();
    536       mProgressDialog = null;
    537     }
    538   }
    539 
    540   // This is static and uses a WeakReference in order to avoid leaking the Activity
    541   private static class ItemListHandler extends Handler {
    542     public static final int MSG_PROGRESS_UPDATE = 0;
    543     public static final int MSG_PROGRESS_HIDE = 1;
    544     public static final int MSG_NOTIFY_CHANGED = 2;
    545     public static final int MSG_BULK_CHECKED_CHANGE = 3;
    546     public static final int MSG_PROGRESS_INDETERMINATE = 4;
    547 
    548     WeakReference<IngestActivity> mParentReference;
    549 
    550     public ItemListHandler(IngestActivity parent) {
    551       super();
    552       mParentReference = new WeakReference<IngestActivity>(parent);
    553     }
    554 
    555     @Override
    556     public void handleMessage(Message message) {
    557       IngestActivity parent = mParentReference.get();
    558       if (parent == null || !parent.mActive) {
    559         return;
    560       }
    561       switch (message.what) {
    562         case MSG_PROGRESS_HIDE:
    563           parent.cleanupProgressDialog();
    564           break;
    565         case MSG_PROGRESS_UPDATE:
    566           parent.updateProgressDialog();
    567           break;
    568         case MSG_NOTIFY_CHANGED:
    569           parent.uiThreadNotifyIndexChanged();
    570           break;
    571         case MSG_BULK_CHECKED_CHANGE:
    572           parent.mPositionMappingCheckBroker.onBulkCheckedChange();
    573           break;
    574         case MSG_PROGRESS_INDETERMINATE:
    575           parent.makeProgressDialogIndeterminate();
    576           break;
    577         default:
    578           break;
    579       }
    580     }
    581   }
    582 
    583   private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
    584     @Override
    585     public void onServiceConnected(ComponentName className, IBinder service) {
    586       mHelperService = ((IngestService.LocalBinder) service).getService();
    587       mHelperService.setClientActivity(IngestActivity.this);
    588       MtpDeviceIndex index = mHelperService.getIndex();
    589       mAdapter.setMtpDeviceIndex(index);
    590       if (mPagerAdapter != null) {
    591         mPagerAdapter.setMtpDeviceIndex(index);
    592       }
    593     }
    594 
    595     @Override
    596     public void onServiceDisconnected(ComponentName className) {
    597       mHelperService = null;
    598     }
    599   };
    600 
    601   private void doBindHelperService() {
    602     bindService(new Intent(getApplicationContext(), IngestService.class),
    603         mHelperServiceConnection, Context.BIND_AUTO_CREATE);
    604   }
    605 
    606   private void doUnbindHelperService() {
    607     if (mHelperService != null) {
    608       mHelperService.setClientActivity(null);
    609       unbindService(mHelperServiceConnection);
    610     }
    611   }
    612 }
    613