blob: b0b3e05322affe8c51bff001912bb9ddc5ab75a7 [file] [log] [blame]
/*
* 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(mMasterObserver);
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 mMasterObserver = 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);
}
}
}