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
17package com.android.gallery3d.ingest;
18
19import com.android.gallery3d.R;
20import com.android.gallery3d.ingest.adapter.CheckBroker;
21import com.android.gallery3d.ingest.adapter.MtpAdapter;
22import com.android.gallery3d.ingest.adapter.MtpPagerAdapter;
23import com.android.gallery3d.ingest.data.ImportTask;
24import com.android.gallery3d.ingest.data.IngestObjectInfo;
25import com.android.gallery3d.ingest.data.MtpBitmapFetch;
26import com.android.gallery3d.ingest.data.MtpDeviceIndex;
27import com.android.gallery3d.ingest.ui.DateTileView;
28import com.android.gallery3d.ingest.ui.IngestGridView;
29import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
30
31import android.annotation.TargetApi;
32import android.app.Activity;
33import android.app.ProgressDialog;
34import android.content.ComponentName;
35import android.content.Context;
36import android.content.Intent;
37import android.content.ServiceConnection;
38import android.content.res.Configuration;
39import android.database.DataSetObserver;
40import android.os.Build;
41import android.os.Bundle;
42import android.os.Handler;
43import android.os.IBinder;
44import android.os.Message;
45import android.support.v4.view.ViewPager;
46import android.util.SparseBooleanArray;
47import android.view.ActionMode;
48import android.view.Menu;
49import android.view.MenuInflater;
50import android.view.MenuItem;
51import android.view.View;
52import android.widget.AbsListView.MultiChoiceModeListener;
53import android.widget.AdapterView;
54import android.widget.AdapterView.OnItemClickListener;
55import android.widget.TextView;
56
57import java.lang.ref.WeakReference;
58import java.util.Collection;
59
60/**
61 * MTP importer, main activity.
62 */
63@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
64public 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