diff options
author | Bobby Georgescu <georgescu@google.com> | 2012-12-26 00:52:28 -0800 |
---|---|---|
committer | Bobby Georgescu <georgescu@google.com> | 2013-01-07 14:43:56 -0800 |
commit | d3aac52ffb88ced53413d5eef29c641dd6982267 (patch) | |
tree | b5e614ec3f5cc1f29ee6b501c7cfa8ac0bfb0fce /src/com/android/gallery3d/ingest | |
parent | 3114fa3a8a99be3c819d102dd1d069face13c43f (diff) | |
download | android_packages_apps_Gallery2-d3aac52ffb88ced53413d5eef29c641dd6982267.tar.gz android_packages_apps_Gallery2-d3aac52ffb88ced53413d5eef29c641dd6982267.tar.bz2 android_packages_apps_Gallery2-d3aac52ffb88ced53413d5eef29c641dd6982267.zip |
Replace camera importer
Change-Id: Icfeb23301bc6c9f8c8b12824bcbb0d8b41852d07
Diffstat (limited to 'src/com/android/gallery3d/ingest')
-rw-r--r-- | src/com/android/gallery3d/ingest/ImportTask.java | 94 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/IngestActivity.java | 344 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/IngestService.java | 287 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/MtpDeviceIndex.java | 501 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/SimpleDate.java | 114 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/adapter/MtpAdapter.java | 159 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/ui/DateTileView.java | 83 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/ui/MtpBitmapCache.java | 64 | ||||
-rw-r--r-- | src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java | 144 |
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() { + } + } +} |