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