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