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