summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/ingest/data
diff options
context:
space:
mode:
authorBobby Georgescu <georgescu@google.com>2014-05-14 10:19:19 -0700
committerBobby Georgescu <georgescu@google.com>2014-05-14 18:01:29 +0000
commitf640d379259bb114a50e3200f49961b89d60f2c2 (patch)
tree9a76ad6315f4f77063dda28b7441e2904ca3cd04 /src/com/android/gallery3d/ingest/data
parent29e13812d006579106c147f87c859aec23dfbe11 (diff)
downloadandroid_packages_apps_Gallery2-f640d379259bb114a50e3200f49961b89d60f2c2.tar.gz
android_packages_apps_Gallery2-f640d379259bb114a50e3200f49961b89d60f2c2.tar.bz2
android_packages_apps_Gallery2-f640d379259bb114a50e3200f49961b89d60f2c2.zip
Update ingest importer code
Change-Id: I0f3b0809deead2f49501a5309f0ddab9c911274f
Diffstat (limited to 'src/com/android/gallery3d/ingest/data')
-rw-r--r--src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java18
-rw-r--r--src/com/android/gallery3d/ingest/data/DateBucket.java63
-rw-r--r--src/com/android/gallery3d/ingest/data/ImportTask.java121
-rw-r--r--src/com/android/gallery3d/ingest/data/IngestObjectInfo.java114
-rw-r--r--src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java141
-rw-r--r--src/com/android/gallery3d/ingest/data/MtpClient.java266
-rw-r--r--src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java433
-rw-r--r--src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java186
-rw-r--r--src/com/android/gallery3d/ingest/data/SimpleDate.java127
9 files changed, 1396 insertions, 73 deletions
diff --git a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
index bbc90f670..c436fa7b4 100644
--- a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
+++ b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
@@ -16,14 +16,20 @@
package com.android.gallery3d.ingest.data;
+import android.annotation.TargetApi;
import android.graphics.Bitmap;
+import android.os.Build;
+/**
+ * Encapsulates a Bitmap and some additional metadata.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public class BitmapWithMetadata {
- public Bitmap bitmap;
- public int rotationDegrees;
+ public Bitmap bitmap;
+ public int rotationDegrees;
- public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) {
- this.bitmap = bitmap;
- this.rotationDegrees = rotationDegrees;
- }
+ public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) {
+ this.bitmap = bitmap;
+ this.rotationDegrees = rotationDegrees;
+ }
}
diff --git a/src/com/android/gallery3d/ingest/data/DateBucket.java b/src/com/android/gallery3d/ingest/data/DateBucket.java
new file mode 100644
index 000000000..85eedb3bd
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/DateBucket.java
@@ -0,0 +1,63 @@
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+/**
+ * Date bucket for {@link MtpDeviceIndex}.
+ * See {@link MtpDeviceIndexRunnable} for implementation notes.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+class DateBucket implements Comparable<DateBucket> {
+ final SimpleDate date;
+ final int unifiedStartIndex;
+ final int unifiedEndIndex;
+ final int itemsStartIndex;
+ final int numItems;
+
+ public DateBucket(SimpleDate date, int unifiedStartIndex, int unifiedEndIndex,
+ int itemsStartIndex, int numItems) {
+ this.date = date;
+ this.unifiedStartIndex = unifiedStartIndex;
+ this.unifiedEndIndex = unifiedEndIndex;
+ this.itemsStartIndex = itemsStartIndex;
+ this.numItems = numItems;
+ }
+
+ @Override
+ public String toString() {
+ return date.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return date.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 (date == null) {
+ if (other.date != null) {
+ return false;
+ }
+ } else if (!date.equals(other.date)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(DateBucket another) {
+ return this.date.compareTo(another.date);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/ingest/data/ImportTask.java b/src/com/android/gallery3d/ingest/data/ImportTask.java
new file mode 100644
index 000000000..ee2a7d0e5
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/ImportTask.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.mtp.MtpDevice;
+import android.os.Build;
+import android.os.Environment;
+import android.os.PowerManager;
+import android.os.StatFs;
+import android.util.Log;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Task that handles the copying of items from an MTP device.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class ImportTask implements Runnable {
+
+ private static final String TAG = "ImportTask";
+
+ /**
+ * Import progress listener.
+ */
+ public interface Listener {
+ void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful);
+
+ void onImportFinish(Collection<IngestObjectInfo> objectsNotImported, int visitedCount);
+ }
+
+ private static final String WAKELOCK_LABEL = "Google Photos MTP Import Task";
+
+ private Listener mListener;
+ private String mDestAlbumName;
+ private Collection<IngestObjectInfo> mObjectsToImport;
+ private MtpDevice mDevice;
+ private PowerManager.WakeLock mWakeLock;
+
+ public ImportTask(MtpDevice device, Collection<IngestObjectInfo> 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<IngestObjectInfo> objectsNotImported = new LinkedList<IngestObjectInfo>();
+ int visited = 0;
+ int total = mObjectsToImport.size();
+ mListener.onImportProgress(visited, total, null);
+ File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName);
+ dest.mkdirs();
+ for (IngestObjectInfo object : mObjectsToImport) {
+ visited++;
+ String importedPath = null;
+ if (hasSpaceForSize(object.getCompressedSize())) {
+ importedPath = new File(dest, object.getName(mDevice)).getAbsolutePath();
+ if (!mDevice.importFile(object.getObjectHandle(), importedPath)) {
+ importedPath = null;
+ }
+ }
+ if (importedPath == null) {
+ objectsNotImported.add(object);
+ }
+ if (mListener != null) {
+ mListener.onImportProgress(visited, total, importedPath);
+ }
+ }
+ if (mListener != null) {
+ mListener.onImportFinish(objectsNotImported, visited);
+ }
+ } finally {
+ mListener = null;
+ mWakeLock.release();
+ }
+ }
+
+ private static boolean hasSpaceForSize(long size) {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return false;
+ }
+
+ String path = Environment.getExternalStorageDirectory().getPath();
+ try {
+ StatFs stat = new StatFs(path);
+ return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
+ } catch (Exception e) {
+ Log.i(TAG, "Fail to access external storage", e);
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/IngestObjectInfo.java b/src/com/android/gallery3d/ingest/data/IngestObjectInfo.java
new file mode 100644
index 000000000..25273838b
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/IngestObjectInfo.java
@@ -0,0 +1,114 @@
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Build;
+
+/**
+ * Holds the info needed for the in-memory index of MTP objects.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class IngestObjectInfo implements Comparable<IngestObjectInfo> {
+
+ private int mHandle;
+ private long mDateCreated;
+ private int mFormat;
+ private int mCompressedSize;
+
+ public IngestObjectInfo(MtpObjectInfo mtpObjectInfo) {
+ mHandle = mtpObjectInfo.getObjectHandle();
+ mDateCreated = mtpObjectInfo.getDateCreated();
+ mFormat = mtpObjectInfo.getFormat();
+ mCompressedSize = mtpObjectInfo.getCompressedSize();
+ }
+
+ public IngestObjectInfo(int handle, long dateCreated, int format, int compressedSize) {
+ mHandle = handle;
+ mDateCreated = dateCreated;
+ mFormat = format;
+ mCompressedSize = compressedSize;
+ }
+
+ public int getCompressedSize() {
+ return mCompressedSize;
+ }
+
+ public int getFormat() {
+ return mFormat;
+ }
+
+ public long getDateCreated() {
+ return mDateCreated;
+ }
+
+ public int getObjectHandle() {
+ return mHandle;
+ }
+
+ public String getName(MtpDevice device) {
+ if (device != null) {
+ MtpObjectInfo info = device.getObjectInfo(mHandle);
+ if (info != null) {
+ return info.getName();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int compareTo(IngestObjectInfo another) {
+ long diff = getDateCreated() - another.getDateCreated();
+ if (diff < 0) {
+ return -1;
+ } else if (diff == 0) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "IngestObjectInfo [mHandle=" + mHandle + ", mDateCreated=" + mDateCreated
+ + ", mFormat=" + mFormat + ", mCompressedSize=" + mCompressedSize + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mCompressedSize;
+ result = prime * result + (int) (mDateCreated ^ (mDateCreated >>> 32));
+ result = prime * result + mFormat;
+ result = prime * result + mHandle;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof IngestObjectInfo)) {
+ return false;
+ }
+ IngestObjectInfo other = (IngestObjectInfo) obj;
+ if (mCompressedSize != other.mCompressedSize) {
+ return false;
+ }
+ if (mDateCreated != other.mDateCreated) {
+ return false;
+ }
+ if (mFormat != other.mFormat) {
+ return false;
+ }
+ if (mHandle != other.mHandle) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
index c6504a561..3295828f1 100644
--- a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
+++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
@@ -16,91 +16,98 @@
package com.android.gallery3d.ingest.data;
+import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.mtp.MtpDevice;
-import android.mtp.MtpObjectInfo;
+import android.os.Build;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import com.android.gallery3d.data.Exif;
import com.android.photos.data.GalleryBitmapPool;
+/**
+ * Helper class for fetching bitmaps from MTP devices.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public class MtpBitmapFetch {
- private static int sMaxSize = 0;
+ private static int sMaxSize = 0;
- public static void recycleThumbnail(Bitmap b) {
- if (b != null) {
- GalleryBitmapPool.getInstance().put(b);
- }
+ public static void recycleThumbnail(Bitmap b) {
+ if (b != null) {
+ GalleryBitmapPool.getInstance().put(b);
}
+ }
- public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) {
- byte[] imageBytes = device.getThumbnail(info.getObjectHandle());
- if (imageBytes == null) {
- return null;
- }
- BitmapFactory.Options o = new BitmapFactory.Options();
- o.inJustDecodeBounds = true;
- BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
- if (o.outWidth == 0 || o.outHeight == 0) {
- return null;
- }
- o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight);
- o.inMutable = true;
- o.inJustDecodeBounds = false;
- o.inSampleSize = 1;
- try {
- return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
- } catch (IllegalArgumentException e) {
- // BitmapFactory throws an exception rather than returning null
- // when image decoding fails and an existing bitmap was supplied
- // for recycling, even if the failure was not caused by the use
- // of that bitmap.
- return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
- }
+ public static Bitmap getThumbnail(MtpDevice device, IngestObjectInfo info) {
+ byte[] imageBytes = device.getThumbnail(info.getObjectHandle());
+ if (imageBytes == null) {
+ return null;
}
-
- public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) {
- return getFullsize(device, info, sMaxSize);
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ if (o.outWidth == 0 || o.outHeight == 0) {
+ return null;
}
+ o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight);
+ o.inMutable = true;
+ o.inJustDecodeBounds = false;
+ o.inSampleSize = 1;
+ try {
+ return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ } catch (IllegalArgumentException e) {
+ // BitmapFactory throws an exception rather than returning null
+ // when image decoding fails and an existing bitmap was supplied
+ // for recycling, even if the failure was not caused by the use
+ // of that bitmap.
+ return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ }
+ }
- public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) {
- byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize());
- if (imageBytes == null) {
- return null;
- }
- Bitmap created;
- if (maxSide > 0) {
- BitmapFactory.Options o = new BitmapFactory.Options();
- o.inJustDecodeBounds = true;
- BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
- int w = o.outWidth;
- int h = o.outHeight;
- int comp = Math.max(h, w);
- int sampleSize = 1;
- while ((comp >> 1) >= maxSide) {
- comp = comp >> 1;
- sampleSize++;
- }
- o.inSampleSize = sampleSize;
- o.inJustDecodeBounds = false;
- created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
- } else {
- created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
- }
- if (created == null) {
- return null;
- }
+ public static BitmapWithMetadata getFullsize(MtpDevice device, IngestObjectInfo info) {
+ return getFullsize(device, info, sMaxSize);
+ }
- return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes));
+ public static BitmapWithMetadata getFullsize(MtpDevice device, IngestObjectInfo info,
+ int maxSide) {
+ byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize());
+ if (imageBytes == null) {
+ return null;
}
-
- public static void configureForContext(Context context) {
- DisplayMetrics metrics = new DisplayMetrics();
- WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
- wm.getDefaultDisplay().getMetrics(metrics);
- sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels);
+ Bitmap created;
+ if (maxSide > 0) {
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ int w = o.outWidth;
+ int h = o.outHeight;
+ int comp = Math.max(h, w);
+ int sampleSize = 1;
+ while ((comp >> 1) >= maxSide) {
+ comp = comp >> 1;
+ sampleSize++;
+ }
+ o.inSampleSize = sampleSize;
+ o.inJustDecodeBounds = false;
+ created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ } else {
+ created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
}
+ if (created == null) {
+ return null;
+ }
+
+ int orientation = Exif.getOrientation(imageBytes);
+ return new BitmapWithMetadata(created, orientation);
+ }
+
+ public static void configureForContext(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels);
+ }
}
diff --git a/src/com/android/gallery3d/ingest/data/MtpClient.java b/src/com/android/gallery3d/ingest/data/MtpClient.java
new file mode 100644
index 000000000..cc6c9ce07
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/MtpClient.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.mtp.MtpDevice;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class helps an application manage a list of connected MTP or PTP devices.
+ * It listens for MTP devices being attached and removed from the USB host bus
+ * and notifies the application when the MTP device list changes.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class MtpClient {
+
+ private static final String TAG = "MtpClient";
+
+ private static final String ACTION_USB_PERMISSION =
+ "com.android.gallery3d.ingest.action.USB_PERMISSION";
+
+ private final Context mContext;
+ private final UsbManager mUsbManager;
+ private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
+ // mDevices contains all MtpDevices that have been seen by our client,
+ // so we can inform when the device has been detached.
+ // mDevices is also used for synchronization in this class.
+ private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>();
+ // List of MTP devices we should not try to open for which we are currently
+ // asking for permission to open.
+ private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>();
+ // List of MTP devices we should not try to open.
+ // We add devices to this list if the user canceled a permission request or we were
+ // unable to open the device.
+ private final ArrayList<String> mIgnoredDevices = new ArrayList<String>();
+
+ private final PendingIntent mPermissionIntent;
+
+ private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ UsbDevice usbDevice = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ String deviceName = usbDevice.getDeviceName();
+
+ synchronized (mDevices) {
+ MtpDevice mtpDevice = mDevices.get(deviceName);
+
+ if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
+ if (mtpDevice == null) {
+ mtpDevice = openDeviceLocked(usbDevice);
+ }
+ if (mtpDevice != null) {
+ for (Listener listener : mListeners) {
+ listener.deviceAdded(mtpDevice);
+ }
+ }
+ } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
+ if (mtpDevice != null) {
+ mDevices.remove(deviceName);
+ mRequestPermissionDevices.remove(deviceName);
+ mIgnoredDevices.remove(deviceName);
+ for (Listener listener : mListeners) {
+ listener.deviceRemoved(mtpDevice);
+ }
+ }
+ } else if (ACTION_USB_PERMISSION.equals(action)) {
+ mRequestPermissionDevices.remove(deviceName);
+ boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
+ false);
+ Log.d(TAG, "ACTION_USB_PERMISSION: " + permission);
+ if (permission) {
+ if (mtpDevice == null) {
+ mtpDevice = openDeviceLocked(usbDevice);
+ }
+ if (mtpDevice != null) {
+ for (Listener listener : mListeners) {
+ listener.deviceAdded(mtpDevice);
+ }
+ }
+ } else {
+ // so we don't ask for permission again
+ mIgnoredDevices.add(deviceName);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * An interface for being notified when MTP or PTP devices are attached
+ * or removed. In the current implementation, only PTP devices are supported.
+ */
+ public interface Listener {
+ /**
+ * Called when a new device has been added
+ *
+ * @param device the new device that was added
+ */
+ public void deviceAdded(MtpDevice device);
+
+ /**
+ * Called when a new device has been removed
+ *
+ * @param device the device that was removed
+ */
+ public void deviceRemoved(MtpDevice device);
+ }
+
+ /**
+ * Tests to see if a {@link android.hardware.usb.UsbDevice}
+ * supports the PTP protocol (typically used by digital cameras)
+ *
+ * @param device the device to test
+ * @return true if the device is a PTP device.
+ */
+ public static boolean isCamera(UsbDevice device) {
+ int count = device.getInterfaceCount();
+ for (int i = 0; i < count; i++) {
+ UsbInterface intf = device.getInterface(i);
+ if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
+ intf.getInterfaceSubclass() == 1 &&
+ intf.getInterfaceProtocol() == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * MtpClient constructor
+ *
+ * @param context the {@link android.content.Context} to use for the MtpClient
+ */
+ public MtpClient(Context context) {
+ mContext = context;
+ mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+ mPermissionIntent = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(ACTION_USB_PERMISSION), 0);
+ IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ filter.addAction(ACTION_USB_PERMISSION);
+ context.registerReceiver(mUsbReceiver, filter);
+ }
+
+ /**
+ * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP
+ * device and return an {@link android.mtp.MtpDevice} for it.
+ *
+ * @param usbDevice the device to open
+ * @return an MtpDevice for the device.
+ */
+ private MtpDevice openDeviceLocked(UsbDevice usbDevice) {
+ String deviceName = usbDevice.getDeviceName();
+
+ // don't try to open devices that we have decided to ignore
+ // or are currently asking permission for
+ if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName)
+ && !mRequestPermissionDevices.contains(deviceName)) {
+ if (!mUsbManager.hasPermission(usbDevice)) {
+ mUsbManager.requestPermission(usbDevice, mPermissionIntent);
+ mRequestPermissionDevices.add(deviceName);
+ } else {
+ UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice);
+ if (connection != null) {
+ MtpDevice mtpDevice = new MtpDevice(usbDevice);
+ if (mtpDevice.open(connection)) {
+ mDevices.put(usbDevice.getDeviceName(), mtpDevice);
+ return mtpDevice;
+ } else {
+ // so we don't try to open it again
+ mIgnoredDevices.add(deviceName);
+ }
+ } else {
+ // so we don't try to open it again
+ mIgnoredDevices.add(deviceName);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Closes all resources related to the MtpClient object
+ */
+ public void close() {
+ mContext.unregisterReceiver(mUsbReceiver);
+ }
+
+ /**
+ * Registers a {@link com.android.gallery3d.data.MtpClient.Listener} interface to receive
+ * notifications when MTP or PTP devices are added or removed.
+ *
+ * @param listener the listener to register
+ */
+ public void addListener(Listener listener) {
+ synchronized (mDevices) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Unregisters a {@link com.android.gallery3d.data.MtpClient.Listener} interface.
+ *
+ * @param listener the listener to unregister
+ */
+ public void removeListener(Listener listener) {
+ synchronized (mDevices) {
+ mListeners.remove(listener);
+ }
+ }
+
+
+ /**
+ * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}.
+ *
+ * @return the list of MtpDevices
+ */
+ public List<MtpDevice> getDeviceList() {
+ synchronized (mDevices) {
+ // Query the USB manager since devices might have attached
+ // before we added our listener.
+ for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
+ if (mDevices.get(usbDevice.getDeviceName()) == null) {
+ openDeviceLocked(usbDevice);
+ }
+ }
+
+ return new ArrayList<MtpDevice>(mDevices.values());
+ }
+ }
+
+
+}
diff --git a/src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java b/src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java
new file mode 100644
index 000000000..b21ad8355
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java
@@ -0,0 +1,433 @@
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.mtp.MtpConstants;
+import android.mtp.MtpDevice;
+import android.os.Build;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Index of MTP media objects organized into "buckets," or groupings, based on the date
+ * they were 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 {@link MtpDeviceIndexRunnable} for implementation notes.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class MtpDeviceIndex {
+
+ /**
+ * Indexing progress listener.
+ */
+ public interface ProgressListener {
+ /**
+ * A media item on the device was indexed.
+ * @param object The media item that was just indexed
+ * @param numVisited Number of items visited so far
+ */
+ public void onObjectIndexed(IngestObjectInfo object, int numVisited);
+
+ /**
+ * The metadata loaded from the device is being sorted.
+ */
+ public void onSortingStarted();
+
+ /**
+ * The indexing is done and the index is ready to be used.
+ */
+ public void onIndexingFinished();
+ }
+
+ /**
+ * Media sort orders.
+ */
+ public enum SortOrder {
+ ASCENDING, DESCENDING
+ }
+
+ /** Quicktime MOV container (not already defined in {@link MtpConstants}) **/
+ public static final int FORMAT_MOV = 0x300D;
+
+ public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
+ public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
+
+ static {
+ Set<Integer> supportedImageFormats = new HashSet<Integer>();
+ supportedImageFormats.add(MtpConstants.FORMAT_JFIF);
+ supportedImageFormats.add(MtpConstants.FORMAT_EXIF_JPEG);
+ supportedImageFormats.add(MtpConstants.FORMAT_PNG);
+ supportedImageFormats.add(MtpConstants.FORMAT_GIF);
+ supportedImageFormats.add(MtpConstants.FORMAT_BMP);
+ SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableSet(supportedImageFormats);
+
+ Set<Integer> supportedVideoFormats = new HashSet<Integer>();
+ supportedVideoFormats.add(MtpConstants.FORMAT_3GP_CONTAINER);
+ supportedVideoFormats.add(MtpConstants.FORMAT_AVI);
+ supportedVideoFormats.add(MtpConstants.FORMAT_MP4_CONTAINER);
+ supportedVideoFormats.add(MtpConstants.FORMAT_MPEG);
+ // TODO(georgescu): add FORMAT_MOV once Android Media Scanner supports .mov files
+ SUPPORTED_VIDEO_FORMATS = Collections.unmodifiableSet(supportedVideoFormats);
+ }
+
+ private MtpDevice mDevice;
+ private long mGeneration;
+ private ProgressListener mProgressListener;
+ private volatile MtpDeviceIndexRunnable.Results mResults;
+ private final MtpDeviceIndexRunnable.Factory mIndexRunnableFactory;
+
+ private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(
+ MtpDeviceIndexRunnable.getFactory());
+
+ public static MtpDeviceIndex getInstance() {
+ return sInstance;
+ }
+
+ protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) {
+ mIndexRunnableFactory = indexRunnableFactory;
+ }
+
+ public synchronized MtpDevice getDevice() {
+ return mDevice;
+ }
+
+ public synchronized boolean isDeviceConnected() {
+ return (mDevice != null);
+ }
+
+ /**
+ * @param format Media format from {@link MtpConstants}
+ * @return Whether the format is supported by this index.
+ */
+ public boolean isFormatSupported(int format) {
+ return SUPPORTED_IMAGE_FORMATS.contains(format)
+ || SUPPORTED_VIDEO_FORMATS.contains(format);
+ }
+
+ /**
+ * 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
+ */
+ public synchronized 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)}).
+ *
+ * @return Runnable for the main indexing task
+ */
+ public synchronized Runnable getIndexRunnable() {
+ if (!isDeviceConnected() || mResults != null) {
+ return null;
+ }
+ return mIndexRunnableFactory.createMtpDeviceIndexRunnable(this);
+ }
+
+ /**
+ * @return Whether the index is ready to be used.
+ */
+ public synchronized boolean isIndexReady() {
+ return mResults != null;
+ }
+
+ /**
+ * @param listener
+ * @return Current progress (useful for configuring initial UI state)
+ */
+ public synchronized void setProgressListener(ProgressListener listener) {
+ mProgressListener = listener;
+ }
+
+ /**
+ * Make the listener null if it matches the argument
+ *
+ * @param listener Listener to unset, if currently registered
+ */
+ public synchronized void unsetProgressListener(ProgressListener listener) {
+ if (mProgressListener == listener) {
+ mProgressListener = null;
+ }
+ }
+
+ /**
+ * @return The total number of elements in the index (labels and items)
+ */
+ public int size() {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ return results != null ? results.unifiedLookupIndex.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 IngestObjectInfo at the specified position and
+ * order
+ */
+ public Object get(int position, SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (results == null) {
+ return null;
+ }
+ if (order == SortOrder.ASCENDING) {
+ DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) {
+ return bucket.date;
+ } else {
+ return results.mtpObjects[bucket.itemsStartIndex + position - 1
+ - bucket.unifiedStartIndex];
+ }
+ } else {
+ int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) {
+ return bucket.date;
+ } else {
+ return results.mtpObjects[bucket.itemsStartIndex + zeroIndex
+ - bucket.unifiedStartIndex];
+ }
+ }
+ }
+
+ /**
+ * @param position Index of item to fetch from a view of the data that does not
+ * include labels and is in the specified order
+ * @return position-th item in specified order, when not including labels
+ */
+ public IngestObjectInfo getWithoutLabels(int position, SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (results == null) {
+ return null;
+ }
+ if (order == SortOrder.ASCENDING) {
+ return results.mtpObjects[position];
+ } else {
+ return results.mtpObjects[results.mtpObjects.length - 1 - position];
+ }
+ }
+
+ /**
+ * @param position Index of item to map from a view of the data that does not
+ * include labels and is in the specified order
+ * @param order
+ * @return position in a view of the data that does include labels, or -1 if the index isn't
+ * ready
+ */
+ public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
+ /* Although this is O(log(number of buckets)), and thus should not be used
+ in hotspots, even if the attached device has items for every day for
+ a five-year timeframe, it would still only take 11 iterations at most,
+ so shouldn't be a huge issue. */
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (results == null) {
+ return -1;
+ }
+ if (order == SortOrder.DESCENDING) {
+ position = results.mtpObjects.length - 1 - position;
+ }
+ int bucketNumber = 0;
+ int iMin = 0;
+ int iMax = results.buckets.length - 1;
+ while (iMax >= iMin) {
+ int iMid = (iMax + iMin) / 2;
+ if (results.buckets[iMid].itemsStartIndex + results.buckets[iMid].numItems
+ <= position) {
+ iMin = iMid + 1;
+ } else if (results.buckets[iMid].itemsStartIndex > position) {
+ iMax = iMid - 1;
+ } else {
+ bucketNumber = iMid;
+ break;
+ }
+ }
+ int mappedPos = results.buckets[bucketNumber].unifiedStartIndex + position
+ - results.buckets[bucketNumber].itemsStartIndex + 1;
+ if (order == SortOrder.DESCENDING) {
+ mappedPos = results.unifiedLookupIndex.length - mappedPos;
+ }
+ return mappedPos;
+ }
+
+ /**
+ * @param position Index of item to map from a view of the data that
+ * includes labels and is in the specified order
+ * @param order
+ * @return position in a view of the data that does not include labels, or -1 if the index isn't
+ * ready
+ */
+ public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (results == null) {
+ return -1;
+ }
+ if (order == SortOrder.ASCENDING) {
+ DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) {
+ position++;
+ }
+ return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
+ } else {
+ int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) {
+ zeroIndex--;
+ }
+ return results.mtpObjects.length - 1 - bucket.itemsStartIndex
+ - zeroIndex + bucket.unifiedStartIndex;
+ }
+ }
+
+ /**
+ * @return The number of media items in the index
+ */
+ public int sizeWithoutLabels() {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ return results != null ? results.mtpObjects.length : 0;
+ }
+
+ /**
+ * @param bucketNumber Index of bucket in the specified order
+ * @param order
+ * @return position of bucket's first item in a view of the data that includes labels
+ */
+ public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (order == SortOrder.ASCENDING) {
+ return results.buckets[bucketNumber].unifiedStartIndex;
+ } else {
+ return results.unifiedLookupIndex.length
+ - results.buckets[results.buckets.length - 1 - bucketNumber].unifiedEndIndex
+ - 1;
+ }
+ }
+
+ /**
+ * @param position Index of item in the view of the data that includes labels and is in
+ * the specified order
+ * @param order
+ * @return Index of the bucket that contains the specified item
+ */
+ public int getBucketNumberForPosition(int position, SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (order == SortOrder.ASCENDING) {
+ return results.unifiedLookupIndex[position];
+ } else {
+ return results.buckets.length - 1
+ - results.unifiedLookupIndex[results.unifiedLookupIndex.length - 1
+ - position];
+ }
+ }
+
+ /**
+ * @param position Index of item in the view of the data that includes labels and is in
+ * the specified order
+ * @param order
+ * @return Whether the specified item is the first item in its bucket
+ */
+ public boolean isFirstInBucket(int position, SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (order == SortOrder.ASCENDING) {
+ return results.buckets[results.unifiedLookupIndex[position]].unifiedStartIndex
+ == position;
+ } else {
+ position = results.unifiedLookupIndex.length - 1 - position;
+ return results.buckets[results.unifiedLookupIndex[position]].unifiedEndIndex
+ == position;
+ }
+ }
+
+ /**
+ * @param order
+ * @return Array of buckets in the specified order
+ */
+ public DateBucket[] getBuckets(SortOrder order) {
+ MtpDeviceIndexRunnable.Results results = mResults;
+ if (results == null) {
+ return null;
+ }
+ return (order == SortOrder.ASCENDING) ? results.buckets : results.reversedBuckets;
+ }
+
+ protected void resetState() {
+ mGeneration++;
+ mResults = null;
+ }
+
+ /**
+ * @param device
+ * @param generation
+ * @return whether the index is at the given generation and the given device is connected
+ */
+ protected boolean isAtGeneration(MtpDevice device, long generation) {
+ return (mGeneration == generation) && (mDevice == device);
+ }
+
+ protected synchronized boolean setIndexingResults(MtpDevice device, long generation,
+ MtpDeviceIndexRunnable.Results results) {
+ if (!isAtGeneration(device, generation)) {
+ return false;
+ }
+ mResults = results;
+ onIndexFinish(true /*successful*/);
+ return true;
+ }
+
+ protected synchronized void onIndexFinish(boolean successful) {
+ if (!successful) {
+ resetState();
+ }
+ if (mProgressListener != null) {
+ mProgressListener.onIndexingFinished();
+ }
+ }
+
+ protected synchronized void onSorting() {
+ if (mProgressListener != null) {
+ mProgressListener.onSortingStarted();
+ }
+ }
+
+ protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) {
+ if (mProgressListener != null) {
+ mProgressListener.onObjectIndexed(object, numVisited);
+ }
+ }
+
+ protected long getGeneration() {
+ return mGeneration;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java b/src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java
new file mode 100644
index 000000000..32275898e
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java
@@ -0,0 +1,186 @@
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.mtp.MtpConstants;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Build;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.Stack;
+import java.util.TreeMap;
+
+/**
+ * Runnable used by the {@link MtpDeviceIndex} to populate its index.
+ *
+ * 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.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class MtpDeviceIndexRunnable implements Runnable {
+
+ /**
+ * MtpDeviceIndexRunnable factory.
+ */
+ public static class Factory {
+ public MtpDeviceIndexRunnable createMtpDeviceIndexRunnable(MtpDeviceIndex index) {
+ return new MtpDeviceIndexRunnable(index);
+ }
+ }
+
+ static class Results {
+ final int[] unifiedLookupIndex;
+ final IngestObjectInfo[] mtpObjects;
+ final DateBucket[] buckets;
+ final DateBucket[] reversedBuckets;
+
+ public Results(
+ int[] unifiedLookupIndex, IngestObjectInfo[] mtpObjects, DateBucket[] buckets) {
+ this.unifiedLookupIndex = unifiedLookupIndex;
+ this.mtpObjects = mtpObjects;
+ this.buckets = buckets;
+ this.reversedBuckets = new DateBucket[buckets.length];
+ for (int i = 0; i < buckets.length; i++) {
+ this.reversedBuckets[i] = buckets[buckets.length - 1 - i];
+ }
+ }
+ }
+
+ private final MtpDevice mDevice;
+ protected final MtpDeviceIndex mIndex;
+ private final long mIndexGeneration;
+
+ private static Factory sDefaultFactory = new Factory();
+
+ public static Factory getFactory() {
+ return sDefaultFactory;
+ }
+
+ /**
+ * Exception thrown when a problem occurred during indexing.
+ */
+ @SuppressWarnings("serial")
+ public class IndexingException extends RuntimeException {}
+
+ MtpDeviceIndexRunnable(MtpDeviceIndex index) {
+ mIndex = index;
+ mDevice = index.getDevice();
+ mIndexGeneration = index.getGeneration();
+ }
+
+ @Override
+ public void run() {
+ try {
+ indexDevice();
+ } catch (IndexingException e) {
+ mIndex.onIndexFinish(false /*successful*/);
+ }
+ }
+
+ private void indexDevice() throws IndexingException {
+ SortedMap<SimpleDate, List<IngestObjectInfo>> bucketsTemp =
+ new TreeMap<SimpleDate, List<IngestObjectInfo>>();
+ int numObjects = addAllObjects(bucketsTemp);
+ mIndex.onSorting();
+ int numBuckets = bucketsTemp.size();
+ DateBucket[] buckets = new DateBucket[numBuckets];
+ IngestObjectInfo[] mtpObjects = new IngestObjectInfo[numObjects];
+ int[] unifiedLookupIndex = new int[numObjects + numBuckets];
+ int currentUnifiedIndexEntry = 0;
+ int currentItemsEntry = 0;
+ int nextUnifiedEntry, unifiedStartIndex, numBucketObjects, unifiedEndIndex, itemsStartIndex;
+
+ int i = 0;
+ for (Map.Entry<SimpleDate, List<IngestObjectInfo>> bucketTemp : bucketsTemp.entrySet()) {
+ List<IngestObjectInfo> objects = bucketTemp.getValue();
+ Collections.sort(objects);
+ numBucketObjects = objects.size();
+
+ nextUnifiedEntry = currentUnifiedIndexEntry + numBucketObjects + 1;
+ Arrays.fill(unifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
+ unifiedStartIndex = currentUnifiedIndexEntry;
+ unifiedEndIndex = nextUnifiedEntry - 1;
+ currentUnifiedIndexEntry = nextUnifiedEntry;
+
+ itemsStartIndex = currentItemsEntry;
+ for (int j = 0; j < numBucketObjects; j++) {
+ mtpObjects[currentItemsEntry] = objects.get(j);
+ currentItemsEntry++;
+ }
+ buckets[i] = new DateBucket(bucketTemp.getKey(), unifiedStartIndex, unifiedEndIndex,
+ itemsStartIndex, numBucketObjects);
+ i++;
+ }
+ if (!mIndex.setIndexingResults(mDevice, mIndexGeneration,
+ new Results(unifiedLookupIndex, mtpObjects, buckets))) {
+ throw new IndexingException();
+ }
+ }
+
+ private SimpleDate mDateInstance = new SimpleDate();
+
+ protected void addObject(IngestObjectInfo objectInfo,
+ SortedMap<SimpleDate, List<IngestObjectInfo>> bucketsTemp, int numObjects) {
+ mDateInstance.setTimestamp(objectInfo.getDateCreated());
+ List<IngestObjectInfo> bucket = bucketsTemp.get(mDateInstance);
+ if (bucket == null) {
+ bucket = new ArrayList<IngestObjectInfo>();
+ bucketsTemp.put(mDateInstance, bucket);
+ mDateInstance = new SimpleDate(); // only create new date objects when they are used
+ }
+ bucket.add(objectInfo);
+ mIndex.onObjectIndexed(objectInfo, numObjects);
+ }
+
+ protected int addAllObjects(SortedMap<SimpleDate, List<IngestObjectInfo>> bucketsTemp)
+ throws IndexingException {
+ int numObjects = 0;
+ for (int storageId : mDevice.getStorageIds()) {
+ if (!mIndex.isAtGeneration(mDevice, mIndexGeneration)) {
+ throw new IndexingException();
+ }
+ Stack<Integer> pendingDirectories = new Stack<Integer>();
+ pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
+ while (!pendingDirectories.isEmpty()) {
+ if (!mIndex.isAtGeneration(mDevice, mIndexGeneration)) {
+ throw new IndexingException();
+ }
+ int dirHandle = pendingDirectories.pop();
+ for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
+ MtpObjectInfo mtpObjectInfo = mDevice.getObjectInfo(objectHandle);
+ if (mtpObjectInfo == null) {
+ throw new IndexingException();
+ }
+ int format = mtpObjectInfo.getFormat();
+ if (format == MtpConstants.FORMAT_ASSOCIATION) {
+ pendingDirectories.add(objectHandle);
+ } else if (mIndex.isFormatSupported(format)) {
+ numObjects++;
+ addObject(new IngestObjectInfo(mtpObjectInfo), bucketsTemp, numObjects);
+ }
+ }
+ }
+ }
+ return numObjects;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/SimpleDate.java b/src/com/android/gallery3d/ingest/data/SimpleDate.java
new file mode 100644
index 000000000..2476f80ed
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/SimpleDate.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ingest.data;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+
+/**
+ * Represents a date (year, month, day)
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+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(georgescu): 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;
+ }
+}