summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/ingest
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/ingest')
-rw-r--r--src/com/android/gallery3d/ingest/ImportTask.java95
-rw-r--r--src/com/android/gallery3d/ingest/IngestActivity.java570
-rw-r--r--src/com/android/gallery3d/ingest/IngestService.java320
-rw-r--r--src/com/android/gallery3d/ingest/MtpDeviceIndex.java596
-rw-r--r--src/com/android/gallery3d/ingest/SimpleDate.java114
-rw-r--r--src/com/android/gallery3d/ingest/adapter/CheckBroker.java56
-rw-r--r--src/com/android/gallery3d/ingest/adapter/MtpAdapter.java192
-rw-r--r--src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java102
-rw-r--r--src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java29
-rw-r--r--src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java106
-rw-r--r--src/com/android/gallery3d/ingest/ui/DateTileView.java107
-rw-r--r--src/com/android/gallery3d/ingest/ui/IngestGridView.java58
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java115
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpImageView.java280
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java106
15 files changed, 2846 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ingest/ImportTask.java b/src/com/android/gallery3d/ingest/ImportTask.java
new file mode 100644
index 000000000..7d2d641a5
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ImportTask.java
@@ -0,0 +1,95 @@
+/*
+ * 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 android.content.Context;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Environment;
+import android.os.PowerManager;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ImportTask implements Runnable {
+
+ public interface Listener {
+ void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful);
+
+ void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, int visitedCount);
+ }
+
+ static private final String WAKELOCK_LABEL = "MTP Import Task";
+
+ private Listener mListener;
+ private String mDestAlbumName;
+ private Collection<MtpObjectInfo> mObjectsToImport;
+ private MtpDevice mDevice;
+ private PowerManager.WakeLock mWakeLock;
+
+ public ImportTask(MtpDevice device, Collection<MtpObjectInfo> objectsToImport,
+ String destAlbumName, Context context) {
+ mDestAlbumName = destAlbumName;
+ mObjectsToImport = objectsToImport;
+ mDevice = device;
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, WAKELOCK_LABEL);
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void run() {
+ mWakeLock.acquire();
+ try {
+ List<MtpObjectInfo> objectsNotImported = new LinkedList<MtpObjectInfo>();
+ int visited = 0;
+ int total = mObjectsToImport.size();
+ mListener.onImportProgress(visited, total, null);
+ File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName);
+ dest.mkdirs();
+ for (MtpObjectInfo object : mObjectsToImport) {
+ visited++;
+ String importedPath = null;
+ if (GalleryUtils.hasSpaceForSize(object.getCompressedSize())) {
+ importedPath = new File(dest, object.getName()).getAbsolutePath();
+ if (!mDevice.importFile(object.getObjectHandle(), importedPath)) {
+ importedPath = null;
+ }
+ }
+ if (importedPath == null) {
+ objectsNotImported.add(object);
+ }
+ if (mListener != null) {
+ mListener.onImportProgress(visited, total, importedPath);
+ }
+ }
+ if (mListener != null) {
+ mListener.onImportFinish(objectsNotImported, visited);
+ }
+ } finally {
+ mListener = null;
+ mWakeLock.release();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/IngestActivity.java b/src/com/android/gallery3d/ingest/IngestActivity.java
new file mode 100644
index 000000000..687e9fd44
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestActivity.java
@@ -0,0 +1,570 @@
+/*
+ * 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 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.mtp.MtpObjectInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.support.v4.view.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 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.MtpBitmapFetch;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.IngestGridView;
+import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
+
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+
+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.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) == false) {
+ 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;
+ }
+ };
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.import_items:
+ if (mActiveActionMode != null) {
+ mHelperService.importSelectedItems(
+ mGridView.getCheckedItemPositions(),
+ mAdapter);
+ mActiveActionMode.finish();
+ }
+ return true;
+ case R.id.ingest_switch_view:
+ setFullscreenPagerVisibility(!mFullscreenPagerVisible);
+ return true;
+ default:
+ 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.import_items).setVisible(false);
+ setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible);
+ return true;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ doUnbindHelperService();
+ }
+
+ @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);
+ }
+
+ private void hideWarningView() {
+ if (mWarningView != null) {
+ mWarningView.setVisibility(View.GONE);
+ setFullscreenPagerVisibility(false);
+ }
+ }
+
+ 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.switch_photo_fullscreen);
+ } else {
+ menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
+ menuItem.setTitle(R.string.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 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(MtpObjectInfo 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 onSorting() {
+ // 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 onIndexFinish() {
+ // 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<MtpObjectInfo> 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: 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.hide();
+ 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);
+ }
+
+ 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() {
+ 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);
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/IngestService.java b/src/com/android/gallery3d/ingest/IngestService.java
new file mode 100644
index 000000000..0ce3ab6a9
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestService.java
@@ -0,0 +1,320 @@
+/*
+ * 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 android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.mtp.MtpDevice;
+import android.mtp.MtpDeviceInfo;
+import android.mtp.MtpObjectInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.util.SparseBooleanArray;
+import android.widget.Adapter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.NotificationIds;
+import com.android.gallery3d.data.MtpClient;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class IngestService extends Service implements ImportTask.Listener,
+ MtpDeviceIndex.ProgressListener, MtpClient.Listener {
+
+ public class LocalBinder extends Binder {
+ IngestService getService() {
+ return IngestService.this;
+ }
+ }
+
+ private static final int PROGRESS_UPDATE_INTERVAL_MS = 180;
+
+ private static MtpClient sClient;
+
+ private final IBinder mBinder = new LocalBinder();
+ private ScannerClient mScannerClient;
+ private MtpDevice mDevice;
+ private String mDevicePrettyName;
+ private MtpDeviceIndex mIndex;
+ private IngestActivity mClientActivity;
+ private boolean mRedeliverImportFinish = false;
+ private int mRedeliverImportFinishCount = 0;
+ private Collection<MtpObjectInfo> mRedeliverObjectsNotImported;
+ private boolean mRedeliverNotifyIndexChanged = false;
+ private boolean mRedeliverIndexFinish = false;
+ private NotificationManager mNotificationManager;
+ private NotificationCompat.Builder mNotificationBuilder;
+ private long mLastProgressIndexTime = 0;
+ private boolean mNeedRelaunchNotification = false;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mScannerClient = new ScannerClient(this);
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mNotificationBuilder = new NotificationCompat.Builder(this);
+ mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync) // TODO drawable
+ .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, IngestActivity.class), 0));
+ mIndex = MtpDeviceIndex.getInstance();
+ mIndex.setProgressListener(this);
+
+ if (sClient == null) {
+ sClient = new MtpClient(getApplicationContext());
+ }
+ List<MtpDevice> devices = sClient.getDeviceList();
+ if (devices.size() > 0) {
+ setDevice(devices.get(0));
+ }
+ sClient.addListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ sClient.removeListener(this);
+ mIndex.unsetProgressListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private void setDevice(MtpDevice device) {
+ if (mDevice == device) return;
+ mRedeliverImportFinish = false;
+ mRedeliverObjectsNotImported = null;
+ mRedeliverNotifyIndexChanged = false;
+ mRedeliverIndexFinish = false;
+ mDevice = device;
+ mIndex.setDevice(mDevice);
+ if (mDevice != null) {
+ MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
+ if (deviceInfo == null) {
+ setDevice(null);
+ return;
+ } else {
+ mDevicePrettyName = deviceInfo.getModel();
+ mNotificationBuilder.setContentTitle(mDevicePrettyName);
+ new Thread(mIndex.getIndexRunnable()).start();
+ }
+ } else {
+ mDevicePrettyName = null;
+ }
+ if (mClientActivity != null) {
+ mClientActivity.notifyIndexChanged();
+ } else {
+ mRedeliverNotifyIndexChanged = true;
+ }
+ }
+
+ protected MtpDeviceIndex getIndex() {
+ return mIndex;
+ }
+
+ protected void setClientActivity(IngestActivity activity) {
+ if (mClientActivity == activity) return;
+ mClientActivity = activity;
+ if (mClientActivity == null) {
+ if (mNeedRelaunchNotification) {
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.ingest_scanning_done));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+ mNotificationBuilder.build());
+ }
+ return;
+ }
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_IMPORTING);
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+ if (mRedeliverImportFinish) {
+ mClientActivity.onImportFinish(mRedeliverObjectsNotImported,
+ mRedeliverImportFinishCount);
+ mRedeliverImportFinish = false;
+ mRedeliverObjectsNotImported = null;
+ }
+ if (mRedeliverNotifyIndexChanged) {
+ mClientActivity.notifyIndexChanged();
+ mRedeliverNotifyIndexChanged = false;
+ }
+ if (mRedeliverIndexFinish) {
+ mClientActivity.onIndexFinish();
+ mRedeliverIndexFinish = false;
+ }
+ }
+
+ protected void importSelectedItems(SparseBooleanArray selected, Adapter adapter) {
+ List<MtpObjectInfo> importHandles = new ArrayList<MtpObjectInfo>();
+ for (int i = 0; i < selected.size(); i++) {
+ if (selected.valueAt(i)) {
+ Object item = adapter.getItem(selected.keyAt(i));
+ if (item instanceof MtpObjectInfo) {
+ importHandles.add(((MtpObjectInfo) item));
+ }
+ }
+ }
+ ImportTask task = new ImportTask(mDevice, importHandles, BucketNames.IMPORTED, this);
+ task.setListener(this);
+ mNotificationBuilder.setProgress(0, 0, true)
+ .setContentText(getResources().getText(R.string.ingest_importing));
+ startForeground(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ new Thread(task).start();
+ }
+
+ @Override
+ public void deviceAdded(MtpDevice device) {
+ if (mDevice == null) {
+ setDevice(device);
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
+ "DeviceConnected", null);
+ }
+ }
+
+ @Override
+ public void deviceRemoved(MtpDevice device) {
+ if (device == mDevice) {
+ setDevice(null);
+ mNeedRelaunchNotification = false;
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+ }
+ }
+
+ @Override
+ public void onImportProgress(int visitedCount, int totalCount,
+ String pathIfSuccessful) {
+ if (pathIfSuccessful != null) {
+ mScannerClient.scanPath(pathIfSuccessful);
+ }
+ mNeedRelaunchNotification = false;
+ if (mClientActivity != null) {
+ mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
+ }
+ mNotificationBuilder.setProgress(totalCount, visitedCount, false)
+ .setContentText(getResources().getText(R.string.ingest_importing));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ }
+
+ @Override
+ public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
+ int visitedCount) {
+ stopForeground(true);
+ mNeedRelaunchNotification = true;
+ if (mClientActivity != null) {
+ mClientActivity.onImportFinish(objectsNotImported, visitedCount);
+ } else {
+ mRedeliverImportFinish = true;
+ mRedeliverObjectsNotImported = objectsNotImported;
+ mRedeliverImportFinishCount = visitedCount;
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.import_complete));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ }
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
+ "ImportFinished", null, visitedCount);
+ }
+
+ @Override
+ public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
+ mNeedRelaunchNotification = false;
+ if (mClientActivity != null) {
+ mClientActivity.onObjectIndexed(object, numVisited);
+ } else {
+ // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds
+ long currentTime = SystemClock.uptimeMillis();
+ if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) {
+ mLastProgressIndexTime = currentTime;
+ mNotificationBuilder.setProgress(0, numVisited, true)
+ .setContentText(getResources().getText(R.string.ingest_scanning));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+ mNotificationBuilder.build());
+ }
+ }
+ }
+
+ @Override
+ public void onSorting() {
+ if (mClientActivity != null) mClientActivity.onSorting();
+ }
+
+ @Override
+ public void onIndexFinish() {
+ mNeedRelaunchNotification = true;
+ if (mClientActivity != null) {
+ mClientActivity.onIndexFinish();
+ } else {
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.ingest_scanning_done));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+ mNotificationBuilder.build());
+ mRedeliverIndexFinish = true;
+ }
+ }
+
+ // Copied from old Gallery3d code
+ private static final class ScannerClient implements MediaScannerConnectionClient {
+ ArrayList<String> mPaths = new ArrayList<String>();
+ MediaScannerConnection mScannerConnection;
+ boolean mConnected;
+ Object mLock = new Object();
+
+ public ScannerClient(Context context) {
+ mScannerConnection = new MediaScannerConnection(context, this);
+ }
+
+ public void scanPath(String path) {
+ synchronized (mLock) {
+ if (mConnected) {
+ mScannerConnection.scanFile(path, null);
+ } else {
+ mPaths.add(path);
+ mScannerConnection.connect();
+ }
+ }
+ }
+
+ @Override
+ public void onMediaScannerConnected() {
+ synchronized (mLock) {
+ mConnected = true;
+ if (!mPaths.isEmpty()) {
+ for (String path : mPaths) {
+ mScannerConnection.scanFile(path, null);
+ }
+ mPaths.clear();
+ }
+ }
+ }
+
+ @Override
+ public void onScanCompleted(String path, Uri uri) {
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/MtpDeviceIndex.java b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
new file mode 100644
index 000000000..d30f94a87
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
@@ -0,0 +1,596 @@
+/*
+ * 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 android.mtp.MtpConstants;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * MTP objects in the index are organized into "buckets," or groupings.
+ * At present, these buckets are based on the date an item was created.
+ *
+ * When the index is created, the buckets are sorted in their natural
+ * order, and the items within the buckets sorted by the date they are taken.
+ *
+ * The index enables the access of items and bucket labels as one unified list.
+ * For example, let's say we have the following data in the index:
+ * [Bucket A]: [photo 1], [photo 2]
+ * [Bucket B]: [photo 3]
+ *
+ * Then the items can be thought of as being organized as a 5 element list:
+ * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
+ *
+ * The data can also be accessed in descending order, in which case the list
+ * would be a bit different from simply reversing the ascending list, since the
+ * bucket labels need to always be at the beginning:
+ * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
+ *
+ * The index enables all the following operations in constant time, both for
+ * ascending and descending views of the data:
+ * - get/getAscending/getDescending: get an item at a specified list position
+ * - size: get the total number of items (bucket labels and MTP objects)
+ * - getFirstPositionForBucketNumber
+ * - getBucketNumberForPosition
+ * - isFirstInBucket
+ *
+ * See the comments in buildLookupIndex for implementation notes.
+ */
+public class MtpDeviceIndex {
+
+ public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants
+
+ public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
+ public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
+
+ static {
+ SUPPORTED_IMAGE_FORMATS = new HashSet<Integer>();
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_JFIF);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_EXIF_JPEG);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_PNG);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_GIF);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_BMP);
+
+ SUPPORTED_VIDEO_FORMATS = new HashSet<Integer>();
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_3GP_CONTAINER);
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_AVI);
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MP4_CONTAINER);
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MPEG);
+ // TODO: add FORMAT_MOV once Media Scanner supports .mov files
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId());
+ result = prime * result + mGeneration;
+ return result;
+ }
+
+ public interface ProgressListener {
+ public void onObjectIndexed(MtpObjectInfo object, int numVisited);
+
+ public void onSorting();
+
+ public void onIndexFinish();
+ }
+
+ public enum SortOrder {
+ Ascending, Descending
+ }
+
+ private MtpDevice mDevice;
+ private int[] mUnifiedLookupIndex;
+ private MtpObjectInfo[] mMtpObjects;
+ private DateBucket[] mBuckets;
+ private int mGeneration = 0;
+
+ public enum Progress {
+ Uninitialized, Initialized, Pending, Started, Sorting, Finished
+ }
+
+ private Progress mProgress = Progress.Uninitialized;
+ private ProgressListener mProgressListener;
+
+ private static final MtpDeviceIndex sInstance = new MtpDeviceIndex();
+ private static final MtpObjectTimestampComparator sMtpObjectComparator =
+ new MtpObjectTimestampComparator();
+
+ public static MtpDeviceIndex getInstance() {
+ return sInstance;
+ }
+
+ private MtpDeviceIndex() {
+ }
+
+ synchronized public MtpDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Sets the MtpDevice that should be indexed and initializes state, but does
+ * not kick off the actual indexing task, which is instead done by using
+ * {@link #getIndexRunnable()}
+ *
+ * @param device The MtpDevice that should be indexed
+ */
+ synchronized public void setDevice(MtpDevice device) {
+ if (device == mDevice) return;
+ mDevice = device;
+ resetState();
+ }
+
+ /**
+ * Provides a Runnable for the indexing task assuming the state has already
+ * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and
+ * has not already been run.
+ *
+ * @return Runnable for the main indexing task
+ */
+ synchronized public Runnable getIndexRunnable() {
+ if (mProgress != Progress.Initialized) return null;
+ mProgress = Progress.Pending;
+ return new IndexRunnable(mDevice);
+ }
+
+ synchronized public boolean indexReady() {
+ return mProgress == Progress.Finished;
+ }
+
+ synchronized public Progress getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * @param listener Listener to change to
+ * @return Progress at the time the listener was added (useful for
+ * configuring initial UI state)
+ */
+ synchronized public Progress setProgressListener(ProgressListener listener) {
+ mProgressListener = listener;
+ return mProgress;
+ }
+
+ /**
+ * Make the listener null if it matches the argument
+ *
+ * @param listener Listener to unset, if currently registered
+ */
+ synchronized public void unsetProgressListener(ProgressListener listener) {
+ if (mProgressListener == listener)
+ mProgressListener = null;
+ }
+
+ /**
+ * @return The total number of elements in the index (labels and items)
+ */
+ public int size() {
+ return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0;
+ }
+
+ /**
+ * @param position Index of item to fetch, where 0 is the first item in the
+ * specified order
+ * @param order
+ * @return the bucket label or MtpObjectInfo at the specified position and
+ * order
+ */
+ public Object get(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return null;
+ if(order == SortOrder.Ascending) {
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) {
+ return bucket.bucket;
+ } else {
+ return mMtpObjects[bucket.itemsStartIndex + position - 1
+ - bucket.unifiedStartIndex];
+ }
+ } else {
+ int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) {
+ return bucket.bucket;
+ } else {
+ return mMtpObjects[bucket.itemsStartIndex + zeroIndex
+ - bucket.unifiedStartIndex];
+ }
+ }
+ }
+
+ /**
+ * @param position Index of item to fetch from a view of the data that doesn't
+ * include labels and is in the specified order
+ * @return position-th item in specified order, when not including labels
+ */
+ public MtpObjectInfo getWithoutLabels(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return null;
+ if (order == SortOrder.Ascending) {
+ return mMtpObjects[position];
+ } else {
+ return mMtpObjects[mMtpObjects.length - 1 - position];
+ }
+ }
+
+ /**
+ * Although this is O(log(number of buckets)), and thus should not be used
+ * in hotspots, even if the attached device has items for every day for
+ * a five-year timeframe, it would still only take 11 iterations at most,
+ * so shouldn't be a huge issue.
+ * @param position Index of item to map from a view of the data that doesn't
+ * include labels and is in the specified order
+ * @param order
+ * @return position in a view of the data that does include labels
+ */
+ public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return -1;
+ if (order == SortOrder.Descending) {
+ position = mMtpObjects.length - 1 - position;
+ }
+ int bucketNumber = 0;
+ int iMin = 0;
+ int iMax = mBuckets.length - 1;
+ while (iMax >= iMin) {
+ int iMid = (iMax + iMin) / 2;
+ if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) {
+ iMin = iMid + 1;
+ } else if (mBuckets[iMid].itemsStartIndex > position) {
+ iMax = iMid - 1;
+ } else {
+ bucketNumber = iMid;
+ break;
+ }
+ }
+ int mappedPos = mBuckets[bucketNumber].unifiedStartIndex
+ + position - mBuckets[bucketNumber].itemsStartIndex;
+ if (order == SortOrder.Descending) {
+ mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos;
+ }
+ return mappedPos;
+ }
+
+ public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return -1;
+ if(order == SortOrder.Ascending) {
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) position++;
+ return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
+ } else {
+ int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--;
+ return mMtpObjects.length - 1 - bucket.itemsStartIndex
+ - zeroIndex + bucket.unifiedStartIndex;
+ }
+ }
+
+ /**
+ * @return The number of MTP items in the index (without labels)
+ */
+ public int sizeWithoutLabels() {
+ return mProgress == Progress.Finished ? mMtpObjects.length : 0;
+ }
+
+ public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
+ if (order == SortOrder.Ascending) {
+ return mBuckets[bucketNumber].unifiedStartIndex;
+ } else {
+ return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1;
+ }
+ }
+
+ public int getBucketNumberForPosition(int position, SortOrder order) {
+ if (order == SortOrder.Ascending) {
+ return mUnifiedLookupIndex[position];
+ } else {
+ return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position];
+ }
+ }
+
+ public boolean isFirstInBucket(int position, SortOrder order) {
+ if (order == SortOrder.Ascending) {
+ return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position;
+ } else {
+ position = mUnifiedLookupIndex.length - 1 - position;
+ return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position;
+ }
+ }
+
+ private Object[] mCachedReverseBuckets;
+
+ public Object[] getBuckets(SortOrder order) {
+ if (mBuckets == null) return null;
+ if (order == SortOrder.Ascending) {
+ return mBuckets;
+ } else {
+ if (mCachedReverseBuckets == null) {
+ computeReversedBuckets();
+ }
+ return mCachedReverseBuckets;
+ }
+ }
+
+ /*
+ * See the comments for buildLookupIndex for notes on the specific fields of
+ * this class.
+ */
+ private class DateBucket implements Comparable<DateBucket> {
+ SimpleDate bucket;
+ List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>();
+ int unifiedStartIndex;
+ int unifiedEndIndex;
+ int itemsStartIndex;
+ int numItems;
+
+ public DateBucket(SimpleDate bucket) {
+ this.bucket = bucket;
+ }
+
+ public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) {
+ this(bucket);
+ tempElementsList.add(firstElement);
+ }
+
+ void sortElements(Comparator<MtpObjectInfo> comparator) {
+ Collections.sort(tempElementsList, comparator);
+ }
+
+ @Override
+ public String toString() {
+ return bucket.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return bucket.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof DateBucket)) return false;
+ DateBucket other = (DateBucket) obj;
+ if (bucket == null) {
+ if (other.bucket != null) return false;
+ } else if (!bucket.equals(other.bucket)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(DateBucket another) {
+ return this.bucket.compareTo(another.bucket);
+ }
+ }
+
+ /**
+ * Comparator to sort MtpObjectInfo objects by date created.
+ */
+ private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> {
+ @Override
+ public int compare(MtpObjectInfo o1, MtpObjectInfo o2) {
+ long diff = o1.getDateCreated() - o2.getDateCreated();
+ if (diff < 0) {
+ return -1;
+ } else if (diff == 0) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+
+ private void resetState() {
+ mGeneration++;
+ mUnifiedLookupIndex = null;
+ mMtpObjects = null;
+ mBuckets = null;
+ mCachedReverseBuckets = null;
+ mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
+ }
+
+
+ private class IndexRunnable implements Runnable {
+ private int[] mUnifiedLookupIndex;
+ private MtpObjectInfo[] mMtpObjects;
+ private DateBucket[] mBuckets;
+ private Map<SimpleDate, DateBucket> mBucketsTemp;
+ private MtpDevice mDevice;
+ private int mNumObjects = 0;
+
+ private class IndexingException extends Exception {};
+
+ public IndexRunnable(MtpDevice device) {
+ mDevice = device;
+ }
+
+ /*
+ * Implementation note: this is the way the index supports a lot of its operations in
+ * constant time and respecting the need to have bucket names always come before items
+ * in that bucket when accessing the list sequentially, both in ascending and descending
+ * orders.
+ *
+ * Let's say the data we have in the index is the following:
+ * [Bucket A]: [photo 1], [photo 2]
+ * [Bucket B]: [photo 3]
+ *
+ * In this case, the lookup index array would be
+ * [0, 0, 0, 1, 1]
+ *
+ * Now, whether we access the list in ascending or descending order, we know which bucket
+ * to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first
+ * item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex
+ * that correspond to indices in this lookup index array, allowing us to calculate the
+ * offset of the specific item we want from within a specific bucket.
+ */
+ private void buildLookupIndex() {
+ int numBuckets = mBuckets.length;
+ mUnifiedLookupIndex = new int[mNumObjects + numBuckets];
+ int currentUnifiedIndexEntry = 0;
+ int nextUnifiedEntry;
+
+ mMtpObjects = new MtpObjectInfo[mNumObjects];
+ int currentItemsEntry = 0;
+ for (int i = 0; i < numBuckets; i++) {
+ DateBucket bucket = mBuckets[i];
+ nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1;
+ Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
+ bucket.unifiedStartIndex = currentUnifiedIndexEntry;
+ bucket.unifiedEndIndex = nextUnifiedEntry - 1;
+ currentUnifiedIndexEntry = nextUnifiedEntry;
+
+ bucket.itemsStartIndex = currentItemsEntry;
+ bucket.numItems = bucket.tempElementsList.size();
+ for (int j = 0; j < bucket.numItems; j++) {
+ mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
+ currentItemsEntry++;
+ }
+ bucket.tempElementsList = null;
+ }
+ }
+
+ private void copyResults() {
+ MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex;
+ MtpDeviceIndex.this.mMtpObjects = mMtpObjects;
+ MtpDeviceIndex.this.mBuckets = mBuckets;
+ mUnifiedLookupIndex = null;
+ mMtpObjects = null;
+ mBuckets = null;
+ }
+
+ @Override
+ public void run() {
+ try {
+ indexDevice();
+ } catch (IndexingException e) {
+ synchronized (MtpDeviceIndex.this) {
+ resetState();
+ if (mProgressListener != null) {
+ mProgressListener.onIndexFinish();
+ }
+ }
+ }
+ }
+
+ private void indexDevice() throws IndexingException {
+ synchronized (MtpDeviceIndex.this) {
+ mProgress = Progress.Started;
+ }
+ mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
+ for (int storageId : mDevice.getStorageIds()) {
+ if (mDevice != getDevice()) throw new IndexingException();
+ Stack<Integer> pendingDirectories = new Stack<Integer>();
+ pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
+ while (!pendingDirectories.isEmpty()) {
+ if (mDevice != getDevice()) throw new IndexingException();
+ int dirHandle = pendingDirectories.pop();
+ for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
+ MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
+ if (objectInfo == null) throw new IndexingException();
+ int format = objectInfo.getFormat();
+ if (format == MtpConstants.FORMAT_ASSOCIATION) {
+ pendingDirectories.add(objectHandle);
+ } else if (SUPPORTED_IMAGE_FORMATS.contains(format)
+ || SUPPORTED_VIDEO_FORMATS.contains(format)) {
+ addObject(objectInfo);
+ }
+ }
+ }
+ }
+ Collection<DateBucket> values = mBucketsTemp.values();
+ mBucketsTemp = null;
+ mBuckets = values.toArray(new DateBucket[values.size()]);
+ values = null;
+ synchronized (MtpDeviceIndex.this) {
+ mProgress = Progress.Sorting;
+ if (mProgressListener != null) {
+ mProgressListener.onSorting();
+ }
+ }
+ sortAll();
+ buildLookupIndex();
+ synchronized (MtpDeviceIndex.this) {
+ if (mDevice != getDevice()) throw new IndexingException();
+ copyResults();
+
+ /*
+ * In order for getBuckets to operate in constant time for descending
+ * order, we must precompute a reversed array of the buckets, mainly
+ * because the android.widget.SectionIndexer interface which adapters
+ * that call getBuckets implement depends on section numbers to be
+ * ascending relative to the scroll position, so we must have this for
+ * descending order or the scrollbar goes crazy.
+ */
+ computeReversedBuckets();
+
+ mProgress = Progress.Finished;
+ if (mProgressListener != null) {
+ mProgressListener.onIndexFinish();
+ }
+ }
+ }
+
+ private SimpleDate mDateInstance = new SimpleDate();
+
+ private void addObject(MtpObjectInfo objectInfo) {
+ mNumObjects++;
+ mDateInstance.setTimestamp(objectInfo.getDateCreated());
+ DateBucket bucket = mBucketsTemp.get(mDateInstance);
+ if (bucket == null) {
+ bucket = new DateBucket(mDateInstance, objectInfo);
+ mBucketsTemp.put(mDateInstance, bucket);
+ mDateInstance = new SimpleDate(); // only create new date
+ // objects when they are used
+ return;
+ } else {
+ bucket.tempElementsList.add(objectInfo);
+ }
+ if (mProgressListener != null) {
+ mProgressListener.onObjectIndexed(objectInfo, mNumObjects);
+ }
+ }
+
+ private void sortAll() {
+ Arrays.sort(mBuckets);
+ for (DateBucket bucket : mBuckets) {
+ bucket.sortElements(sMtpObjectComparator);
+ }
+ }
+
+ }
+
+ private void computeReversedBuckets() {
+ mCachedReverseBuckets = new Object[mBuckets.length];
+ for (int i = 0; i < mCachedReverseBuckets.length; i++) {
+ mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i];
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/SimpleDate.java b/src/com/android/gallery3d/ingest/SimpleDate.java
new file mode 100644
index 000000000..05db2cde2
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/SimpleDate.java
@@ -0,0 +1,114 @@
+/*
+ * 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 java.text.DateFormat;
+import java.util.Calendar;
+
+/**
+ * Represents a date (year, month, day)
+ */
+public class SimpleDate implements Comparable<SimpleDate> {
+ public int month; // MM
+ public int day; // DD
+ public int year; // YYYY
+ private long timestamp;
+ private String mCachedStringRepresentation;
+
+ public SimpleDate() {
+ }
+
+ public SimpleDate(long timestamp) {
+ setTimestamp(timestamp);
+ }
+
+ private static Calendar sCalendarInstance = Calendar.getInstance();
+
+ public void setTimestamp(long timestamp) {
+ synchronized (sCalendarInstance) {
+ // TODO find a more efficient way to convert a timestamp to a date?
+ sCalendarInstance.setTimeInMillis(timestamp);
+ this.day = sCalendarInstance.get(Calendar.DATE);
+ this.month = sCalendarInstance.get(Calendar.MONTH);
+ this.year = sCalendarInstance.get(Calendar.YEAR);
+ this.timestamp = timestamp;
+ mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + day;
+ result = prime * result + month;
+ result = prime * result + year;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (!(obj instanceof SimpleDate))
+ return false;
+ SimpleDate other = (SimpleDate) obj;
+ if (year != other.year)
+ return false;
+ if (month != other.month)
+ return false;
+ if (day != other.day)
+ return false;
+ return true;
+ }
+
+ @Override
+ public int compareTo(SimpleDate other) {
+ int yearDiff = this.year - other.getYear();
+ if (yearDiff != 0)
+ return yearDiff;
+ else {
+ int monthDiff = this.month - other.getMonth();
+ if (monthDiff != 0)
+ return monthDiff;
+ else
+ return this.day - other.getDay();
+ }
+ }
+
+ public int getDay() {
+ return day;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ @Override
+ public String toString() {
+ if (mCachedStringRepresentation == null) {
+ mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp);
+ }
+ return mCachedStringRepresentation;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/CheckBroker.java b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java
new file mode 100644
index 000000000..6783f23c5
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java
@@ -0,0 +1,56 @@
+/*
+ * 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.adapter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public abstract class CheckBroker {
+ private Collection<OnCheckedChangedListener> mListeners =
+ new ArrayList<OnCheckedChangedListener>();
+
+ public interface OnCheckedChangedListener {
+ public void onCheckedChanged(int position, boolean isChecked);
+ public void onBulkCheckedChanged();
+ }
+
+ public abstract void setItemChecked(int position, boolean checked);
+
+ public void onCheckedChange(int position, boolean checked) {
+ if (isItemChecked(position) != checked) {
+ for (OnCheckedChangedListener l : mListeners) {
+ l.onCheckedChanged(position, checked);
+ }
+ }
+ }
+
+ public void onBulkCheckedChange() {
+ for (OnCheckedChangedListener l : mListeners) {
+ l.onBulkCheckedChanged();
+ }
+ }
+
+ public abstract boolean isItemChecked(int position);
+
+ public void registerOnCheckedChangeListener(OnCheckedChangedListener l) {
+ mListeners.add(l);
+ }
+
+ public void unregisterOnCheckedChangeListener(OnCheckedChangedListener l) {
+ mListeners.remove(l);
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
new file mode 100644
index 000000000..e8dd69f8c
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
@@ -0,0 +1,192 @@
+/*
+ * 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.adapter;
+
+import android.app.Activity;
+import android.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.SectionIndexer;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.SimpleDate;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.MtpThumbnailTileView;
+
+public class MtpAdapter extends BaseAdapter implements SectionIndexer {
+ public static final int ITEM_TYPE_MEDIA = 0;
+ public static final int ITEM_TYPE_BUCKET = 1;
+
+ private Context mContext;
+ private MtpDeviceIndex mModel;
+ private SortOrder mSortOrder = SortOrder.Descending;
+ private LayoutInflater mInflater;
+ private int mGeneration = 0;
+
+ public MtpAdapter(Activity context) {
+ super();
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void setMtpDeviceIndex(MtpDeviceIndex index) {
+ mModel = index;
+ notifyDataSetChanged();
+ }
+
+ public MtpDeviceIndex getMtpDeviceIndex() {
+ return mModel;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ mGeneration++;
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifyDataSetInvalidated() {
+ mGeneration++;
+ super.notifyDataSetInvalidated();
+ }
+
+ public boolean deviceConnected() {
+ return (mModel != null) && (mModel.getDevice() != null);
+ }
+
+ public boolean indexReady() {
+ return (mModel != null) && mModel.indexReady();
+ }
+
+ @Override
+ public int getCount() {
+ return mModel != null ? mModel.size() : 0;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mModel.get(position, mSortOrder);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ // If the position is the first in its section, then it corresponds to
+ // a title tile, if not it's a media tile
+ if (position == getPositionForSection(getSectionForPosition(position))) {
+ return ITEM_TYPE_BUCKET;
+ } else {
+ return ITEM_TYPE_MEDIA;
+ }
+ }
+
+ public boolean itemAtPositionIsBucket(int position) {
+ return getItemViewType(position) == ITEM_TYPE_BUCKET;
+ }
+
+ public boolean itemAtPositionIsMedia(int position) {
+ return getItemViewType(position) == ITEM_TYPE_MEDIA;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ int type = getItemViewType(position);
+ if (type == ITEM_TYPE_MEDIA) {
+ MtpThumbnailTileView imageView;
+ if (convertView == null) {
+ imageView = (MtpThumbnailTileView) mInflater.inflate(
+ R.layout.ingest_thumbnail, parent, false);
+ } else {
+ imageView = (MtpThumbnailTileView) convertView;
+ }
+ imageView.setMtpDeviceAndObjectInfo(mModel.getDevice(), (MtpObjectInfo)getItem(position), mGeneration);
+ return imageView;
+ } else {
+ DateTileView dateTile;
+ if (convertView == null) {
+ dateTile = (DateTileView) mInflater.inflate(
+ R.layout.ingest_date_tile, parent, false);
+ } else {
+ dateTile = (DateTileView) convertView;
+ }
+ dateTile.setDate((SimpleDate)getItem(position));
+ return dateTile;
+ }
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ if (getCount() == 0) {
+ return 0;
+ }
+ int numSections = getSections().length;
+ if (section >= numSections) {
+ section = numSections - 1;
+ }
+ return mModel.getFirstPositionForBucketNumber(section, mSortOrder);
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ int count = getCount();
+ if (count == 0) {
+ return 0;
+ }
+ if (position >= count) {
+ position = count - 1;
+ }
+ return mModel.getBucketNumberForPosition(position, mSortOrder);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return getCount() > 0 ? mModel.getBuckets(mSortOrder) : null;
+ }
+
+ public SortOrder getSortOrder() {
+ return mSortOrder;
+ }
+
+ public int translatePositionWithoutLabels(int position) {
+ if (mModel == null) return -1;
+ return mModel.getPositionFromPositionWithoutLabels(position, mSortOrder);
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
new file mode 100644
index 000000000..9e7abc01d
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
@@ -0,0 +1,102 @@
+/*
+ * 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.adapter;
+
+import android.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.ui.MtpFullscreenView;
+
+public class MtpPagerAdapter extends PagerAdapter {
+
+ private LayoutInflater mInflater;
+ private int mGeneration = 0;
+ private CheckBroker mBroker;
+ private MtpDeviceIndex mModel;
+ private SortOrder mSortOrder = SortOrder.Descending;
+
+ private MtpFullscreenView mReusableView = null;
+
+ public MtpPagerAdapter(Context context, CheckBroker broker) {
+ super();
+ mInflater = LayoutInflater.from(context);
+ mBroker = broker;
+ }
+
+ public void setMtpDeviceIndex(MtpDeviceIndex index) {
+ mModel = index;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mModel != null ? mModel.sizeWithoutLabels() : 0;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ mGeneration++;
+ super.notifyDataSetChanged();
+ }
+
+ public int translatePositionWithLabels(int position) {
+ if (mModel == null) return -1;
+ return mModel.getPositionWithoutLabelsFromPosition(position, mSortOrder);
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ mReusableView = null;
+ super.finishUpdate(container);
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ MtpFullscreenView v = (MtpFullscreenView)object;
+ container.removeView(v);
+ mBroker.unregisterOnCheckedChangeListener(v);
+ mReusableView = v;
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ MtpFullscreenView v;
+ if (mReusableView != null) {
+ v = mReusableView;
+ mReusableView = null;
+ } else {
+ v = (MtpFullscreenView) mInflater.inflate(R.layout.ingest_fullsize, container, false);
+ }
+ MtpObjectInfo i = mModel.getWithoutLabels(position, mSortOrder);
+ v.getImageView().setMtpDeviceAndObjectInfo(mModel.getDevice(), i, mGeneration);
+ v.setPositionAndBroker(position, mBroker);
+ container.addView(v);
+ return v;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
new file mode 100644
index 000000000..bbc90f670
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
@@ -0,0 +1,29 @@
+/*
+ * 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.data;
+
+import android.graphics.Bitmap;
+
+public class BitmapWithMetadata {
+ public Bitmap bitmap;
+ public int rotationDegrees;
+
+ public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) {
+ this.bitmap = bitmap;
+ this.rotationDegrees = rotationDegrees;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
new file mode 100644
index 000000000..30868c22b
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
@@ -0,0 +1,106 @@
+/*
+ * 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.data;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.android.camera.Exif;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class MtpBitmapFetch {
+ private static int sMaxSize = 0;
+
+ public static void recycleThumbnail(Bitmap b) {
+ if (b != null) {
+ GalleryBitmapPool.getInstance().put(b);
+ }
+ }
+
+ public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) {
+ byte[] imageBytes = device.getThumbnail(info.getObjectHandle());
+ if (imageBytes == null) {
+ return null;
+ }
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ if (o.outWidth == 0 || o.outHeight == 0) {
+ return null;
+ }
+ o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight);
+ o.inMutable = true;
+ o.inJustDecodeBounds = false;
+ o.inSampleSize = 1;
+ try {
+ return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ } catch (IllegalArgumentException e) {
+ // BitmapFactory throws an exception rather than returning null
+ // when image decoding fails and an existing bitmap was supplied
+ // for recycling, even if the failure was not caused by the use
+ // of that bitmap.
+ return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ }
+ }
+
+ public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) {
+ return getFullsize(device, info, sMaxSize);
+ }
+
+ public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) {
+ byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize());
+ if (imageBytes == null) {
+ return null;
+ }
+ Bitmap created;
+ if (maxSide > 0) {
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ int w = o.outWidth;
+ int h = o.outHeight;
+ int comp = Math.max(h, w);
+ int sampleSize = 1;
+ while ((comp >> 1) >= maxSide) {
+ comp = comp >> 1;
+ sampleSize++;
+ }
+ o.inSampleSize = sampleSize;
+ o.inJustDecodeBounds = false;
+ created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ } else {
+ created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ }
+ if (created == null) {
+ return null;
+ }
+
+ return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes));
+ }
+
+ public static void configureForContext(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels);
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/DateTileView.java b/src/com/android/gallery3d/ingest/ui/DateTileView.java
new file mode 100644
index 000000000..52fe9b85b
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/DateTileView.java
@@ -0,0 +1,107 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.SimpleDate;
+
+import java.text.DateFormatSymbols;
+import java.util.Locale;
+
+public class DateTileView extends FrameLayout {
+ private static String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths();
+ private static Locale sLocale;
+
+ static {
+ refreshLocale();
+ }
+
+ public static boolean refreshLocale() {
+ Locale currentLocale = Locale.getDefault();
+ if (!currentLocale.equals(sLocale)) {
+ sLocale = currentLocale;
+ sMonthNames = DateFormatSymbols.getInstance(sLocale).getShortMonths();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private TextView mDateTextView;
+ private TextView mMonthTextView;
+ private TextView mYearTextView;
+ private int mMonth = -1;
+ private int mYear = -1;
+ private int mDate = -1;
+ private String[] mMonthNames = sMonthNames;
+
+ public DateTileView(Context context) {
+ super(context);
+ }
+
+ public DateTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DateTileView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Force this to be square
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mDateTextView = (TextView) findViewById(R.id.date_tile_day);
+ mMonthTextView = (TextView) findViewById(R.id.date_tile_month);
+ mYearTextView = (TextView) findViewById(R.id.date_tile_year);
+ }
+
+ public void setDate(SimpleDate date) {
+ setDate(date.getDay(), date.getMonth(), date.getYear());
+ }
+
+ public void setDate(int date, int month, int year) {
+ if (date != mDate) {
+ mDate = date;
+ mDateTextView.setText(mDate > 9 ? Integer.toString(mDate) : "0" + mDate);
+ }
+ if (mMonthNames != sMonthNames) {
+ mMonthNames = sMonthNames;
+ if (month == mMonth) {
+ mMonthTextView.setText(mMonthNames[mMonth]);
+ }
+ }
+ if (month != mMonth) {
+ mMonth = month;
+ mMonthTextView.setText(mMonthNames[mMonth]);
+ }
+ if (year != mYear) {
+ mYear = year;
+ mYearTextView.setText(Integer.toString(mYear));
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/IngestGridView.java b/src/com/android/gallery3d/ingest/ui/IngestGridView.java
new file mode 100644
index 000000000..c821259fe
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/IngestGridView.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+/**
+ * This just extends GridView with the ability to listen for calls
+ * to clearChoices()
+ */
+public class IngestGridView extends GridView {
+
+ public interface OnClearChoicesListener {
+ public void onClearChoices();
+ }
+
+ private OnClearChoicesListener mOnClearChoicesListener = null;
+
+ public IngestGridView(Context context) {
+ super(context);
+ }
+
+ public IngestGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public IngestGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setOnClearChoicesListener(OnClearChoicesListener l) {
+ mOnClearChoicesListener = l;
+ }
+
+ @Override
+ public void clearChoices() {
+ super.clearChoices();
+ if (mOnClearChoicesListener != null) {
+ mOnClearChoicesListener.onClearChoices();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
new file mode 100644
index 000000000..8d3884dc6
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
@@ -0,0 +1,115 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.CheckBox;
+import android.widget.Checkable;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+
+public class MtpFullscreenView extends RelativeLayout implements Checkable,
+ CompoundButton.OnCheckedChangeListener, CheckBroker.OnCheckedChangedListener {
+
+ private MtpImageView mImageView;
+ private CheckBox mCheckbox;
+ private int mPosition = -1;
+ private CheckBroker mBroker;
+
+ public MtpFullscreenView(Context context) {
+ super(context);
+ }
+
+ public MtpFullscreenView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MtpFullscreenView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = (MtpImageView) findViewById(R.id.ingest_fullsize_image);
+ mCheckbox = (CheckBox) findViewById(R.id.ingest_fullsize_image_checkbox);
+ mCheckbox.setOnCheckedChangeListener(this);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mCheckbox.isChecked();
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mCheckbox.setChecked(checked);
+ }
+
+ @Override
+ public void toggle() {
+ mCheckbox.toggle();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ setPositionAndBroker(-1, null);
+ super.onDetachedFromWindow();
+ }
+
+ public MtpImageView getImageView() {
+ return mImageView;
+ }
+
+ public int getPosition() {
+ return mPosition;
+ }
+
+ public void setPositionAndBroker(int position, CheckBroker b) {
+ if (mBroker != null) {
+ mBroker.unregisterOnCheckedChangeListener(this);
+ }
+ mPosition = position;
+ mBroker = b;
+ if (mBroker != null) {
+ setChecked(mBroker.isItemChecked(position));
+ mBroker.registerOnCheckedChangeListener(this);
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
+ if (mBroker != null) mBroker.setItemChecked(mPosition, isChecked);
+ }
+
+ @Override
+ public void onCheckedChanged(int position, boolean isChecked) {
+ if (position == mPosition) {
+ setChecked(isChecked);
+ }
+ }
+
+ @Override
+ public void onBulkCheckedChanged() {
+ if(mBroker != null) setChecked(mBroker.isItemChecked(mPosition));
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpImageView.java b/src/com/android/gallery3d/ingest/ui/MtpImageView.java
new file mode 100644
index 000000000..80c105126
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpImageView.java
@@ -0,0 +1,280 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.data.BitmapWithMetadata;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
+import java.lang.ref.WeakReference;
+
+public class MtpImageView extends ImageView {
+ // We will use the thumbnail for images larger than this threshold
+ private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes
+
+ private int mObjectHandle;
+ private int mGeneration;
+
+ private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this);
+ private Object mFetchLock = new Object();
+ private boolean mFetchPending = false;
+ private MtpObjectInfo mFetchObjectInfo;
+ private MtpDevice mFetchDevice;
+ private Object mFetchResult;
+ private Drawable mOverlayIcon;
+ private boolean mShowOverlayIcon;
+
+ private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread();
+ private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler();
+
+ private void init() {
+ showPlaceholder();
+ }
+
+ public MtpImageView(Context context) {
+ super(context);
+ init();
+ }
+
+ public MtpImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public MtpImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void showPlaceholder() {
+ setImageResource(android.R.color.transparent);
+ }
+
+ public void setMtpDeviceAndObjectInfo(MtpDevice device, MtpObjectInfo object, int gen) {
+ int handle = object.getObjectHandle();
+ if (handle == mObjectHandle && gen == mGeneration) {
+ return;
+ }
+ cancelLoadingAndClear();
+ showPlaceholder();
+ mGeneration = gen;
+ mObjectHandle = handle;
+ mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat());
+ if (mShowOverlayIcon && mOverlayIcon == null) {
+ mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play);
+ updateOverlayIconBounds();
+ }
+ synchronized (mFetchLock) {
+ mFetchObjectInfo = object;
+ mFetchDevice = device;
+ if (mFetchPending) return;
+ mFetchPending = true;
+ sFetchHandler.sendMessage(
+ sFetchHandler.obtainMessage(0, mWeakReference));
+ }
+ }
+
+ protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
+ if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE
+ && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) {
+ return MtpBitmapFetch.getFullsize(device, info);
+ } else {
+ return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0);
+ }
+ }
+
+ private float mLastBitmapWidth;
+ private float mLastBitmapHeight;
+ private int mLastRotationDegrees;
+ private Matrix mDrawMatrix = new Matrix();
+
+ private void updateDrawMatrix() {
+ mDrawMatrix.reset();
+ float dwidth;
+ float dheight;
+ float vheight = getHeight();
+ float vwidth = getWidth();
+ float scale;
+ boolean rotated90 = (mLastRotationDegrees % 180 != 0);
+ if (rotated90) {
+ dwidth = mLastBitmapHeight;
+ dheight = mLastBitmapWidth;
+ } else {
+ dwidth = mLastBitmapWidth;
+ dheight = mLastBitmapHeight;
+ }
+ if (dwidth <= vwidth && dheight <= vheight) {
+ scale = 1.0f;
+ } else {
+ scale = Math.min(vwidth / dwidth, vheight / dheight);
+ }
+ mDrawMatrix.setScale(scale, scale);
+ if (rotated90) {
+ mDrawMatrix.postTranslate(-dheight * scale * 0.5f,
+ -dwidth * scale * 0.5f);
+ mDrawMatrix.postRotate(mLastRotationDegrees);
+ mDrawMatrix.postTranslate(dwidth * scale * 0.5f,
+ dheight * scale * 0.5f);
+ }
+ mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f,
+ (vheight - dheight * scale) * 0.5f);
+ if (!rotated90 && mLastRotationDegrees > 0) {
+ // rotated by a multiple of 180
+ mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2);
+ }
+ setImageMatrix(mDrawMatrix);
+ }
+
+ private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4;
+
+ private void updateOverlayIconBounds() {
+ int iheight = mOverlayIcon.getIntrinsicHeight();
+ int iwidth = mOverlayIcon.getIntrinsicWidth();
+ int vheight = getHeight();
+ int vwidth = getWidth();
+ float scale_height = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR);
+ float scale_width = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR);
+ if (scale_height >= 1f && scale_width >= 1f) {
+ mOverlayIcon.setBounds((vwidth - iwidth) / 2,
+ (vheight - iheight) / 2,
+ (vwidth + iwidth) / 2,
+ (vheight + iheight) / 2);
+ } else {
+ float scale = Math.min(scale_height, scale_width);
+ mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2,
+ (int) (vheight - scale * iheight) / 2,
+ (int) (vwidth + scale * iwidth) / 2,
+ (int) (vheight + scale * iheight) / 2);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed && getScaleType() == ScaleType.MATRIX) {
+ updateDrawMatrix();
+ }
+ if (mShowOverlayIcon && changed && mOverlayIcon != null) {
+ updateOverlayIconBounds();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mShowOverlayIcon && mOverlayIcon != null) {
+ mOverlayIcon.draw(canvas);
+ }
+ }
+
+ protected void onMtpImageDataFetchedFromDevice(Object result) {
+ BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata)result;
+ if (getScaleType() == ScaleType.MATRIX) {
+ mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight();
+ mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth();
+ mLastRotationDegrees = bitmapWithMetadata.rotationDegrees;
+ updateDrawMatrix();
+ } else {
+ setRotation(bitmapWithMetadata.rotationDegrees);
+ }
+ setAlpha(0f);
+ setImageBitmap(bitmapWithMetadata.bitmap);
+ animate().alpha(1f);
+ }
+
+ protected void cancelLoadingAndClear() {
+ synchronized (mFetchLock) {
+ mFetchDevice = null;
+ mFetchObjectInfo = null;
+ mFetchResult = null;
+ }
+ animate().cancel();
+ setImageResource(android.R.color.transparent);
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ cancelLoadingAndClear();
+ super.onDetachedFromWindow();
+ }
+
+ private static class FetchImageHandler extends Handler {
+ public FetchImageHandler(Looper l) {
+ super(l);
+ }
+
+ public static FetchImageHandler createOnNewThread() {
+ HandlerThread t = new HandlerThread("MtpImageView Fetch");
+ t.start();
+ return new FetchImageHandler(t.getLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ @SuppressWarnings("unchecked")
+ MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+ if (parent == null) return;
+ MtpObjectInfo objectInfo;
+ MtpDevice device;
+ synchronized (parent.mFetchLock) {
+ parent.mFetchPending = false;
+ device = parent.mFetchDevice;
+ objectInfo = parent.mFetchObjectInfo;
+ }
+ if (device == null) return;
+ Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo);
+ if (result == null) return;
+ synchronized (parent.mFetchLock) {
+ if (parent.mFetchObjectInfo != objectInfo) return;
+ parent.mFetchResult = result;
+ parent.mFetchDevice = null;
+ parent.mFetchObjectInfo = null;
+ sFetchCompleteHandler.sendMessage(
+ sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference));
+ }
+ }
+ }
+
+ private static class ShowImageHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ @SuppressWarnings("unchecked")
+ MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+ if (parent == null) return;
+ Object result;
+ synchronized (parent.mFetchLock) {
+ result = parent.mFetchResult;
+ }
+ if (result == null) return;
+ parent.onMtpImageDataFetchedFromDevice(result);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
new file mode 100644
index 000000000..3307e78aa
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
@@ -0,0 +1,106 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
+
+public class MtpThumbnailTileView extends MtpImageView implements Checkable {
+
+ private Paint mForegroundPaint;
+ private boolean mIsChecked;
+ private Bitmap mBitmap;
+
+ private void init() {
+ mForegroundPaint = new Paint();
+ mForegroundPaint.setColor(getResources().getColor(R.color.ingest_highlight_semitransparent));
+ }
+
+ public MtpThumbnailTileView(Context context) {
+ super(context);
+ init();
+ }
+
+ public MtpThumbnailTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public MtpThumbnailTileView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Force this to be square
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+ @Override
+ protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
+ return MtpBitmapFetch.getThumbnail(device, info);
+ }
+
+ @Override
+ protected void onMtpImageDataFetchedFromDevice(Object result) {
+ mBitmap = (Bitmap)result;
+ setImageBitmap(mBitmap);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (isChecked()) {
+ canvas.drawRect(canvas.getClipBounds(), mForegroundPaint);
+ }
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mIsChecked = checked;
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!mIsChecked);
+ }
+
+ @Override
+ protected void cancelLoadingAndClear() {
+ super.cancelLoadingAndClear();
+ if (mBitmap != null) {
+ MtpBitmapFetch.recycleThumbnail(mBitmap);
+ mBitmap = null;
+ }
+ }
+}