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 androidx.viewpager.widget.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
onCreate(Bundle savedInstanceState)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
onOptionsItemSelected(MenuItem item)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
onCreateOptionsMenu(Menu menu)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
onDestroy()239   protected void onDestroy() {
240     doUnbindHelperService();
241     super.onDestroy();
242   }
243 
244   @Override
onResume()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
onPause()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
onConfigurationChanged(Configuration newConfig)266   public void onConfigurationChanged(Configuration newConfig) {
267     super.onConfigurationChanged(newConfig);
268     MtpBitmapFetch.configureForContext(this);
269   }
270 
showWarningView(int textResId)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 
hideWarningView()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 
mapPagerToGridPosition(int position)300     private int mapPagerToGridPosition(int position) {
301       if (position != mLastMappingPager) {
302         mLastMappingPager = position;
303         mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
304       }
305       return mLastMappingGrid;
306     }
307 
mapGridToPagerPosition(int position)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
setItemChecked(int position, boolean checked)317     public void setItemChecked(int position, boolean checked) {
318       mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
319     }
320 
321     @Override
onCheckedChange(int position, boolean checked)322     public void onCheckedChange(int position, boolean checked) {
323       if (mPagerAdapter != null) {
324         super.onCheckedChange(mapGridToPagerPosition(position), checked);
325       }
326     }
327 
328     @Override
isItemChecked(int position)329     public boolean isItemChecked(int position) {
330       return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
331     }
332 
333     @Override
onClearChoices()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 
pickFullscreenStartingPosition()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 
setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode)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 
setFullscreenPagerVisibility(boolean visible)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 
setSwitcherMenuVisibility(boolean visible)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 
updateWarningView()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 
uiThreadNotifyIndexChanged()420   private void uiThreadNotifyIndexChanged() {
421     mAdapter.notifyDataSetChanged();
422     if (mActiveActionMode != null) {
423       mActiveActionMode.finish();
424       mActiveActionMode = null;
425     }
426     updateWarningView();
427   }
428 
notifyIndexChanged()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 
reset()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
onObjectIndexed(IngestObjectInfo object, int numVisited)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
onSortingStarted()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
onIndexingFinished()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
onImportProgress(final int visitedCount, final int totalCount, String pathIfSuccessful)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
onImportFinish(Collection<IngestObjectInfo> objectsNotImported, int numVisited)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 
getProgressDialog()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 
updateProgressDialog()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 
makeProgressDialogIndeterminate()528   private void makeProgressDialogIndeterminate() {
529     ProgressDialog dialog = getProgressDialog();
530     dialog.setIndeterminate(true);
531   }
532 
cleanupProgressDialog()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 
ItemListHandler(IngestActivity parent)550     public ItemListHandler(IngestActivity parent) {
551       super();
552       mParentReference = new WeakReference<IngestActivity>(parent);
553     }
554 
555     @Override
handleMessage(Message message)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 
doBindHelperService()601   private void doBindHelperService() {
602     bindService(new Intent(getApplicationContext(), IngestService.class),
603         mHelperServiceConnection, Context.BIND_AUTO_CREATE);
604   }
605 
doUnbindHelperService()606   private void doUnbindHelperService() {
607     if (mHelperService != null) {
608       mHelperService.setClientActivity(null);
609       unbindService(mHelperServiceConnection);
610     }
611   }
612 }
613