summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/ingest
diff options
context:
space:
mode:
authorBobby Georgescu <georgescu@google.com>2012-12-26 00:52:28 -0800
committerBobby Georgescu <georgescu@google.com>2013-01-07 14:43:56 -0800
commita4205e4eab691b402e7a6dc1e913a611b7fdc45f (patch)
treebaa01cd5497b0616a4cf324feca8e3700b6460c8 /src/com/android/gallery3d/ingest
parentbf7af1173998fe8ac9e0eb7059d2b64e75a1c417 (diff)
downloadandroid_packages_apps_Snap-a4205e4eab691b402e7a6dc1e913a611b7fdc45f.tar.gz
android_packages_apps_Snap-a4205e4eab691b402e7a6dc1e913a611b7fdc45f.tar.bz2
android_packages_apps_Snap-a4205e4eab691b402e7a6dc1e913a611b7fdc45f.zip
Replace camera importer
Change-Id: Icfeb23301bc6c9f8c8b12824bcbb0d8b41852d07
Diffstat (limited to 'src/com/android/gallery3d/ingest')
-rw-r--r--src/com/android/gallery3d/ingest/ImportTask.java94
-rw-r--r--src/com/android/gallery3d/ingest/IngestActivity.java344
-rw-r--r--src/com/android/gallery3d/ingest/IngestService.java287
-rw-r--r--src/com/android/gallery3d/ingest/MtpDeviceIndex.java501
-rw-r--r--src/com/android/gallery3d/ingest/SimpleDate.java114
-rw-r--r--src/com/android/gallery3d/ingest/adapter/MtpAdapter.java159
-rw-r--r--src/com/android/gallery3d/ingest/ui/DateTileView.java83
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpBitmapCache.java64
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java144
9 files changed, 1790 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..d82ccd61f
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ImportTask.java
@@ -0,0 +1,94 @@
+/*
+ * 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);
+ }
+
+ 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();
+ 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);
+ }
+ } 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..a21ccc2fe
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestActivity.java
@@ -0,0 +1,344 @@
+/*
+ * 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.mtp.MtpObjectInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+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.GridView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.MtpAdapter;
+
+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 GridView mGridView;
+ private MtpAdapter mAdapter;
+ private Handler mHandler;
+ private ProgressDialog mProgressDialog;
+ private ActionMode mActiveActionMode;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ doBindHelperService();
+
+ setContentView(R.layout.ingest_activity_item_list);
+ mGridView = (GridView) findViewById(R.id.ingest_gridview);
+ mAdapter = new MtpAdapter(this);
+ mGridView.setAdapter(mAdapter);
+ mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener);
+ mGridView.setOnItemClickListener(mOnItemClickListener);
+
+ mHandler = new ItemListHandler(this);
+ }
+
+ private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View itemView, int position, long arg3) {
+ 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);
+ }
+
+ mIgnoreItemCheckedStateChanges = false;
+ }
+ updateSelectedTitle(mode);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.import_items:
+ mHelperService.importSelectedItems(
+ mGridView.getCheckedItemPositions(),
+ mAdapter);
+ mode.finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @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;
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActiveActionMode = null;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ updateSelectedTitle(mode);
+ return false;
+ }
+ };
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ doUnbindHelperService();
+ }
+
+ @Override
+ protected void onResume() {
+ mActive = true;
+ if (mHelperService != null) mHelperService.setClientActivity(this);
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ if (mHelperService != null) mHelperService.setClientActivity(null);
+ mActive = false;
+ cleanupProgressDialog();
+ super.onPause();
+ }
+
+ protected void notifyIndexChanged() {
+ mAdapter.notifyDataSetChanged();
+ if (mActiveActionMode != null) {
+ mActiveActionMode.finish();
+ mActiveActionMode = null;
+ }
+ }
+
+ 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_ADAPTER_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);
+ }
+
+ @Override
+ public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported) {
+ // Not guaranteed to be called on the UI thread
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+ // 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 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_ADAPTER_NOTIFY_CHANGED = 2;
+
+ 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_ADAPTER_NOTIFY_CHANGED:
+ parent.notifyIndexChanged();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mHelperService = ((IngestService.LocalBinder) service).getService();
+ mHelperService.setClientActivity(IngestActivity.this);
+ mAdapter.setMtpDeviceIndex(mHelperService.getIndex());
+ }
+
+ 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..2ca4d4ca2
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestService.java
@@ -0,0 +1,287 @@
+/*
+ * 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.ingest.ui.MtpBitmapCache;
+import com.android.gallery3d.util.BucketNames;
+
+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 Collection<MtpObjectInfo> mRedeliverObjectsNotImported;
+ private boolean mRedeliverNotifyIndexChanged = false;
+ private NotificationManager mNotificationManager;
+ private NotificationCompat.Builder mNotificationBuilder;
+ private long mLastProgressIndexTime = 0;
+
+ @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;
+ mDevice = device;
+ mIndex.setDevice(mDevice);
+ if (mDevice != null) {
+ MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
+ 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) return;
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_IMPORTING);
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+ if (mRedeliverImportFinish) {
+ mClientActivity.onImportFinish(mRedeliverObjectsNotImported);
+ mRedeliverImportFinish = false;
+ mRedeliverObjectsNotImported = null;
+ }
+ if (mRedeliverNotifyIndexChanged) {
+ mClientActivity.notifyIndexChanged();
+ mRedeliverNotifyIndexChanged = 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);
+ }
+ }
+
+ @Override
+ public void deviceRemoved(MtpDevice device) {
+ if (device == mDevice) {
+ setDevice(null);
+ }
+ MtpBitmapCache.onDeviceDisconnected(device);
+ }
+
+ @Override
+ public void onImportProgress(int visitedCount, int totalCount,
+ String pathIfSuccessful) {
+ if (pathIfSuccessful != null) {
+ mScannerClient.scanPath(pathIfSuccessful);
+ }
+ if (mClientActivity != null) {
+ mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
+ } else {
+ 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) {
+ if (mClientActivity != null) {
+ mClientActivity.onImportFinish(objectsNotImported);
+ } else {
+ mRedeliverImportFinish = true;
+ mRedeliverObjectsNotImported = objectsNotImported;
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.import_complete));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ }
+ stopForeground(mClientActivity != null);
+ }
+
+ @Override
+ public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
+ 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() {
+ 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());
+ }
+ }
+
+ // 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..10ccb82ac
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
@@ -0,0 +1,501 @@
+/*
+ * 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.List;
+import java.util.Map;
+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 {
+
+ @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 Map<SimpleDate, DateBucket> mBucketsTemp;
+ private int mGeneration = 0;
+
+ public enum Progress {
+ Uninitialized, Initialized, Pending, Started, Sorting, Finished
+ }
+
+ private Progress mProgress = Progress.Uninitialized;
+ private int mNumObjects = 0;
+ 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 Runnable() {
+ @Override
+ public void run() {
+ indexDevice();
+ }
+ };
+ }
+
+ 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(order == SortOrder.Ascending) {
+ return getAscending(position);
+ } else {
+ return getDescending(position);
+ }
+ }
+
+ /**
+ * @param position Index of item to fetch, where 0 is the first item in
+ * ascending order
+ * @return position-th item in ascending order
+ */
+ public Object getAscending(int position) {
+ if (mProgress != Progress.Finished) return null;
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) {
+ return bucket.bucket;
+ } else {
+ return bucket.get(position - 1 - bucket.unifiedStartIndex);
+ }
+ }
+
+ /**
+ * @param position Index of item to fetch, where 0 is the last item in
+ * ascending order
+ * @return position-th item in descending order
+ */
+ public Object getDescending(int position) {
+ if (mProgress != Progress.Finished) return null;
+ int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) {
+ return bucket.bucket;
+ } else {
+ return bucket.get(zeroIndex - bucket.unifiedStartIndex);
+ }
+ }
+
+ /**
+ * @param position Index of item to fetch from a view of the data that doesn't
+ * include labels and is in ascending order
+ * @return position-th item in ascending 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];
+ }
+ }
+
+ /**
+ * @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;
+
+ 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);
+ }
+
+ public MtpObjectInfo get(int position) {
+ return mMtpObjects[itemsStartIndex + position];
+ }
+
+ @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;
+ mBucketsTemp = null;
+ mNumObjects = 0;
+ mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
+ }
+
+ /*
+ * 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;
+ for (int j = 0; j < bucket.tempElementsList.size(); j++) {
+ mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
+ currentItemsEntry++;
+ }
+ bucket.tempElementsList = null;
+ }
+ }
+
+ private void indexDevice() {
+ synchronized (this) {
+ mProgress = Progress.Started;
+ }
+ mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
+ for (int storageId : mDevice.getStorageIds()) {
+ Stack<Integer> pendingDirectories = new Stack<Integer>();
+ pendingDirectories.add(0xFFFFFFFF); // start at the root of the
+ // device
+ while (!pendingDirectories.isEmpty()) {
+ int dirHandle = pendingDirectories.pop();
+ for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
+ MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
+ switch (objectInfo.getFormat()) {
+ case MtpConstants.FORMAT_JFIF:
+ case MtpConstants.FORMAT_EXIF_JPEG:
+ addObject(objectInfo);
+ break;
+ case MtpConstants.FORMAT_ASSOCIATION:
+ pendingDirectories.add(objectHandle);
+ break;
+ }
+ }
+ }
+ }
+ Collection<DateBucket> values = mBucketsTemp.values();
+ mBucketsTemp = null;
+ mBuckets = values.toArray(new DateBucket[values.size()]);
+ values = null;
+ synchronized (this) {
+ mProgress = Progress.Sorting;
+ if (mProgressListener != null) {
+ mProgressListener.onSorting();
+ }
+ }
+ sortAll();
+ buildLookupIndex();
+ synchronized (this) {
+ 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);
+ }
+
+ /*
+ * 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();
+ }
+
+ 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/MtpAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
new file mode 100644
index 000000000..e09fcd962
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
@@ -0,0 +1,159 @@
+/*
+ * 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;
+
+ public MtpAdapter(Activity context) {
+ super();
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService
+ (Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ public void setMtpDeviceIndex(MtpDeviceIndex index) {
+ mModel = index;
+ notifyDataSetChanged();
+ }
+
+ @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));
+ 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;
+ }
+}
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..19b3c608d
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/DateTileView.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+public class DateTileView extends FrameLayout {
+ private static final String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths();
+
+ private TextView mDateTextView;
+ private TextView mMonthTextView;
+ private TextView mYearTextView;
+ private int mMonth = -1;
+ private int mYear = -1;
+ private int mDate = -1;
+
+ 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 (month != mMonth) {
+ mMonth = month;
+ mMonthTextView.setText(sMonthNames[mMonth]);
+ }
+ if (year != mYear) {
+ mYear = year;
+ mYearTextView.setText(Integer.toString(mYear));
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpBitmapCache.java b/src/com/android/gallery3d/ingest/ui/MtpBitmapCache.java
new file mode 100644
index 000000000..66ac4301f
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpBitmapCache.java
@@ -0,0 +1,64 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.mtp.MtpDevice;
+import android.util.LruCache;
+
+public class MtpBitmapCache extends LruCache<Integer, Bitmap> {
+ private static final int PER_DEVICE_CACHE_MAX_BYTES = 4194304;
+ private static MtpBitmapCache sInstance;
+
+ public synchronized static MtpBitmapCache getInstanceForDevice(MtpDevice device) {
+ if (sInstance == null || sInstance.mDevice != device) {
+ sInstance = new MtpBitmapCache(PER_DEVICE_CACHE_MAX_BYTES, device);
+ }
+ return sInstance;
+ }
+
+ public synchronized static void onDeviceDisconnected(MtpDevice device) {
+ if (sInstance != null && sInstance.mDevice == device) {
+ sInstance = null;
+ }
+ }
+
+ private MtpDevice mDevice;
+
+ private MtpBitmapCache(int maxSize, MtpDevice device) {
+ super(maxSize);
+ mDevice = device;
+ }
+
+ @Override
+ protected int sizeOf(Integer key, Bitmap value) {
+ return value.getByteCount();
+ }
+
+ public Bitmap getOrCreate(Integer key) {
+ Bitmap b = get(key);
+ return b == null ? createAndInsert(key) : b;
+ }
+
+ private Bitmap createAndInsert(Integer key) {
+ byte[] imageBytes = mDevice.getThumbnail(key);
+ Bitmap created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ put(key, created);
+ return created;
+ }
+}
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..ddba6af91
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
@@ -0,0 +1,144 @@
+/*
+ * 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.os.AsyncTask;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+
+public class MtpThumbnailTileView extends ImageView implements Checkable {
+ private static final int FADE_IN_TIME_MS = 80;
+
+ private Paint mForegroundPaint;
+ private boolean mIsChecked;
+
+ private void init() {
+ mForegroundPaint = new Paint();
+ mForegroundPaint.setColor(getResources().getColor(R.color.ingest_highlight_semitransparent));
+ showPlaceholder();
+ }
+
+ 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
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mIsChecked) {
+ 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);
+ }
+
+ private void showPlaceholder() {
+ setAlpha(0f);
+ }
+
+ private LoadThumbnailTask mTask;
+
+ public void setMtpDeviceAndObjectInfo(MtpDevice device, MtpObjectInfo object) {
+ animate().cancel();
+ if (mTask != null) {
+ mTask.cancel(true);
+ }
+ Bitmap thumbnail = MtpBitmapCache.getInstanceForDevice(device)
+ .get(object.getObjectHandle());
+ if (thumbnail != null) {
+ setAlpha(1f);
+ setImageBitmap(thumbnail);
+ } else {
+ showPlaceholder();
+ mTask = new LoadThumbnailTask(device);
+ mTask.execute(object);
+ }
+ }
+
+ private class LoadThumbnailTask extends AsyncTask<MtpObjectInfo, Void, Bitmap> {
+ private MtpDevice mDevice;
+
+ public LoadThumbnailTask(MtpDevice device) {
+ mDevice = device;
+ }
+
+ @Override
+ protected Bitmap doInBackground(MtpObjectInfo... args) {
+ Bitmap result = null;
+ if (!isCancelled()) {
+ result = MtpBitmapCache.getInstanceForDevice(mDevice).getOrCreate(
+ args[0].getObjectHandle());
+ }
+ mDevice = null;
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ if (isCancelled() || result == null) {
+ return;
+ }
+ setAlpha(0f);
+ setImageBitmap(result);
+ animate().alpha(1f).setDuration(FADE_IN_TIME_MS);
+ }
+
+ @Override
+ protected void onCancelled() {
+ }
+ }
+}