| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.gallery3d.ingest; |
| |
| import com.android.gallery3d.R; |
| import com.android.gallery3d.ingest.adapter.CheckBroker; |
| import com.android.gallery3d.ingest.adapter.MtpAdapter; |
| import com.android.gallery3d.ingest.adapter.MtpPagerAdapter; |
| import com.android.gallery3d.ingest.data.ImportTask; |
| import com.android.gallery3d.ingest.data.IngestObjectInfo; |
| import com.android.gallery3d.ingest.data.MtpBitmapFetch; |
| import com.android.gallery3d.ingest.data.MtpDeviceIndex; |
| import com.android.gallery3d.ingest.ui.DateTileView; |
| import com.android.gallery3d.ingest.ui.IngestGridView; |
| import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener; |
| |
| import android.annotation.TargetApi; |
| import android.app.Activity; |
| import android.app.ProgressDialog; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.res.Configuration; |
| import android.database.DataSetObserver; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import androidx.viewpager.widget.ViewPager; |
| import android.util.SparseBooleanArray; |
| import android.view.ActionMode; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.widget.AbsListView.MultiChoiceModeListener; |
| import android.widget.AdapterView; |
| import android.widget.AdapterView.OnItemClickListener; |
| import android.widget.TextView; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.Collection; |
| |
| /** |
| * MTP importer, main activity. |
| */ |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) |
| public class IngestActivity extends Activity implements |
| MtpDeviceIndex.ProgressListener, ImportTask.Listener { |
| |
| private IngestService mHelperService; |
| private boolean mActive = false; |
| private IngestGridView mGridView; |
| private MtpAdapter mAdapter; |
| private Handler mHandler; |
| private ProgressDialog mProgressDialog; |
| private ActionMode mActiveActionMode; |
| |
| private View mWarningView; |
| private TextView mWarningText; |
| private int mLastCheckedPosition = 0; |
| |
| private ViewPager mFullscreenPager; |
| private MtpPagerAdapter mPagerAdapter; |
| private boolean mFullscreenPagerVisible = false; |
| |
| private MenuItem mMenuSwitcherItem; |
| private MenuItem mActionMenuSwitcherItem; |
| |
| // The MTP framework components don't give us fine-grained file copy |
| // progress updates, so for large photos and videos, we will be stuck |
| // with a dialog not updating for a long time. To give the user feedback, |
| // we switch to the animated indeterminate progress bar after the timeout |
| // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from |
| // the framework, we switch back to the normal progress bar. |
| private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| doBindHelperService(); |
| |
| setContentView(R.layout.ingest_activity_item_list); |
| mGridView = (IngestGridView) findViewById(R.id.ingest_gridview); |
| mAdapter = new MtpAdapter(this); |
| mAdapter.registerDataSetObserver(mPrimaryObserver); |
| mGridView.setAdapter(mAdapter); |
| mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener); |
| mGridView.setOnItemClickListener(mOnItemClickListener); |
| mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker); |
| |
| mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager); |
| |
| mHandler = new ItemListHandler(this); |
| |
| MtpBitmapFetch.configureForContext(this); |
| } |
| |
| private OnItemClickListener mOnItemClickListener = new OnItemClickListener() { |
| @Override |
| public void onItemClick(AdapterView<?> adapterView, View itemView, int position, |
| long arg3) { |
| mLastCheckedPosition = position; |
| mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position)); |
| } |
| }; |
| |
| private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() { |
| private boolean mIgnoreItemCheckedStateChanges = false; |
| |
| private void updateSelectedTitle(ActionMode mode) { |
| int count = mGridView.getCheckedItemCount(); |
| mode.setTitle(getResources().getQuantityString( |
| R.plurals.ingest_number_of_items_selected, count, count)); |
| } |
| |
| @Override |
| public void onItemCheckedStateChanged(ActionMode mode, int position, long id, |
| boolean checked) { |
| if (mIgnoreItemCheckedStateChanges) { |
| return; |
| } |
| if (mAdapter.itemAtPositionIsBucket(position)) { |
| SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions(); |
| mIgnoreItemCheckedStateChanges = true; |
| mGridView.setItemChecked(position, false); |
| |
| // Takes advantage of the fact that SectionIndexer imposes the |
| // need to clamp to the valid range |
| int nextSectionStart = mAdapter.getPositionForSection( |
| mAdapter.getSectionForPosition(position) + 1); |
| if (nextSectionStart == position) { |
| nextSectionStart = mAdapter.getCount(); |
| } |
| |
| boolean rangeValue = false; // Value we want to set all of the bucket items to |
| |
| // Determine if all the items in the bucket are currently checked, so that we |
| // can uncheck them, otherwise we will check all items in the bucket. |
| for (int i = position + 1; i < nextSectionStart; i++) { |
| if (!checkedItems.get(i)) { |
| rangeValue = true; |
| break; |
| } |
| } |
| |
| // Set all items in the bucket to the desired state |
| for (int i = position + 1; i < nextSectionStart; i++) { |
| if (checkedItems.get(i) != rangeValue) { |
| mGridView.setItemChecked(i, rangeValue); |
| } |
| } |
| |
| mPositionMappingCheckBroker.onBulkCheckedChange(); |
| mIgnoreItemCheckedStateChanges = false; |
| } else { |
| mPositionMappingCheckBroker.onCheckedChange(position, checked); |
| } |
| mLastCheckedPosition = position; |
| updateSelectedTitle(mode); |
| } |
| |
| @Override |
| public boolean onActionItemClicked(ActionMode mode, MenuItem item) { |
| return onOptionsItemSelected(item); |
| } |
| |
| @Override |
| public boolean onCreateActionMode(ActionMode mode, Menu menu) { |
| MenuInflater inflater = mode.getMenuInflater(); |
| inflater.inflate(R.menu.ingest_menu_item_list_selection, menu); |
| updateSelectedTitle(mode); |
| mActiveActionMode = mode; |
| mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view); |
| setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible); |
| return true; |
| } |
| |
| @Override |
| public void onDestroyActionMode(ActionMode mode) { |
| mActiveActionMode = null; |
| mActionMenuSwitcherItem = null; |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE); |
| } |
| |
| @Override |
| public boolean onPrepareActionMode(ActionMode mode, Menu menu) { |
| updateSelectedTitle(mode); |
| return false; |
| } |
| }; |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| int id = item.getItemId(); |
| if (id == R.id.ingest_import_items) { |
| if (mActiveActionMode != null) { |
| mHelperService.importSelectedItems( |
| mGridView.getCheckedItemPositions(), |
| mAdapter); |
| mActiveActionMode.finish(); |
| } |
| return true; |
| } else if (id == R.id.ingest_switch_view) { |
| setFullscreenPagerVisibility(!mFullscreenPagerVisible); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean onCreateOptionsMenu(Menu menu) { |
| MenuInflater inflater = getMenuInflater(); |
| inflater.inflate(R.menu.ingest_menu_item_list_selection, menu); |
| mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view); |
| menu.findItem(R.id.ingest_import_items).setVisible(false); |
| setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible); |
| return true; |
| } |
| |
| @Override |
| protected void onDestroy() { |
| doUnbindHelperService(); |
| super.onDestroy(); |
| } |
| |
| @Override |
| protected void onResume() { |
| DateTileView.refreshLocale(); |
| mActive = true; |
| if (mHelperService != null) { |
| mHelperService.setClientActivity(this); |
| } |
| updateWarningView(); |
| super.onResume(); |
| } |
| |
| @Override |
| protected void onPause() { |
| if (mHelperService != null) { |
| mHelperService.setClientActivity(null); |
| } |
| mActive = false; |
| cleanupProgressDialog(); |
| super.onPause(); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| MtpBitmapFetch.configureForContext(this); |
| } |
| |
| private void showWarningView(int textResId) { |
| if (mWarningView == null) { |
| mWarningView = findViewById(R.id.ingest_warning_view); |
| mWarningText = |
| (TextView) mWarningView.findViewById(R.id.ingest_warning_view_text); |
| } |
| mWarningText.setText(textResId); |
| mWarningView.setVisibility(View.VISIBLE); |
| setFullscreenPagerVisibility(false); |
| mGridView.setVisibility(View.GONE); |
| setSwitcherMenuVisibility(false); |
| } |
| |
| private void hideWarningView() { |
| if (mWarningView != null) { |
| mWarningView.setVisibility(View.GONE); |
| setFullscreenPagerVisibility(false); |
| } |
| setSwitcherMenuVisibility(true); |
| } |
| |
| private PositionMappingCheckBroker mPositionMappingCheckBroker = |
| new PositionMappingCheckBroker(); |
| |
| private class PositionMappingCheckBroker extends CheckBroker |
| implements OnClearChoicesListener { |
| private int mLastMappingPager = -1; |
| private int mLastMappingGrid = -1; |
| |
| private int mapPagerToGridPosition(int position) { |
| if (position != mLastMappingPager) { |
| mLastMappingPager = position; |
| mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position); |
| } |
| return mLastMappingGrid; |
| } |
| |
| private int mapGridToPagerPosition(int position) { |
| if (position != mLastMappingGrid) { |
| mLastMappingGrid = position; |
| mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position); |
| } |
| return mLastMappingPager; |
| } |
| |
| @Override |
| public void setItemChecked(int position, boolean checked) { |
| mGridView.setItemChecked(mapPagerToGridPosition(position), checked); |
| } |
| |
| @Override |
| public void onCheckedChange(int position, boolean checked) { |
| if (mPagerAdapter != null) { |
| super.onCheckedChange(mapGridToPagerPosition(position), checked); |
| } |
| } |
| |
| @Override |
| public boolean isItemChecked(int position) { |
| return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position)); |
| } |
| |
| @Override |
| public void onClearChoices() { |
| onBulkCheckedChange(); |
| } |
| } |
| |
| private DataSetObserver mPrimaryObserver = new DataSetObserver() { |
| @Override |
| public void onChanged() { |
| if (mPagerAdapter != null) { |
| mPagerAdapter.notifyDataSetChanged(); |
| } |
| } |
| |
| @Override |
| public void onInvalidated() { |
| if (mPagerAdapter != null) { |
| mPagerAdapter.notifyDataSetChanged(); |
| } |
| } |
| }; |
| |
| private int pickFullscreenStartingPosition() { |
| int firstVisiblePosition = mGridView.getFirstVisiblePosition(); |
| if (mLastCheckedPosition <= firstVisiblePosition |
| || mLastCheckedPosition > mGridView.getLastVisiblePosition()) { |
| return firstVisiblePosition; |
| } else { |
| return mLastCheckedPosition; |
| } |
| } |
| |
| private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) { |
| if (menuItem == null) { |
| return; |
| } |
| if (!inFullscreenMode) { |
| menuItem.setIcon(android.R.drawable.ic_menu_zoom); |
| menuItem.setTitle(R.string.ingest_switch_photo_fullscreen); |
| } else { |
| menuItem.setIcon(android.R.drawable.ic_dialog_dialer); |
| menuItem.setTitle(R.string.ingest_switch_photo_grid); |
| } |
| } |
| |
| private void setFullscreenPagerVisibility(boolean visible) { |
| mFullscreenPagerVisible = visible; |
| if (visible) { |
| if (mPagerAdapter == null) { |
| mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker); |
| mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex()); |
| } |
| mFullscreenPager.setAdapter(mPagerAdapter); |
| mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels( |
| pickFullscreenStartingPosition()), false); |
| } else if (mPagerAdapter != null) { |
| mGridView.setSelection(mAdapter.translatePositionWithoutLabels( |
| mFullscreenPager.getCurrentItem())); |
| mFullscreenPager.setAdapter(null); |
| } |
| mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE); |
| mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); |
| if (mActionMenuSwitcherItem != null) { |
| setSwitcherMenuState(mActionMenuSwitcherItem, visible); |
| } |
| setSwitcherMenuState(mMenuSwitcherItem, visible); |
| } |
| |
| private void setSwitcherMenuVisibility(boolean visible) { |
| if (mActionMenuSwitcherItem != null) { |
| mActionMenuSwitcherItem.setVisible(visible); |
| } |
| if (mMenuSwitcherItem != null) { |
| mMenuSwitcherItem.setVisible(visible); |
| } |
| } |
| |
| private void updateWarningView() { |
| if (!mAdapter.deviceConnected()) { |
| showWarningView(R.string.ingest_no_device); |
| } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) { |
| showWarningView(R.string.ingest_empty_device); |
| } else { |
| hideWarningView(); |
| } |
| } |
| |
| private void uiThreadNotifyIndexChanged() { |
| mAdapter.notifyDataSetChanged(); |
| if (mActiveActionMode != null) { |
| mActiveActionMode.finish(); |
| mActiveActionMode = null; |
| } |
| updateWarningView(); |
| } |
| |
| protected void notifyIndexChanged() { |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED); |
| } |
| |
| private static class ProgressState { |
| String message; |
| String title; |
| int current; |
| int max; |
| |
| public void reset() { |
| title = null; |
| message = null; |
| current = 0; |
| max = 0; |
| } |
| } |
| |
| private ProgressState mProgressState = new ProgressState(); |
| |
| @Override |
| public void onObjectIndexed(IngestObjectInfo object, int numVisited) { |
| // Not guaranteed to be called on the UI thread |
| mProgressState.reset(); |
| mProgressState.max = 0; |
| mProgressState.message = getResources().getQuantityString( |
| R.plurals.ingest_number_of_items_scanned, numVisited, numVisited); |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE); |
| } |
| |
| @Override |
| public void onSortingStarted() { |
| // Not guaranteed to be called on the UI thread |
| mProgressState.reset(); |
| mProgressState.max = 0; |
| mProgressState.message = getResources().getString(R.string.ingest_sorting); |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE); |
| } |
| |
| @Override |
| public void onIndexingFinished() { |
| // Not guaranteed to be called on the UI thread |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE); |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED); |
| } |
| |
| @Override |
| public void onImportProgress(final int visitedCount, final int totalCount, |
| String pathIfSuccessful) { |
| // Not guaranteed to be called on the UI thread |
| mProgressState.reset(); |
| mProgressState.max = totalCount; |
| mProgressState.current = visitedCount; |
| mProgressState.title = getResources().getString(R.string.ingest_importing); |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE); |
| mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE); |
| mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE, |
| INDETERMINATE_SWITCH_TIMEOUT_MS); |
| } |
| |
| @Override |
| public void onImportFinish(Collection<IngestObjectInfo> objectsNotImported, |
| int numVisited) { |
| // Not guaranteed to be called on the UI thread |
| mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE); |
| mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE); |
| // TODO(georgescu): maybe show an extra dialog listing the ones that failed |
| // importing, if any? |
| } |
| |
| private ProgressDialog getProgressDialog() { |
| if (mProgressDialog == null || !mProgressDialog.isShowing()) { |
| mProgressDialog = new ProgressDialog(this); |
| mProgressDialog.setCancelable(false); |
| } |
| return mProgressDialog; |
| } |
| |
| private void updateProgressDialog() { |
| ProgressDialog dialog = getProgressDialog(); |
| boolean indeterminate = (mProgressState.max == 0); |
| dialog.setIndeterminate(indeterminate); |
| dialog.setProgressStyle(indeterminate ? ProgressDialog.STYLE_SPINNER |
| : ProgressDialog.STYLE_HORIZONTAL); |
| if (mProgressState.title != null) { |
| dialog.setTitle(mProgressState.title); |
| } |
| if (mProgressState.message != null) { |
| dialog.setMessage(mProgressState.message); |
| } |
| if (!indeterminate) { |
| dialog.setProgress(mProgressState.current); |
| dialog.setMax(mProgressState.max); |
| } |
| if (!dialog.isShowing()) { |
| dialog.show(); |
| } |
| } |
| |
| private void makeProgressDialogIndeterminate() { |
| ProgressDialog dialog = getProgressDialog(); |
| dialog.setIndeterminate(true); |
| } |
| |
| private void cleanupProgressDialog() { |
| if (mProgressDialog != null) { |
| mProgressDialog.dismiss(); |
| mProgressDialog = null; |
| } |
| } |
| |
| // This is static and uses a WeakReference in order to avoid leaking the Activity |
| private static class ItemListHandler extends Handler { |
| public static final int MSG_PROGRESS_UPDATE = 0; |
| public static final int MSG_PROGRESS_HIDE = 1; |
| public static final int MSG_NOTIFY_CHANGED = 2; |
| public static final int MSG_BULK_CHECKED_CHANGE = 3; |
| public static final int MSG_PROGRESS_INDETERMINATE = 4; |
| |
| WeakReference<IngestActivity> mParentReference; |
| |
| public ItemListHandler(IngestActivity parent) { |
| super(); |
| mParentReference = new WeakReference<IngestActivity>(parent); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| IngestActivity parent = mParentReference.get(); |
| if (parent == null || !parent.mActive) { |
| return; |
| } |
| switch (message.what) { |
| case MSG_PROGRESS_HIDE: |
| parent.cleanupProgressDialog(); |
| break; |
| case MSG_PROGRESS_UPDATE: |
| parent.updateProgressDialog(); |
| break; |
| case MSG_NOTIFY_CHANGED: |
| parent.uiThreadNotifyIndexChanged(); |
| break; |
| case MSG_BULK_CHECKED_CHANGE: |
| parent.mPositionMappingCheckBroker.onBulkCheckedChange(); |
| break; |
| case MSG_PROGRESS_INDETERMINATE: |
| parent.makeProgressDialogIndeterminate(); |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| private ServiceConnection mHelperServiceConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| mHelperService = ((IngestService.LocalBinder) service).getService(); |
| mHelperService.setClientActivity(IngestActivity.this); |
| MtpDeviceIndex index = mHelperService.getIndex(); |
| mAdapter.setMtpDeviceIndex(index); |
| if (mPagerAdapter != null) { |
| mPagerAdapter.setMtpDeviceIndex(index); |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName className) { |
| mHelperService = null; |
| } |
| }; |
| |
| private void doBindHelperService() { |
| bindService(new Intent(getApplicationContext(), IngestService.class), |
| mHelperServiceConnection, Context.BIND_AUTO_CREATE); |
| } |
| |
| private void doUnbindHelperService() { |
| if (mHelperService != null) { |
| mHelperService.setClientActivity(null); |
| unbindService(mHelperServiceConnection); |
| } |
| } |
| } |