2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.gallery3d.ingest;
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;
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;
57 import java.lang.ref.WeakReference;
58 import java.util.Collection;
61 * MTP importer, main activity.
63 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
64 public class IngestActivity extends Activity implements
65 MtpDeviceIndex.ProgressListener, ImportTask.Listener {
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;
75 private View mWarningView;
76 private TextView mWarningText;
77 private int mLastCheckedPosition = 0;
79 private ViewPager mFullscreenPager;
80 private MtpPagerAdapter mPagerAdapter;
81 private boolean mFullscreenPagerVisible = false;
83 private MenuItem mMenuSwitcherItem;
84 private MenuItem mActionMenuSwitcherItem;
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;
95 protected void onCreate(Bundle savedInstanceState) {
96 super.onCreate(savedInstanceState);
97 doBindHelperService();
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);
108 mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager);
110 mHandler = new ItemListHandler(this);
112 MtpBitmapFetch.configureForContext(this);
115 private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
117 public void onItemClick(AdapterView<?> adapterView, View itemView, int position,
119 mLastCheckedPosition = position;
120 mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position));
124 private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() {
125 private boolean mIgnoreItemCheckedStateChanges = false;
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));
134 public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
136 if (mIgnoreItemCheckedStateChanges) {
139 if (mAdapter.itemAtPositionIsBucket(position)) {
140 SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions();
141 mIgnoreItemCheckedStateChanges = true;
142 mGridView.setItemChecked(position, false);
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();
152 boolean rangeValue = false; // Value we want to set all of the bucket items to
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)) {
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);
170 mPositionMappingCheckBroker.onBulkCheckedChange();
171 mIgnoreItemCheckedStateChanges = false;
173 mPositionMappingCheckBroker.onCheckedChange(position, checked);
175 mLastCheckedPosition = position;
176 updateSelectedTitle(mode);
180 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
181 return onOptionsItemSelected(item);
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);
196 public void onDestroyActionMode(ActionMode mode) {
197 mActiveActionMode = null;
198 mActionMenuSwitcherItem = null;
199 mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE);
203 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
204 updateSelectedTitle(mode);
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(),
217 mActiveActionMode.finish();
220 } else if (id == R.id.ingest_switch_view) {
221 setFullscreenPagerVisibility(!mFullscreenPagerVisible);
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);
239 protected void onDestroy() {
240 doUnbindHelperService();
245 protected void onResume() {
246 DateTileView.refreshLocale();
248 if (mHelperService != null) {
249 mHelperService.setClientActivity(this);
256 protected void onPause() {
257 if (mHelperService != null) {
258 mHelperService.setClientActivity(null);
261 cleanupProgressDialog();
266 public void onConfigurationChanged(Configuration newConfig) {
267 super.onConfigurationChanged(newConfig);
268 MtpBitmapFetch.configureForContext(this);
271 private void showWarningView(int textResId) {
272 if (mWarningView == null) {
273 mWarningView = findViewById(R.id.ingest_warning_view);
275 (TextView) mWarningView.findViewById(R.id.ingest_warning_view_text);
277 mWarningText.setText(textResId);
278 mWarningView.setVisibility(View.VISIBLE);
279 setFullscreenPagerVisibility(false);
280 mGridView.setVisibility(View.GONE);
281 setSwitcherMenuVisibility(false);
284 private void hideWarningView() {
285 if (mWarningView != null) {
286 mWarningView.setVisibility(View.GONE);
287 setFullscreenPagerVisibility(false);
289 setSwitcherMenuVisibility(true);
292 private PositionMappingCheckBroker mPositionMappingCheckBroker =
293 new PositionMappingCheckBroker();
295 private class PositionMappingCheckBroker extends CheckBroker
296 implements OnClearChoicesListener {
297 private int mLastMappingPager = -1;
298 private int mLastMappingGrid = -1;
300 private int mapPagerToGridPosition(int position) {
301 if (position != mLastMappingPager) {
302 mLastMappingPager = position;
303 mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
305 return mLastMappingGrid;
308 private int mapGridToPagerPosition(int position) {
309 if (position != mLastMappingGrid) {
310 mLastMappingGrid = position;
311 mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position);
313 return mLastMappingPager;
317 public void setItemChecked(int position, boolean checked) {
318 mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
322 public void onCheckedChange(int position, boolean checked) {
323 if (mPagerAdapter != null) {
324 super.onCheckedChange(mapGridToPagerPosition(position), checked);
329 public boolean isItemChecked(int position) {
330 return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
334 public void onClearChoices() {
335 onBulkCheckedChange();
339 private DataSetObserver mMasterObserver = new DataSetObserver() {
341 public void onChanged() {
342 if (mPagerAdapter != null) {
343 mPagerAdapter.notifyDataSetChanged();
348 public void onInvalidated() {
349 if (mPagerAdapter != null) {
350 mPagerAdapter.notifyDataSetChanged();
355 private int pickFullscreenStartingPosition() {
356 int firstVisiblePosition = mGridView.getFirstVisiblePosition();
357 if (mLastCheckedPosition <= firstVisiblePosition
358 || mLastCheckedPosition > mGridView.getLastVisiblePosition()) {
359 return firstVisiblePosition;
361 return mLastCheckedPosition;
365 private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) {
366 if (menuItem == null) {
369 if (!inFullscreenMode) {
370 menuItem.setIcon(android.R.drawable.ic_menu_zoom);
371 menuItem.setTitle(R.string.ingest_switch_photo_fullscreen);
373 menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
374 menuItem.setTitle(R.string.ingest_switch_photo_grid);
378 private void setFullscreenPagerVisibility(boolean visible) {
379 mFullscreenPagerVisible = visible;
381 if (mPagerAdapter == null) {
382 mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker);
383 mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex());
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);
393 mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE);
394 mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
395 if (mActionMenuSwitcherItem != null) {
396 setSwitcherMenuState(mActionMenuSwitcherItem, visible);
398 setSwitcherMenuState(mMenuSwitcherItem, visible);
401 private void setSwitcherMenuVisibility(boolean visible) {
402 if (mActionMenuSwitcherItem != null) {
403 mActionMenuSwitcherItem.setVisible(visible);
405 if (mMenuSwitcherItem != null) {
406 mMenuSwitcherItem.setVisible(visible);
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);
420 private void uiThreadNotifyIndexChanged() {
421 mAdapter.notifyDataSetChanged();
422 if (mActiveActionMode != null) {
423 mActiveActionMode.finish();
424 mActiveActionMode = null;
429 protected void notifyIndexChanged() {
430 mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
433 private static class ProgressState {
439 public void reset() {
447 private ProgressState mProgressState = new ProgressState();
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);
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);
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);
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);
490 public void onImportFinish(Collection<IngestObjectInfo> objectsNotImported,
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?
499 private ProgressDialog getProgressDialog() {
500 if (mProgressDialog == null || !mProgressDialog.isShowing()) {
501 mProgressDialog = new ProgressDialog(this);
502 mProgressDialog.setCancelable(false);
504 return mProgressDialog;
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);
516 if (mProgressState.message != null) {
517 dialog.setMessage(mProgressState.message);
519 if (!indeterminate) {
520 dialog.setProgress(mProgressState.current);
521 dialog.setMax(mProgressState.max);
523 if (!dialog.isShowing()) {
528 private void makeProgressDialogIndeterminate() {
529 ProgressDialog dialog = getProgressDialog();
530 dialog.setIndeterminate(true);
533 private void cleanupProgressDialog() {
534 if (mProgressDialog != null) {
535 mProgressDialog.dismiss();
536 mProgressDialog = null;
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;
548 WeakReference<IngestActivity> mParentReference;
550 public ItemListHandler(IngestActivity parent) {
552 mParentReference = new WeakReference<IngestActivity>(parent);
556 public void handleMessage(Message message) {
557 IngestActivity parent = mParentReference.get();
558 if (parent == null || !parent.mActive) {
561 switch (message.what) {
562 case MSG_PROGRESS_HIDE:
563 parent.cleanupProgressDialog();
565 case MSG_PROGRESS_UPDATE:
566 parent.updateProgressDialog();
568 case MSG_NOTIFY_CHANGED:
569 parent.uiThreadNotifyIndexChanged();
571 case MSG_BULK_CHECKED_CHANGE:
572 parent.mPositionMappingCheckBroker.onBulkCheckedChange();
574 case MSG_PROGRESS_INDETERMINATE:
575 parent.makeProgressDialogIndeterminate();
583 private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
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);
596 public void onServiceDisconnected(ComponentName className) {
597 mHelperService = null;
601 private void doBindHelperService() {
602 bindService(new Intent(getApplicationContext(), IngestService.class),
603 mHelperServiceConnection, Context.BIND_AUTO_CREATE);
606 private void doUnbindHelperService() {
607 if (mHelperService != null) {
608 mHelperService.setClientActivity(null);
609 unbindService(mHelperServiceConnection);