OSDN Git Service

Update ingest importer code
authorBobby Georgescu <georgescu@google.com>
Wed, 14 May 2014 17:19:19 +0000 (10:19 -0700)
committerBobby Georgescu <georgescu@google.com>
Wed, 14 May 2014 18:01:29 +0000 (18:01 +0000)
Change-Id: I0f3b0809deead2f49501a5309f0ddab9c911274f

29 files changed:
res/layout/ingest_activity_item_list.xml
res/layout/ingest_date_tile.xml
res/layout/ingest_fullsize.xml
res/layout/ingest_thumbnail.xml
res/menu/ingest_menu_item_list_selection.xml
res/values/ids.xml
res/values/strings.xml
src/com/android/gallery3d/ingest/ImportTask.java [deleted file]
src/com/android/gallery3d/ingest/IngestActivity.java
src/com/android/gallery3d/ingest/IngestService.java
src/com/android/gallery3d/ingest/MtpDeviceIndex.java [deleted file]
src/com/android/gallery3d/ingest/SimpleDate.java [deleted file]
src/com/android/gallery3d/ingest/adapter/CheckBroker.java
src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
src/com/android/gallery3d/ingest/data/DateBucket.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/data/ImportTask.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/data/IngestObjectInfo.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
src/com/android/gallery3d/ingest/data/MtpClient.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/data/SimpleDate.java [new file with mode: 0644]
src/com/android/gallery3d/ingest/ui/DateTileView.java
src/com/android/gallery3d/ingest/ui/IngestGridView.java
src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
src/com/android/gallery3d/ingest/ui/MtpImageView.java
src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java

index f0e91e8..b91f0ce 100644 (file)
@@ -52,6 +52,7 @@
             android:id="@+id/ingest_warning_view_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_marginStart="10dip"
             android:layout_marginLeft="10dip"
             android:textAppearance="?android:attr/textAppearanceSmall" />
     </LinearLayout>
index 6b5e934..8cd63e9 100644 (file)
@@ -27,7 +27,7 @@
             android:layout_height="wrap_content"
             android:layout_column="0"
             android:layout_row="0"
-            android:layout_gravity="bottom|right"
+            android:layout_gravity="bottom|end"
             android:layout_marginTop="7sp"
             android:includeFontPadding="false"
             android:textSize="16sp"
@@ -40,7 +40,7 @@
             android:layout_height="wrap_content"
             android:layout_column="0"
             android:layout_row="1"
-            android:layout_gravity="top|right"
+            android:layout_gravity="top|end"
             android:includeFontPadding="false"
             android:textSize="13sp"
             android:fontFamily="sans-serif-light"
@@ -52,7 +52,8 @@
             android:layout_column="1"
             android:layout_row="0"
             android:layout_rowSpan="2"
-            android:layout_gravity="top|left"
+            android:layout_gravity="top|start"
+            android:layout_marginStart="5sp"
             android:layout_marginLeft="5sp"
             android:includeFontPadding="false"
             android:textSize="44sp"
index fad596c..d57c4ae 100644 (file)
@@ -37,7 +37,8 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
         android:layout_alignParentRight="true"
-        android:text="@string/Import" />
+        android:text="@string/ingest_import" />
 
 </com.android.gallery3d.ingest.ui.MtpFullscreenView>
\ No newline at end of file
index 6907149..b36ec93 100644 (file)
@@ -17,6 +17,4 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:scaleType="centerCrop"
-    android:background="@drawable/ingest_item_list_selector">
-</com.android.gallery3d.ingest.ui.MtpThumbnailTileView>
\ No newline at end of file
+    android:scaleType="centerCrop" />
index 2f020b6..b3fa111 100644 (file)
@@ -16,7 +16,7 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:id="@+id/ingest_switch_view"
           android:showAsAction="always" />
-    <item android:id="@+id/import_items"
+    <item android:id="@+id/ingest_import_items"
           android:showAsAction="always|withText"
-          android:title="@string/Import" />
+          android:title="@string/ingest_import" />
 </menu>
\ No newline at end of file
index fefd5f0..f5b4a48 100644 (file)
@@ -20,4 +20,7 @@
     <item type="id" name="action_toggle_full_caching" />
     <item type="id" name="action_select_all" />
     <item type="id" name="viewpager" />
+
+    <item type="id" name="ingest_notification_scanning" />
+    <item type="id" name="ingest_notification_importing" />
 </resources>
index 1ef8c13..570014e 100644 (file)
     <!-- The label on the button that will save an edited image -->
     <string name="save" msgid="8140440041190264400">Save</string>
 
+    <!-- A label representing the action of importing media item(s) [CHAR LIMIT=20] -->
+    <string name="ingest_import">@string/Import</string>
+
+    <!-- A label that indicates the media import operation completed [CHAR LIMIT=20] -->
+    <string name="ingest_import_complete">@string/import_complete</string>
+
     <!--  Text of notification message which is shown when user attaches camera -->
     <string name="ingest_scanning" msgid="2048262851775139720">Scanning content...</string>
 
         <item quantity="other">%1$d items scanned</item>
     </plurals>
 
+    <!-- String indicating how many media items from the camera have been selected -->
+    <plurals name="ingest_number_of_items_selected">
+        <item quantity="zero">%1$d items selected</item>
+        <item quantity="one">%1$d item selected</item>
+        <item quantity="other">%1$d items selected</item>
+    </plurals>
+
     <!--  Status message shown when content from the camera is being sorted -->
     <string name="ingest_sorting" msgid="624687230903648118">Sorting...</string>
 
     <!--  Status message shown when there is no MTP device connected  -->
     <string name="ingest_no_device">There is no MTP device connected</string>
 
+    <!-- Label for album grid button -->
+    <string name="ingest_switch_photo_grid">@string/switch_photo_grid</string>
+
+    <!-- Label for fullscreen button. [CHAR LIMIT=20] -->
+    <string name="ingest_switch_photo_fullscreen">@string/switch_photo_fullscreen</string>
+
     <!-- Camera resources below -->
 
     <!-- General strings -->
diff --git a/src/com/android/gallery3d/ingest/ImportTask.java b/src/com/android/gallery3d/ingest/ImportTask.java
deleted file mode 100644 (file)
index 7d2d641..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ingest;
-
-import android.content.Context;
-import android.mtp.MtpDevice;
-import android.mtp.MtpObjectInfo;
-import android.os.Environment;
-import android.os.PowerManager;
-
-import com.android.gallery3d.util.GalleryUtils;
-
-import java.io.File;
-import java.util.Collection;
-import java.util.LinkedList;
-import java.util.List;
-
-public class ImportTask implements Runnable {
-
-    public interface Listener {
-        void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful);
-
-        void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, int visitedCount);
-    }
-
-    static private final String WAKELOCK_LABEL = "MTP Import Task";
-
-    private Listener mListener;
-    private String mDestAlbumName;
-    private Collection<MtpObjectInfo> mObjectsToImport;
-    private MtpDevice mDevice;
-    private PowerManager.WakeLock mWakeLock;
-
-    public ImportTask(MtpDevice device, Collection<MtpObjectInfo> objectsToImport,
-            String destAlbumName, Context context) {
-        mDestAlbumName = destAlbumName;
-        mObjectsToImport = objectsToImport;
-        mDevice = device;
-        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, WAKELOCK_LABEL);
-    }
-
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
-    @Override
-    public void run() {
-        mWakeLock.acquire();
-        try {
-            List<MtpObjectInfo> objectsNotImported = new LinkedList<MtpObjectInfo>();
-            int visited = 0;
-            int total = mObjectsToImport.size();
-            mListener.onImportProgress(visited, total, null);
-            File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName);
-            dest.mkdirs();
-            for (MtpObjectInfo object : mObjectsToImport) {
-                visited++;
-                String importedPath = null;
-                if (GalleryUtils.hasSpaceForSize(object.getCompressedSize())) {
-                    importedPath = new File(dest, object.getName()).getAbsolutePath();
-                    if (!mDevice.importFile(object.getObjectHandle(), importedPath)) {
-                        importedPath = null;
-                    }
-                }
-                if (importedPath == null) {
-                    objectsNotImported.add(object);
-                }
-                if (mListener != null) {
-                    mListener.onImportProgress(visited, total, importedPath);
-                }
-            }
-            if (mListener != null) {
-                mListener.onImportFinish(objectsNotImported, visited);
-            }
-        } finally {
-            mListener = null;
-            mWakeLock.release();
-        }
-    }
-}
index 687e9fd..46173d6 100644 (file)
 
 package com.android.gallery3d.ingest;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+import com.android.gallery3d.ingest.adapter.MtpAdapter;
+import com.android.gallery3d.ingest.adapter.MtpPagerAdapter;
+import com.android.gallery3d.ingest.data.ImportTask;
+import com.android.gallery3d.ingest.data.IngestObjectInfo;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.IngestGridView;
+import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
+
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.ProgressDialog;
 import android.content.ComponentName;
@@ -24,7 +37,7 @@ import android.content.Intent;
 import android.content.ServiceConnection;
 import android.content.res.Configuration;
 import android.database.DataSetObserver;
-import android.mtp.MtpObjectInfo;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -41,530 +54,559 @@ import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.TextView;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.adapter.CheckBroker;
-import com.android.gallery3d.ingest.adapter.MtpAdapter;
-import com.android.gallery3d.ingest.adapter.MtpPagerAdapter;
-import com.android.gallery3d.ingest.data.MtpBitmapFetch;
-import com.android.gallery3d.ingest.ui.DateTileView;
-import com.android.gallery3d.ingest.ui.IngestGridView;
-import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
-
 import java.lang.ref.WeakReference;
 import java.util.Collection;
 
+/**
+ * MTP importer, main activity.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
 public class IngestActivity extends Activity implements
-        MtpDeviceIndex.ProgressListener, ImportTask.Listener {
-
-    private IngestService mHelperService;
-    private boolean mActive = false;
-    private IngestGridView mGridView;
-    private MtpAdapter mAdapter;
-    private Handler mHandler;
-    private ProgressDialog mProgressDialog;
-    private ActionMode mActiveActionMode;
-
-    private View mWarningView;
-    private TextView mWarningText;
-    private int mLastCheckedPosition = 0;
-
-    private ViewPager mFullscreenPager;
-    private MtpPagerAdapter mPagerAdapter;
-    private boolean mFullscreenPagerVisible = false;
-
-    private MenuItem mMenuSwitcherItem;
-    private MenuItem mActionMenuSwitcherItem;
-
-    // The MTP framework components don't give us fine-grained file copy
-    // progress updates, so for large photos and videos, we will be stuck
-    // with a dialog not updating for a long time. To give the user feedback,
-    // we switch to the animated indeterminate progress bar after the timeout
-    // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from
-    // the framework, we switch back to the normal progress bar.
-    private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000;
-
+    MtpDeviceIndex.ProgressListener, ImportTask.Listener {
+
+  private IngestService mHelperService;
+  private boolean mActive = false;
+  private IngestGridView mGridView;
+  private MtpAdapter mAdapter;
+  private Handler mHandler;
+  private ProgressDialog mProgressDialog;
+  private ActionMode mActiveActionMode;
+
+  private View mWarningView;
+  private TextView mWarningText;
+  private int mLastCheckedPosition = 0;
+
+  private ViewPager mFullscreenPager;
+  private MtpPagerAdapter mPagerAdapter;
+  private boolean mFullscreenPagerVisible = false;
+
+  private MenuItem mMenuSwitcherItem;
+  private MenuItem mActionMenuSwitcherItem;
+
+  // The MTP framework components don't give us fine-grained file copy
+  // progress updates, so for large photos and videos, we will be stuck
+  // with a dialog not updating for a long time. To give the user feedback,
+  // we switch to the animated indeterminate progress bar after the timeout
+  // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from
+  // the framework, we switch back to the normal progress bar.
+  private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    doBindHelperService();
+
+    setContentView(R.layout.ingest_activity_item_list);
+    mGridView = (IngestGridView) findViewById(R.id.ingest_gridview);
+    mAdapter = new MtpAdapter(this);
+    mAdapter.registerDataSetObserver(mMasterObserver);
+    mGridView.setAdapter(mAdapter);
+    mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener);
+    mGridView.setOnItemClickListener(mOnItemClickListener);
+    mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker);
+
+    mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager);
+
+    mHandler = new ItemListHandler(this);
+
+    MtpBitmapFetch.configureForContext(this);
+  }
+
+  private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        doBindHelperService();
-
-        setContentView(R.layout.ingest_activity_item_list);
-        mGridView = (IngestGridView) findViewById(R.id.ingest_gridview);
-        mAdapter = new MtpAdapter(this);
-        mAdapter.registerDataSetObserver(mMasterObserver);
-        mGridView.setAdapter(mAdapter);
-        mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener);
-        mGridView.setOnItemClickListener(mOnItemClickListener);
-        mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker);
-
-        mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager);
-
-        mHandler = new ItemListHandler(this);
-
-        MtpBitmapFetch.configureForContext(this);
+    public void onItemClick(AdapterView<?> adapterView, View itemView, int position,
+        long arg3) {
+      mLastCheckedPosition = position;
+      mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position));
     }
+  };
 
-    private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
-        @Override
-        public void onItemClick(AdapterView<?> adapterView, View itemView, int position, long arg3) {
-            mLastCheckedPosition = position;
-            mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position));
-        }
-    };
-
-    private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() {
-        private boolean mIgnoreItemCheckedStateChanges = false;
+  private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() {
+    private boolean mIgnoreItemCheckedStateChanges = false;
 
-        private void updateSelectedTitle(ActionMode mode) {
-            int count = mGridView.getCheckedItemCount();
-            mode.setTitle(getResources().getQuantityString(
-                    R.plurals.number_of_items_selected, count, count));
-        }
-
-        @Override
-        public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
-                boolean checked) {
-            if (mIgnoreItemCheckedStateChanges) return;
-            if (mAdapter.itemAtPositionIsBucket(position)) {
-                SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions();
-                mIgnoreItemCheckedStateChanges = true;
-                mGridView.setItemChecked(position, false);
-
-                // Takes advantage of the fact that SectionIndexer imposes the
-                // need to clamp to the valid range
-                int nextSectionStart = mAdapter.getPositionForSection(
-                        mAdapter.getSectionForPosition(position) + 1);
-                if (nextSectionStart == position)
-                    nextSectionStart = mAdapter.getCount();
-
-                boolean rangeValue = false; // Value we want to set all of the bucket items to
-
-                // Determine if all the items in the bucket are currently checked, so that we
-                // can uncheck them, otherwise we will check all items in the bucket.
-                for (int i = position + 1; i < nextSectionStart; i++) {
-                    if (checkedItems.get(i) == false) {
-                        rangeValue = true;
-                        break;
-                    }
-                }
-
-                // Set all items in the bucket to the desired state
-                for (int i = position + 1; i < nextSectionStart; i++) {
-                    if (checkedItems.get(i) != rangeValue)
-                        mGridView.setItemChecked(i, rangeValue);
-                }
-
-                mPositionMappingCheckBroker.onBulkCheckedChange();
-                mIgnoreItemCheckedStateChanges = false;
-            } else {
-                mPositionMappingCheckBroker.onCheckedChange(position, checked);
-            }
-            mLastCheckedPosition = position;
-            updateSelectedTitle(mode);
-        }
-
-        @Override
-        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            return onOptionsItemSelected(item);
-        }
-
-        @Override
-        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-            MenuInflater inflater = mode.getMenuInflater();
-            inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
-            updateSelectedTitle(mode);
-            mActiveActionMode = mode;
-            mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
-            setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible);
-            return true;
-        }
-
-        @Override
-        public void onDestroyActionMode(ActionMode mode) {
-            mActiveActionMode = null;
-            mActionMenuSwitcherItem = null;
-            mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE);
-        }
-
-        @Override
-        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-            updateSelectedTitle(mode);
-            return false;
-        }
-    };
-
-    public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.import_items:
-                if (mActiveActionMode != null) {
-                    mHelperService.importSelectedItems(
-                            mGridView.getCheckedItemPositions(),
-                            mAdapter);
-                    mActiveActionMode.finish();
-                }
-                return true;
-            case R.id.ingest_switch_view:
-                setFullscreenPagerVisibility(!mFullscreenPagerVisible);
-                return true;
-            default:
-                return false;
-        }
+    private void updateSelectedTitle(ActionMode mode) {
+      int count = mGridView.getCheckedItemCount();
+      mode.setTitle(getResources().getQuantityString(
+          R.plurals.ingest_number_of_items_selected, count, count));
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
-        mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
-        menu.findItem(R.id.import_items).setVisible(false);
-        setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible);
-        return true;
+    public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+        boolean checked) {
+      if (mIgnoreItemCheckedStateChanges) {
+        return;
+      }
+      if (mAdapter.itemAtPositionIsBucket(position)) {
+        SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions();
+        mIgnoreItemCheckedStateChanges = true;
+        mGridView.setItemChecked(position, false);
+
+        // Takes advantage of the fact that SectionIndexer imposes the
+        // need to clamp to the valid range
+        int nextSectionStart = mAdapter.getPositionForSection(
+            mAdapter.getSectionForPosition(position) + 1);
+        if (nextSectionStart == position) {
+          nextSectionStart = mAdapter.getCount();
+        }
+
+        boolean rangeValue = false; // Value we want to set all of the bucket items to
+
+        // Determine if all the items in the bucket are currently checked, so that we
+        // can uncheck them, otherwise we will check all items in the bucket.
+        for (int i = position + 1; i < nextSectionStart; i++) {
+          if (!checkedItems.get(i)) {
+            rangeValue = true;
+            break;
+          }
+        }
+
+        // Set all items in the bucket to the desired state
+        for (int i = position + 1; i < nextSectionStart; i++) {
+          if (checkedItems.get(i) != rangeValue) {
+            mGridView.setItemChecked(i, rangeValue);
+          }
+        }
+
+        mPositionMappingCheckBroker.onBulkCheckedChange();
+        mIgnoreItemCheckedStateChanges = false;
+      } else {
+        mPositionMappingCheckBroker.onCheckedChange(position, checked);
+      }
+      mLastCheckedPosition = position;
+      updateSelectedTitle(mode);
     }
 
     @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        doUnbindHelperService();
+    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+      return onOptionsItemSelected(item);
     }
 
     @Override
-    protected void onResume() {
-        DateTileView.refreshLocale();
-        mActive = true;
-        if (mHelperService != null) mHelperService.setClientActivity(this);
-        updateWarningView();
-        super.onResume();
+    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+      MenuInflater inflater = mode.getMenuInflater();
+      inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
+      updateSelectedTitle(mode);
+      mActiveActionMode = mode;
+      mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
+      setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible);
+      return true;
     }
 
     @Override
-    protected void onPause() {
-        if (mHelperService != null) mHelperService.setClientActivity(null);
-        mActive = false;
-        cleanupProgressDialog();
-        super.onPause();
+    public void onDestroyActionMode(ActionMode mode) {
+      mActiveActionMode = null;
+      mActionMenuSwitcherItem = null;
+      mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE);
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        MtpBitmapFetch.configureForContext(this);
-    }
-
-    private void showWarningView(int textResId) {
-        if (mWarningView == null) {
-            mWarningView = findViewById(R.id.ingest_warning_view);
-            mWarningText =
-                    (TextView)mWarningView.findViewById(R.id.ingest_warning_view_text);
-        }
-        mWarningText.setText(textResId);
-        mWarningView.setVisibility(View.VISIBLE);
-        setFullscreenPagerVisibility(false);
-        mGridView.setVisibility(View.GONE);
+    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+      updateSelectedTitle(mode);
+      return false;
     }
-
-    private void hideWarningView() {
-        if (mWarningView != null) {
-            mWarningView.setVisibility(View.GONE);
-            setFullscreenPagerVisibility(false);
-        }
+  };
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    int id = item.getItemId();
+    if (id == R.id.ingest_import_items) {
+      if (mActiveActionMode != null) {
+        mHelperService.importSelectedItems(
+            mGridView.getCheckedItemPositions(),
+            mAdapter);
+        mActiveActionMode.finish();
+      }
+      return true;
+    } else if (id == R.id.ingest_switch_view) {
+      setFullscreenPagerVisibility(!mFullscreenPagerVisible);
+      return true;
+    } else {
+      return false;
     }
-
-    private PositionMappingCheckBroker mPositionMappingCheckBroker = new PositionMappingCheckBroker();
-
-    private class PositionMappingCheckBroker extends CheckBroker
-        implements OnClearChoicesListener {
-        private int mLastMappingPager = -1;
-        private int mLastMappingGrid = -1;
-
-        private int mapPagerToGridPosition(int position) {
-            if (position != mLastMappingPager) {
-               mLastMappingPager = position;
-               mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
-            }
-            return mLastMappingGrid;
-        }
-
-        private int mapGridToPagerPosition(int position) {
-            if (position != mLastMappingGrid) {
-                mLastMappingGrid = position;
-                mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position);
-            }
-            return mLastMappingPager;
-        }
-
-        @Override
-        public void setItemChecked(int position, boolean checked) {
-            mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
-        }
-
-        @Override
-        public void onCheckedChange(int position, boolean checked) {
-            if (mPagerAdapter != null) {
-                super.onCheckedChange(mapGridToPagerPosition(position), checked);
-            }
-        }
-
-        @Override
-        public boolean isItemChecked(int position) {
-            return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
-        }
-
-        @Override
-        public void onClearChoices() {
-            onBulkCheckedChange();
-        }
-    };
-
-    private DataSetObserver mMasterObserver = new DataSetObserver() {
-        @Override
-        public void onChanged() {
-            if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged();
-        }
-
-        @Override
-        public void onInvalidated() {
-            if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged();
-        }
-    };
-
-    private int pickFullscreenStartingPosition() {
-        int firstVisiblePosition = mGridView.getFirstVisiblePosition();
-        if (mLastCheckedPosition <= firstVisiblePosition
-                || mLastCheckedPosition > mGridView.getLastVisiblePosition()) {
-            return firstVisiblePosition;
-        } else {
-            return mLastCheckedPosition;
-        }
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    MenuInflater inflater = getMenuInflater();
+    inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
+    mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
+    menu.findItem(R.id.ingest_import_items).setVisible(false);
+    setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible);
+    return true;
+  }
+
+  @Override
+  protected void onDestroy() {
+    doUnbindHelperService();
+    super.onDestroy();
+  }
+
+  @Override
+  protected void onResume() {
+    DateTileView.refreshLocale();
+    mActive = true;
+    if (mHelperService != null) {
+      mHelperService.setClientActivity(this);
     }
-
-    private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) {
-        if (menuItem == null) return;
-        if (!inFullscreenMode) {
-            menuItem.setIcon(android.R.drawable.ic_menu_zoom);
-            menuItem.setTitle(R.string.switch_photo_fullscreen);
-        } else {
-            menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
-            menuItem.setTitle(R.string.switch_photo_grid);
-        }
+    updateWarningView();
+    super.onResume();
+  }
+
+  @Override
+  protected void onPause() {
+    if (mHelperService != null) {
+      mHelperService.setClientActivity(null);
     }
-
-    private void setFullscreenPagerVisibility(boolean visible) {
-        mFullscreenPagerVisible = visible;
-        if (visible) {
-            if (mPagerAdapter == null) {
-                mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker);
-                mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex());
-            }
-            mFullscreenPager.setAdapter(mPagerAdapter);
-            mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels(
-                    pickFullscreenStartingPosition()), false);
-        } else if (mPagerAdapter != null) {
-            mGridView.setSelection(mAdapter.translatePositionWithoutLabels(
-                    mFullscreenPager.getCurrentItem()));
-            mFullscreenPager.setAdapter(null);
-        }
-        mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE);
-        mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-        if (mActionMenuSwitcherItem != null) {
-            setSwitcherMenuState(mActionMenuSwitcherItem, visible);
-        }
-        setSwitcherMenuState(mMenuSwitcherItem, visible);
+    mActive = false;
+    cleanupProgressDialog();
+    super.onPause();
+  }
+
+  @Override
+  public void onConfigurationChanged(Configuration newConfig) {
+    super.onConfigurationChanged(newConfig);
+    MtpBitmapFetch.configureForContext(this);
+  }
+
+  private void showWarningView(int textResId) {
+    if (mWarningView == null) {
+      mWarningView = findViewById(R.id.ingest_warning_view);
+      mWarningText =
+          (TextView) mWarningView.findViewById(R.id.ingest_warning_view_text);
     }
-
-    private void updateWarningView() {
-        if (!mAdapter.deviceConnected()) {
-            showWarningView(R.string.ingest_no_device);
-        } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) {
-            showWarningView(R.string.ingest_empty_device);
-        } else {
-            hideWarningView();
-        }
+    mWarningText.setText(textResId);
+    mWarningView.setVisibility(View.VISIBLE);
+    setFullscreenPagerVisibility(false);
+    mGridView.setVisibility(View.GONE);
+    setSwitcherMenuVisibility(false);
+  }
+
+  private void hideWarningView() {
+    if (mWarningView != null) {
+      mWarningView.setVisibility(View.GONE);
+      setFullscreenPagerVisibility(false);
     }
-
-    private void UiThreadNotifyIndexChanged() {
-        mAdapter.notifyDataSetChanged();
-        if (mActiveActionMode != null) {
-            mActiveActionMode.finish();
-            mActiveActionMode = null;
-        }
-        updateWarningView();
+    setSwitcherMenuVisibility(true);
+  }
+
+  private PositionMappingCheckBroker mPositionMappingCheckBroker =
+      new PositionMappingCheckBroker();
+
+  private class PositionMappingCheckBroker extends CheckBroker
+      implements OnClearChoicesListener {
+    private int mLastMappingPager = -1;
+    private int mLastMappingGrid = -1;
+
+    private int mapPagerToGridPosition(int position) {
+      if (position != mLastMappingPager) {
+        mLastMappingPager = position;
+        mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
+      }
+      return mLastMappingGrid;
     }
 
-    protected void notifyIndexChanged() {
-        mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+    private int mapGridToPagerPosition(int position) {
+      if (position != mLastMappingGrid) {
+        mLastMappingGrid = position;
+        mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position);
+      }
+      return mLastMappingPager;
     }
 
-    private static class ProgressState {
-        String message;
-        String title;
-        int current;
-        int max;
-
-        public void reset() {
-            title = null;
-            message = null;
-            current = 0;
-            max = 0;
-        }
+    @Override
+    public void setItemChecked(int position, boolean checked) {
+      mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
     }
 
-    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);
+    public void onCheckedChange(int position, boolean checked) {
+      if (mPagerAdapter != null) {
+        super.onCheckedChange(mapGridToPagerPosition(position), checked);
+      }
     }
 
     @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);
+    public boolean isItemChecked(int position) {
+      return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
     }
 
     @Override
-    public void onIndexFinish() {
-        // Not guaranteed to be called on the UI thread
-        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
-        mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+    public void onClearChoices() {
+      onBulkCheckedChange();
     }
+  }
 
+  private DataSetObserver mMasterObserver = new DataSetObserver() {
     @Override
-    public void onImportProgress(final int visitedCount, final int totalCount,
-            String pathIfSuccessful) {
-        // Not guaranteed to be called on the UI thread
-        mProgressState.reset();
-        mProgressState.max = totalCount;
-        mProgressState.current = visitedCount;
-        mProgressState.title = getResources().getString(R.string.ingest_importing);
-        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
-        mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
-        mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE,
-                INDETERMINATE_SWITCH_TIMEOUT_MS);
+    public void onChanged() {
+      if (mPagerAdapter != null) {
+        mPagerAdapter.notifyDataSetChanged();
+      }
     }
 
     @Override
-    public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
-            int numVisited) {
-        // Not guaranteed to be called on the UI thread
-        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
-        mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
-        // TODO: maybe show an extra dialog listing the ones that failed
-        // importing, if any?
-    }
-
-    private ProgressDialog getProgressDialog() {
-        if (mProgressDialog == null || !mProgressDialog.isShowing()) {
-            mProgressDialog = new ProgressDialog(this);
-            mProgressDialog.setCancelable(false);
-        }
-        return mProgressDialog;
+    public void onInvalidated() {
+      if (mPagerAdapter != null) {
+        mPagerAdapter.notifyDataSetChanged();
+      }
+    }
+  };
+
+  private int pickFullscreenStartingPosition() {
+    int firstVisiblePosition = mGridView.getFirstVisiblePosition();
+    if (mLastCheckedPosition <= firstVisiblePosition
+        || mLastCheckedPosition > mGridView.getLastVisiblePosition()) {
+      return firstVisiblePosition;
+    } else {
+      return mLastCheckedPosition;
     }
+  }
 
-    private void 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 setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) {
+    if (menuItem == null) {
+      return;
+    }
+    if (!inFullscreenMode) {
+      menuItem.setIcon(android.R.drawable.ic_menu_zoom);
+      menuItem.setTitle(R.string.ingest_switch_photo_fullscreen);
+    } else {
+      menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
+      menuItem.setTitle(R.string.ingest_switch_photo_grid);
+    }
+  }
+
+  private void setFullscreenPagerVisibility(boolean visible) {
+    mFullscreenPagerVisible = visible;
+    if (visible) {
+      if (mPagerAdapter == null) {
+        mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker);
+        mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex());
+      }
+      mFullscreenPager.setAdapter(mPagerAdapter);
+      mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels(
+          pickFullscreenStartingPosition()), false);
+    } else if (mPagerAdapter != null) {
+      mGridView.setSelection(mAdapter.translatePositionWithoutLabels(
+          mFullscreenPager.getCurrentItem()));
+      mFullscreenPager.setAdapter(null);
+    }
+    mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE);
+    mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+    if (mActionMenuSwitcherItem != null) {
+      setSwitcherMenuState(mActionMenuSwitcherItem, visible);
     }
+    setSwitcherMenuState(mMenuSwitcherItem, visible);
+  }
 
-    private void makeProgressDialogIndeterminate() {
-        ProgressDialog dialog = getProgressDialog();
-        dialog.setIndeterminate(true);
+  private void setSwitcherMenuVisibility(boolean visible) {
+    if (mActionMenuSwitcherItem != null) {
+      mActionMenuSwitcherItem.setVisible(visible);
     }
+    if (mMenuSwitcherItem != null) {
+      mMenuSwitcherItem.setVisible(visible);
+    }
+  }
+
+  private void updateWarningView() {
+    if (!mAdapter.deviceConnected()) {
+      showWarningView(R.string.ingest_no_device);
+    } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) {
+      showWarningView(R.string.ingest_empty_device);
+    } else {
+      hideWarningView();
+    }
+  }
 
-    private void cleanupProgressDialog() {
-        if (mProgressDialog != null) {
-            mProgressDialog.hide();
-            mProgressDialog = null;
-        }
+  private void uiThreadNotifyIndexChanged() {
+    mAdapter.notifyDataSetChanged();
+    if (mActiveActionMode != null) {
+      mActiveActionMode.finish();
+      mActiveActionMode = null;
+    }
+    updateWarningView();
+  }
+
+  protected void notifyIndexChanged() {
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+  }
+
+  private static class ProgressState {
+    String message;
+    String title;
+    int current;
+    int max;
+
+    public void reset() {
+      title = null;
+      message = null;
+      current = 0;
+      max = 0;
+    }
+  }
+
+  private ProgressState mProgressState = new ProgressState();
+
+  @Override
+  public void onObjectIndexed(IngestObjectInfo object, int numVisited) {
+    // Not guaranteed to be called on the UI thread
+    mProgressState.reset();
+    mProgressState.max = 0;
+    mProgressState.message = getResources().getQuantityString(
+        R.plurals.ingest_number_of_items_scanned, numVisited, numVisited);
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+  }
+
+  @Override
+  public void onSortingStarted() {
+    // Not guaranteed to be called on the UI thread
+    mProgressState.reset();
+    mProgressState.max = 0;
+    mProgressState.message = getResources().getString(R.string.ingest_sorting);
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+  }
+
+  @Override
+  public void onIndexingFinished() {
+    // Not guaranteed to be called on the UI thread
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+  }
+
+  @Override
+  public void onImportProgress(final int visitedCount, final int totalCount,
+      String pathIfSuccessful) {
+    // Not guaranteed to be called on the UI thread
+    mProgressState.reset();
+    mProgressState.max = totalCount;
+    mProgressState.current = visitedCount;
+    mProgressState.title = getResources().getString(R.string.ingest_importing);
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+    mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
+    mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE,
+        INDETERMINATE_SWITCH_TIMEOUT_MS);
+  }
+
+  @Override
+  public void onImportFinish(Collection<IngestObjectInfo> objectsNotImported,
+      int numVisited) {
+    // Not guaranteed to be called on the UI thread
+    mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+    mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
+    // TODO(georgescu): maybe show an extra dialog listing the ones that failed
+    // importing, if any?
+  }
+
+  private ProgressDialog getProgressDialog() {
+    if (mProgressDialog == null || !mProgressDialog.isShowing()) {
+      mProgressDialog = new ProgressDialog(this);
+      mProgressDialog.setCancelable(false);
+    }
+    return mProgressDialog;
+  }
+
+  private void updateProgressDialog() {
+    ProgressDialog dialog = getProgressDialog();
+    boolean indeterminate = (mProgressState.max == 0);
+    dialog.setIndeterminate(indeterminate);
+    dialog.setProgressStyle(indeterminate ? ProgressDialog.STYLE_SPINNER
+        : ProgressDialog.STYLE_HORIZONTAL);
+    if (mProgressState.title != null) {
+      dialog.setTitle(mProgressState.title);
+    }
+    if (mProgressState.message != null) {
+      dialog.setMessage(mProgressState.message);
+    }
+    if (!indeterminate) {
+      dialog.setProgress(mProgressState.current);
+      dialog.setMax(mProgressState.max);
+    }
+    if (!dialog.isShowing()) {
+      dialog.show();
     }
+  }
 
-    // This is static and uses a WeakReference in order to avoid leaking the Activity
-    private static class ItemListHandler extends Handler {
-        public static final int MSG_PROGRESS_UPDATE = 0;
-        public static final int MSG_PROGRESS_HIDE = 1;
-        public static final int MSG_NOTIFY_CHANGED = 2;
-        public static final int MSG_BULK_CHECKED_CHANGE = 3;
-        public static final int MSG_PROGRESS_INDETERMINATE = 4;
+  private void makeProgressDialogIndeterminate() {
+    ProgressDialog dialog = getProgressDialog();
+    dialog.setIndeterminate(true);
+  }
 
-        WeakReference<IngestActivity> mParentReference;
+  private void cleanupProgressDialog() {
+    if (mProgressDialog != null) {
+      mProgressDialog.dismiss();
+      mProgressDialog = null;
+    }
+  }
 
-        public ItemListHandler(IngestActivity parent) {
-            super();
-            mParentReference = new WeakReference<IngestActivity>(parent);
-        }
+  // This is static and uses a WeakReference in order to avoid leaking the Activity
+  private static class ItemListHandler extends Handler {
+    public static final int MSG_PROGRESS_UPDATE = 0;
+    public static final int MSG_PROGRESS_HIDE = 1;
+    public static final int MSG_NOTIFY_CHANGED = 2;
+    public static final int MSG_BULK_CHECKED_CHANGE = 3;
+    public static final int MSG_PROGRESS_INDETERMINATE = 4;
 
-        public void handleMessage(Message message) {
-            IngestActivity parent = mParentReference.get();
-            if (parent == null || !parent.mActive)
-                return;
-            switch (message.what) {
-                case MSG_PROGRESS_HIDE:
-                    parent.cleanupProgressDialog();
-                    break;
-                case MSG_PROGRESS_UPDATE:
-                    parent.updateProgressDialog();
-                    break;
-                case MSG_NOTIFY_CHANGED:
-                    parent.UiThreadNotifyIndexChanged();
-                    break;
-                case MSG_BULK_CHECKED_CHANGE:
-                    parent.mPositionMappingCheckBroker.onBulkCheckedChange();
-                    break;
-                case MSG_PROGRESS_INDETERMINATE:
-                    parent.makeProgressDialogIndeterminate();
-                    break;
-                default:
-                    break;
-            }
-        }
+    WeakReference<IngestActivity> mParentReference;
+
+    public ItemListHandler(IngestActivity parent) {
+      super();
+      mParentReference = new WeakReference<IngestActivity>(parent);
     }
 
-    private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            mHelperService = ((IngestService.LocalBinder) service).getService();
-            mHelperService.setClientActivity(IngestActivity.this);
-            MtpDeviceIndex index = mHelperService.getIndex();
-            mAdapter.setMtpDeviceIndex(index);
-            if (mPagerAdapter != null) mPagerAdapter.setMtpDeviceIndex(index);
-        }
+    @Override
+    public void handleMessage(Message message) {
+      IngestActivity parent = mParentReference.get();
+      if (parent == null || !parent.mActive) {
+        return;
+      }
+      switch (message.what) {
+        case MSG_PROGRESS_HIDE:
+          parent.cleanupProgressDialog();
+          break;
+        case MSG_PROGRESS_UPDATE:
+          parent.updateProgressDialog();
+          break;
+        case MSG_NOTIFY_CHANGED:
+          parent.uiThreadNotifyIndexChanged();
+          break;
+        case MSG_BULK_CHECKED_CHANGE:
+          parent.mPositionMappingCheckBroker.onBulkCheckedChange();
+          break;
+        case MSG_PROGRESS_INDETERMINATE:
+          parent.makeProgressDialogIndeterminate();
+          break;
+        default:
+          break;
+      }
+    }
+  }
 
-        public void onServiceDisconnected(ComponentName className) {
-            mHelperService = null;
-        }
-    };
+  private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
+    @Override
+    public void onServiceConnected(ComponentName className, IBinder service) {
+      mHelperService = ((IngestService.LocalBinder) service).getService();
+      mHelperService.setClientActivity(IngestActivity.this);
+      MtpDeviceIndex index = mHelperService.getIndex();
+      mAdapter.setMtpDeviceIndex(index);
+      if (mPagerAdapter != null) {
+        mPagerAdapter.setMtpDeviceIndex(index);
+      }
+    }
 
-    private void doBindHelperService() {
-        bindService(new Intent(getApplicationContext(), IngestService.class),
-                mHelperServiceConnection, Context.BIND_AUTO_CREATE);
+    @Override
+    public void onServiceDisconnected(ComponentName className) {
+      mHelperService = null;
     }
+  };
 
-    private void doUnbindHelperService() {
-        if (mHelperService != null) {
-            mHelperService.setClientActivity(null);
-            unbindService(mHelperServiceConnection);
-        }
+  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);
     }
+  }
 }
index 9d406b1..98aa7f5 100644 (file)
 
 package com.android.gallery3d.ingest;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.ImportTask;
+import com.android.gallery3d.ingest.data.IngestObjectInfo;
+import com.android.gallery3d.ingest.data.MtpClient;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex;
+
+import android.annotation.TargetApi;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -25,299 +32,303 @@ 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.Build;
 import android.os.IBinder;
 import android.os.SystemClock;
 import android.support.v4.app.NotificationCompat;
 import android.util.SparseBooleanArray;
 import android.widget.Adapter;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.NotificationIds;
-import com.android.gallery3d.data.MtpClient;
-import com.android.gallery3d.util.BucketNames;
-import com.android.gallery3d.util.UsageStatistics;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
+/**
+ * Service for MTP importing tasks.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
 public class IngestService extends Service implements ImportTask.Listener,
-        MtpDeviceIndex.ProgressListener, MtpClient.Listener {
+    MtpDeviceIndex.ProgressListener, MtpClient.Listener {
 
-    public class LocalBinder extends Binder {
-        IngestService getService() {
-            return IngestService.this;
-        }
+  /**
+   * Convenience class to allow easy access to the service instance.
+   */
+  public class LocalBinder extends Binder {
+    IngestService getService() {
+      return IngestService.this;
     }
+  }
 
-    private static final int PROGRESS_UPDATE_INTERVAL_MS = 180;
+  private static final int PROGRESS_UPDATE_INTERVAL_MS = 180;
 
-    private static MtpClient sClient;
+  private MtpClient mClient;
+  private final IBinder mBinder = new LocalBinder();
+  private ScannerClient mScannerClient;
+  private MtpDevice mDevice;
+  private String mDevicePrettyName;
+  private MtpDeviceIndex mIndex;
+  private IngestActivity mClientActivity;
+  private boolean mRedeliverImportFinish = false;
+  private int mRedeliverImportFinishCount = 0;
+  private Collection<IngestObjectInfo> mRedeliverObjectsNotImported;
+  private boolean mRedeliverNotifyIndexChanged = false;
+  private boolean mRedeliverIndexFinish = false;
+  private NotificationManager mNotificationManager;
+  private NotificationCompat.Builder mNotificationBuilder;
+  private long mLastProgressIndexTime = 0;
+  private boolean mNeedRelaunchNotification = false;
 
-    private final IBinder mBinder = new LocalBinder();
-    private ScannerClient mScannerClient;
-    private MtpDevice mDevice;
-    private String mDevicePrettyName;
-    private MtpDeviceIndex mIndex;
-    private IngestActivity mClientActivity;
-    private boolean mRedeliverImportFinish = false;
-    private int mRedeliverImportFinishCount = 0;
-    private Collection<MtpObjectInfo> mRedeliverObjectsNotImported;
-    private boolean mRedeliverNotifyIndexChanged = false;
-    private boolean mRedeliverIndexFinish = false;
-    private NotificationManager mNotificationManager;
-    private NotificationCompat.Builder mNotificationBuilder;
-    private long mLastProgressIndexTime = 0;
-    private boolean mNeedRelaunchNotification = false;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        mScannerClient = new ScannerClient(this);
-        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        mNotificationBuilder = new NotificationCompat.Builder(this);
-        mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync) // TODO drawable
-                .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, IngestActivity.class), 0));
-        mIndex = MtpDeviceIndex.getInstance();
-        mIndex.setProgressListener(this);
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    mScannerClient = new ScannerClient(this);
+    mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+    mNotificationBuilder = new NotificationCompat.Builder(this);
+    // TODO(georgescu): Use a better drawable for the notificaton?
+    mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync)
+        .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);
+    mClient = new MtpClient(getApplicationContext());
+    List<MtpDevice> devices = mClient.getDeviceList();
+    if (!devices.isEmpty()) {
+      setDevice(devices.get(0));
     }
+    mClient.addListener(this);
+  }
 
-    @Override
-    public void onDestroy() {
-        sClient.removeListener(this);
-        mIndex.unsetProgressListener(this);
-        super.onDestroy();
-    }
+  @Override
+  public void onDestroy() {
+    mClient.close();
+    mIndex.unsetProgressListener(this);
+    super.onDestroy();
+  }
 
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
+  @Override
+  public IBinder onBind(Intent intent) {
+    return mBinder;
+  }
 
-    private void setDevice(MtpDevice device) {
-        if (mDevice == device) return;
-        mRedeliverImportFinish = false;
-        mRedeliverObjectsNotImported = null;
-        mRedeliverNotifyIndexChanged = false;
-        mRedeliverIndexFinish = false;
-        mDevice = device;
-        mIndex.setDevice(mDevice);
-        if (mDevice != null) {
-            MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
-            if (deviceInfo == null) {
-                setDevice(null);
-                return;
-            } else {
-                mDevicePrettyName = deviceInfo.getModel();
-                mNotificationBuilder.setContentTitle(mDevicePrettyName);
-                new Thread(mIndex.getIndexRunnable()).start();
-            }
-        } else {
-            mDevicePrettyName = null;
-        }
-        if (mClientActivity != null) {
-            mClientActivity.notifyIndexChanged();
-        } else {
-            mRedeliverNotifyIndexChanged = true;
-        }
+  private void setDevice(MtpDevice device) {
+    if (mDevice == device) {
+      return;
     }
-
-    protected MtpDeviceIndex getIndex() {
-        return mIndex;
+    mRedeliverImportFinish = false;
+    mRedeliverObjectsNotImported = null;
+    mRedeliverNotifyIndexChanged = false;
+    mRedeliverIndexFinish = false;
+    mDevice = device;
+    mIndex.setDevice(mDevice);
+    if (mDevice != null) {
+      MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
+      if (deviceInfo == null) {
+        setDevice(null);
+        return;
+      } else {
+        mDevicePrettyName = deviceInfo.getModel();
+        mNotificationBuilder.setContentTitle(mDevicePrettyName);
+        new Thread(mIndex.getIndexRunnable()).start();
+      }
+    } else {
+      mDevicePrettyName = null;
     }
+    if (mClientActivity != null) {
+      mClientActivity.notifyIndexChanged();
+    } else {
+      mRedeliverNotifyIndexChanged = true;
+    }
+  }
 
-    protected void setClientActivity(IngestActivity activity) {
-        if (mClientActivity == activity) return;
-        mClientActivity = activity;
-        if (mClientActivity == null) {
-            if (mNeedRelaunchNotification) {
-                mNotificationBuilder.setProgress(0, 0, false)
-                    .setContentText(getResources().getText(R.string.ingest_scanning_done));
-                mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
-                    mNotificationBuilder.build());
-            }
-            return;
-        }
-        mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_IMPORTING);
-        mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
-        if (mRedeliverImportFinish) {
-            mClientActivity.onImportFinish(mRedeliverObjectsNotImported,
-                    mRedeliverImportFinishCount);
-            mRedeliverImportFinish = false;
-            mRedeliverObjectsNotImported = null;
-        }
-        if (mRedeliverNotifyIndexChanged) {
-            mClientActivity.notifyIndexChanged();
-            mRedeliverNotifyIndexChanged = false;
-        }
-        if (mRedeliverIndexFinish) {
-            mClientActivity.onIndexFinish();
-            mRedeliverIndexFinish = false;
-        }
-        if (mDevice != null) {
-            mNeedRelaunchNotification = true;
-        }
+  protected MtpDeviceIndex getIndex() {
+    return mIndex;
+  }
+
+  protected void setClientActivity(IngestActivity activity) {
+    if (mClientActivity == activity) {
+      return;
+    }
+    mClientActivity = activity;
+    if (mClientActivity == null) {
+      if (mNeedRelaunchNotification) {
+        mNotificationBuilder.setProgress(0, 0, false)
+            .setContentText(getResources().getText(R.string.ingest_scanning_done));
+        mNotificationManager.notify(R.id.ingest_notification_scanning,
+            mNotificationBuilder.build());
+      }
+      return;
+    }
+    mNotificationManager.cancel(R.id.ingest_notification_importing);
+    mNotificationManager.cancel(R.id.ingest_notification_scanning);
+    if (mRedeliverImportFinish) {
+      mClientActivity.onImportFinish(mRedeliverObjectsNotImported,
+          mRedeliverImportFinishCount);
+      mRedeliverImportFinish = false;
+      mRedeliverObjectsNotImported = null;
+    }
+    if (mRedeliverNotifyIndexChanged) {
+      mClientActivity.notifyIndexChanged();
+      mRedeliverNotifyIndexChanged = false;
     }
+    if (mRedeliverIndexFinish) {
+      mClientActivity.onIndexingFinished();
+      mRedeliverIndexFinish = false;
+    }
+    if (mDevice != null) {
+      mNeedRelaunchNotification = true;
+    }
+  }
 
-    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));
-                }
-            }
+  protected void importSelectedItems(SparseBooleanArray selected, Adapter adapter) {
+    List<IngestObjectInfo> importHandles = new ArrayList<IngestObjectInfo>();
+    for (int i = 0; i < selected.size(); i++) {
+      if (selected.valueAt(i)) {
+        Object item = adapter.getItem(selected.keyAt(i));
+        if (item instanceof IngestObjectInfo) {
+          importHandles.add(((IngestObjectInfo) 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();
+      }
     }
+    ImportTask task = new ImportTask(mDevice, importHandles, mDevicePrettyName, this);
+    task.setListener(this);
+    mNotificationBuilder.setProgress(0, 0, true)
+        .setContentText(getResources().getText(R.string.ingest_importing));
+    startForeground(R.id.ingest_notification_importing,
+        mNotificationBuilder.build());
+    new Thread(task).start();
+  }
 
-    @Override
-    public void deviceAdded(MtpDevice device) {
-        if (mDevice == null) {
-            setDevice(device);
-            UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
-                    "DeviceConnected", null);
-        }
+  @Override
+  public void deviceAdded(MtpDevice device) {
+    if (mDevice == null) {
+      setDevice(device);
     }
+  }
+
+  @Override
+  public void deviceRemoved(MtpDevice device) {
+    if (device == mDevice) {
+      mNotificationManager.cancel(R.id.ingest_notification_scanning);
+      mNotificationManager.cancel(R.id.ingest_notification_importing);
+      setDevice(null);
+      mNeedRelaunchNotification = false;
 
-    @Override
-    public void deviceRemoved(MtpDevice device) {
-        if (device == mDevice) {
-            setDevice(null);
-            mNeedRelaunchNotification = false;
-            mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
-        }
     }
+  }
 
-    @Override
-    public void onImportProgress(int visitedCount, int totalCount,
-            String pathIfSuccessful) {
-        if (pathIfSuccessful != null) {
-            mScannerClient.scanPath(pathIfSuccessful);
-        }
-        mNeedRelaunchNotification = false;
-        if (mClientActivity != null) {
-            mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
-        }
-        mNotificationBuilder.setProgress(totalCount, visitedCount, false)
-            .setContentText(getResources().getText(R.string.ingest_importing));
-        mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
-                mNotificationBuilder.build());
+  @Override
+  public void onImportProgress(int visitedCount, int totalCount,
+      String pathIfSuccessful) {
+    if (pathIfSuccessful != null) {
+      mScannerClient.scanPath(pathIfSuccessful);
     }
+    mNeedRelaunchNotification = false;
+    if (mClientActivity != null) {
+      mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
+    }
+    mNotificationBuilder.setProgress(totalCount, visitedCount, false)
+        .setContentText(getResources().getText(R.string.ingest_importing));
+    mNotificationManager.notify(R.id.ingest_notification_importing,
+        mNotificationBuilder.build());
+  }
 
-    @Override
-    public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
-            int visitedCount) {
-        stopForeground(true);
-        mNeedRelaunchNotification = true;
-        if (mClientActivity != null) {
-            mClientActivity.onImportFinish(objectsNotImported, visitedCount);
-        } else {
-            mRedeliverImportFinish = true;
-            mRedeliverObjectsNotImported = objectsNotImported;
-            mRedeliverImportFinishCount = visitedCount;
-            mNotificationBuilder.setProgress(0, 0, false)
-                .setContentText(getResources().getText(R.string.import_complete));
-            mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
-                    mNotificationBuilder.build());
-        }
-        UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
-                "ImportFinished", null, visitedCount);
+  @Override
+  public void onImportFinish(Collection<IngestObjectInfo> objectsNotImported,
+      int visitedCount) {
+    stopForeground(true);
+    mNeedRelaunchNotification = true;
+    if (mClientActivity != null) {
+      mClientActivity.onImportFinish(objectsNotImported, visitedCount);
+    } else {
+      mRedeliverImportFinish = true;
+      mRedeliverObjectsNotImported = objectsNotImported;
+      mRedeliverImportFinishCount = visitedCount;
+      mNotificationBuilder.setProgress(0, 0, false)
+          .setContentText(getResources().getText(R.string.ingest_import_complete));
+      mNotificationManager.notify(R.id.ingest_notification_importing,
+          mNotificationBuilder.build());
     }
+  }
 
-    @Override
-    public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
-        mNeedRelaunchNotification = false;
-        if (mClientActivity != null) {
-            mClientActivity.onObjectIndexed(object, numVisited);
-        } else {
-            // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds
-            long currentTime = SystemClock.uptimeMillis();
-            if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) {
-                mLastProgressIndexTime = currentTime;
-                mNotificationBuilder.setProgress(0, numVisited, true)
-                        .setContentText(getResources().getText(R.string.ingest_scanning));
-                mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
-                        mNotificationBuilder.build());
-            }
-        }
+  @Override
+  public void onObjectIndexed(IngestObjectInfo object, int numVisited) {
+    mNeedRelaunchNotification = false;
+    if (mClientActivity != null) {
+      mClientActivity.onObjectIndexed(object, numVisited);
+    } else {
+      // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds
+      long currentTime = SystemClock.uptimeMillis();
+      if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) {
+        mLastProgressIndexTime = currentTime;
+        mNotificationBuilder.setProgress(0, numVisited, true)
+            .setContentText(getResources().getText(R.string.ingest_scanning));
+        mNotificationManager.notify(R.id.ingest_notification_scanning,
+            mNotificationBuilder.build());
+      }
     }
+  }
 
-    @Override
-    public void onSorting() {
-        if (mClientActivity != null) mClientActivity.onSorting();
+  @Override
+  public void onSortingStarted() {
+    if (mClientActivity != null) {
+      mClientActivity.onSortingStarted();
     }
+  }
 
-    @Override
-    public void onIndexFinish() {
-        mNeedRelaunchNotification = true;
-        if (mClientActivity != null) {
-            mClientActivity.onIndexFinish();
-        } else {
-            mNotificationBuilder.setProgress(0, 0, false)
-                .setContentText(getResources().getText(R.string.ingest_scanning_done));
-            mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
-                    mNotificationBuilder.build());
-            mRedeliverIndexFinish = true;
-        }
+  @Override
+  public void onIndexingFinished() {
+    mNeedRelaunchNotification = true;
+    if (mClientActivity != null) {
+      mClientActivity.onIndexingFinished();
+    } else {
+      mNotificationBuilder.setProgress(0, 0, false)
+          .setContentText(getResources().getText(R.string.ingest_scanning_done));
+      mNotificationManager.notify(R.id.ingest_notification_scanning,
+          mNotificationBuilder.build());
+      mRedeliverIndexFinish = true;
     }
+  }
 
-    // Copied from old Gallery3d code
-    private static final class ScannerClient implements MediaScannerConnectionClient {
-        ArrayList<String> mPaths = new ArrayList<String>();
-        MediaScannerConnection mScannerConnection;
-        boolean mConnected;
-        Object mLock = new Object();
+  // 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 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();
-                }
-            }
+    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 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) {
-        }
+    @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
deleted file mode 100644 (file)
index fed851e..0000000
+++ /dev/null
@@ -1,602 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ingest;
-
-import android.mtp.MtpConstants;
-import android.mtp.MtpDevice;
-import android.mtp.MtpObjectInfo;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.Stack;
-
-/**
- * MTP objects in the index are organized into "buckets," or groupings.
- * At present, these buckets are based on the date an item was created.
- *
- * When the index is created, the buckets are sorted in their natural
- * order, and the items within the buckets sorted by the date they are taken.
- *
- * The index enables the access of items and bucket labels as one unified list.
- * For example, let's say we have the following data in the index:
- *    [Bucket A]: [photo 1], [photo 2]
- *    [Bucket B]: [photo 3]
- *
- * Then the items can be thought of as being organized as a 5 element list:
- *   [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
- *
- * The data can also be accessed in descending order, in which case the list
- * would be a bit different from simply reversing the ascending list, since the
- * bucket labels need to always be at the beginning:
- *   [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
- *
- * The index enables all the following operations in constant time, both for
- * ascending and descending views of the data:
- *   - get/getAscending/getDescending: get an item at a specified list position
- *   - size: get the total number of items (bucket labels and MTP objects)
- *   - getFirstPositionForBucketNumber
- *   - getBucketNumberForPosition
- *   - isFirstInBucket
- *
- * See the comments in buildLookupIndex for implementation notes.
- */
-public class MtpDeviceIndex {
-
-    public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants
-
-    public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
-    public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
-
-    static {
-        SUPPORTED_IMAGE_FORMATS = new HashSet<Integer>();
-        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_JFIF);
-        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_EXIF_JPEG);
-        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_PNG);
-        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_GIF);
-        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_BMP);
-
-        SUPPORTED_VIDEO_FORMATS = new HashSet<Integer>();
-        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_3GP_CONTAINER);
-        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_AVI);
-        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MP4_CONTAINER);
-        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MPEG);
-        // TODO: add FORMAT_MOV once Media Scanner supports .mov files
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId());
-        result = prime * result + mGeneration;
-        return result;
-    }
-
-    public interface ProgressListener {
-        public void onObjectIndexed(MtpObjectInfo object, int numVisited);
-
-        public void onSorting();
-
-        public void onIndexFinish();
-    }
-
-    public enum SortOrder {
-        Ascending, Descending
-    }
-
-    private MtpDevice mDevice;
-    private int[] mUnifiedLookupIndex;
-    private MtpObjectInfo[] mMtpObjects;
-    private DateBucket[] mBuckets;
-    private int mGeneration = 0;
-
-    public enum Progress {
-        Uninitialized, Initialized, Pending, Started, Sorting, Finished
-    }
-
-    private Progress mProgress = Progress.Uninitialized;
-    private ProgressListener mProgressListener;
-
-    private static final MtpDeviceIndex sInstance = new MtpDeviceIndex();
-    private static final MtpObjectTimestampComparator sMtpObjectComparator =
-            new MtpObjectTimestampComparator();
-
-    public static MtpDeviceIndex getInstance() {
-        return sInstance;
-    }
-
-    private MtpDeviceIndex() {
-    }
-
-    synchronized public MtpDevice getDevice() {
-        return mDevice;
-    }
-
-    /**
-     * Sets the MtpDevice that should be indexed and initializes state, but does
-     * not kick off the actual indexing task, which is instead done by using
-     * {@link #getIndexRunnable()}
-     *
-     * @param device The MtpDevice that should be indexed
-     */
-    synchronized public void setDevice(MtpDevice device) {
-        if (device == mDevice) return;
-        mDevice = device;
-        resetState();
-    }
-
-    /**
-     * Provides a Runnable for the indexing task assuming the state has already
-     * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and
-     * has not already been run.
-     *
-     * @return Runnable for the main indexing task
-     */
-    synchronized public Runnable getIndexRunnable() {
-        if (mProgress != Progress.Initialized) return null;
-        mProgress = Progress.Pending;
-        return new IndexRunnable(mDevice);
-    }
-
-    synchronized public boolean indexReady() {
-        return mProgress == Progress.Finished;
-    }
-
-    synchronized public Progress getProgress() {
-        return mProgress;
-    }
-
-    /**
-     * @param listener Listener to change to
-     * @return Progress at the time the listener was added (useful for
-     *         configuring initial UI state)
-     */
-    synchronized public Progress setProgressListener(ProgressListener listener) {
-        mProgressListener = listener;
-        return mProgress;
-    }
-
-    /**
-     * Make the listener null if it matches the argument
-     *
-     * @param listener Listener to unset, if currently registered
-     */
-    synchronized public void unsetProgressListener(ProgressListener listener) {
-        if (mProgressListener == listener)
-            mProgressListener = null;
-    }
-
-    /**
-     * @return The total number of elements in the index (labels and items)
-     */
-    public int size() {
-        return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0;
-    }
-
-    /**
-     * @param position Index of item to fetch, where 0 is the first item in the
-     *            specified order
-     * @param order
-     * @return the bucket label or MtpObjectInfo at the specified position and
-     *         order
-     */
-    public Object get(int position, SortOrder order) {
-        if (mProgress != Progress.Finished) return null;
-        if(order == SortOrder.Ascending) {
-            DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
-            if (bucket.unifiedStartIndex == position) {
-                return bucket.bucket;
-            } else {
-                return mMtpObjects[bucket.itemsStartIndex + position - 1
-                                   - bucket.unifiedStartIndex];
-            }
-        } else {
-            int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
-            DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
-            if (bucket.unifiedEndIndex == zeroIndex) {
-                return bucket.bucket;
-            } else {
-                return mMtpObjects[bucket.itemsStartIndex + zeroIndex
-                                   - bucket.unifiedStartIndex];
-            }
-        }
-    }
-
-    /**
-     * @param position Index of item to fetch from a view of the data that doesn't
-     *            include labels and is in the specified order
-     * @return position-th item in specified order, when not including labels
-     */
-    public MtpObjectInfo getWithoutLabels(int position, SortOrder order) {
-        if (mProgress != Progress.Finished) return null;
-        if (order == SortOrder.Ascending) {
-            return mMtpObjects[position];
-        } else {
-            return mMtpObjects[mMtpObjects.length - 1 - position];
-        }
-    }
-
-    /**
-     * Although this is O(log(number of buckets)), and thus should not be used
-     * in hotspots, even if the attached device has items for every day for
-     * a five-year timeframe, it would still only take 11 iterations at most,
-     * so shouldn't be a huge issue.
-     * @param position Index of item to map from a view of the data that doesn't
-     *            include labels and is in the specified order
-     * @param order
-     * @return position in a view of the data that does include labels
-     */
-    public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
-        if (mProgress != Progress.Finished) return -1;
-        if (order == SortOrder.Descending) {
-            position = mMtpObjects.length - 1 - position;
-        }
-        int bucketNumber = 0;
-        int iMin = 0;
-        int iMax = mBuckets.length - 1;
-        while (iMax >= iMin) {
-            int iMid = (iMax + iMin) / 2;
-            if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) {
-                iMin = iMid + 1;
-            } else if (mBuckets[iMid].itemsStartIndex > position) {
-                iMax = iMid - 1;
-            } else {
-                bucketNumber = iMid;
-                break;
-            }
-        }
-        if (mBuckets.length == 0 || mUnifiedLookupIndex.length == 0) {
-            return -1;
-        }
-        int mappedPos = mBuckets[bucketNumber].unifiedStartIndex
-                + position - mBuckets[bucketNumber].itemsStartIndex;
-        if (order == SortOrder.Descending) {
-            mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos;
-        }
-        return mappedPos;
-    }
-
-    public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
-        if (mProgress != Progress.Finished) return -1;
-        if(order == SortOrder.Ascending) {
-            DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
-            if (bucket.unifiedStartIndex == position) position++;
-            return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
-        } else {
-            int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
-            if (mBuckets.length == 0 || mUnifiedLookupIndex.length == 0) {
-                return -1;
-            }
-            DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
-            if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--;
-            return mMtpObjects.length - 1 - bucket.itemsStartIndex
-                    - zeroIndex + bucket.unifiedStartIndex;
-        }
-    }
-
-    /**
-     * @return The number of MTP items in the index (without labels)
-     */
-    public int sizeWithoutLabels() {
-        return mProgress == Progress.Finished ? mMtpObjects.length : 0;
-    }
-
-    public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
-        if (order == SortOrder.Ascending) {
-            return mBuckets[bucketNumber].unifiedStartIndex;
-        } else {
-            return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1;
-        }
-    }
-
-    public int getBucketNumberForPosition(int position, SortOrder order) {
-        if (order == SortOrder.Ascending) {
-            return mUnifiedLookupIndex[position];
-        } else {
-            return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position];
-        }
-    }
-
-    public boolean isFirstInBucket(int position, SortOrder order) {
-        if (order == SortOrder.Ascending) {
-            return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position;
-        } else {
-            position = mUnifiedLookupIndex.length - 1 - position;
-            return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position;
-        }
-    }
-
-    private Object[] mCachedReverseBuckets;
-
-    public Object[] getBuckets(SortOrder order) {
-        if (mBuckets == null) return null;
-        if (order == SortOrder.Ascending) {
-            return mBuckets;
-        } else {
-            if (mCachedReverseBuckets == null) {
-                computeReversedBuckets();
-            }
-            return mCachedReverseBuckets;
-        }
-    }
-
-    /*
-     * See the comments for buildLookupIndex for notes on the specific fields of
-     * this class.
-     */
-    private class DateBucket implements Comparable<DateBucket> {
-        SimpleDate bucket;
-        List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>();
-        int unifiedStartIndex;
-        int unifiedEndIndex;
-        int itemsStartIndex;
-        int numItems;
-
-        public DateBucket(SimpleDate bucket) {
-            this.bucket = bucket;
-        }
-
-        public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) {
-            this(bucket);
-            tempElementsList.add(firstElement);
-        }
-
-        void sortElements(Comparator<MtpObjectInfo> comparator) {
-            Collections.sort(tempElementsList, comparator);
-        }
-
-        @Override
-        public String toString() {
-            return bucket.toString();
-        }
-
-        @Override
-        public int hashCode() {
-            return bucket.hashCode();
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) return true;
-            if (obj == null) return false;
-            if (!(obj instanceof DateBucket)) return false;
-            DateBucket other = (DateBucket) obj;
-            if (bucket == null) {
-                if (other.bucket != null) return false;
-            } else if (!bucket.equals(other.bucket)) {
-                return false;
-            }
-            return true;
-        }
-
-        @Override
-        public int compareTo(DateBucket another) {
-            return this.bucket.compareTo(another.bucket);
-        }
-    }
-
-    /**
-     * Comparator to sort MtpObjectInfo objects by date created.
-     */
-    private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> {
-        @Override
-        public int compare(MtpObjectInfo o1, MtpObjectInfo o2) {
-            long diff = o1.getDateCreated() - o2.getDateCreated();
-            if (diff < 0) {
-                return -1;
-            } else if (diff == 0) {
-                return 0;
-            } else {
-                return 1;
-            }
-        }
-    }
-
-    private void resetState() {
-        mGeneration++;
-        mUnifiedLookupIndex = null;
-        mMtpObjects = null;
-        mBuckets = null;
-        mCachedReverseBuckets = null;
-        mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
-    }
-
-
-    private class IndexRunnable implements Runnable {
-        private int[] mUnifiedLookupIndex;
-        private MtpObjectInfo[] mMtpObjects;
-        private DateBucket[] mBuckets;
-        private Map<SimpleDate, DateBucket> mBucketsTemp;
-        private MtpDevice mDevice;
-        private int mNumObjects = 0;
-
-        private class IndexingException extends Exception {};
-
-        public IndexRunnable(MtpDevice device) {
-            mDevice = device;
-        }
-
-        /*
-         * Implementation note: this is the way the index supports a lot of its operations in
-         * constant time and respecting the need to have bucket names always come before items
-         * in that bucket when accessing the list sequentially, both in ascending and descending
-         * orders.
-         *
-         * Let's say the data we have in the index is the following:
-         *  [Bucket A]: [photo 1], [photo 2]
-         *  [Bucket B]: [photo 3]
-         *
-         *  In this case, the lookup index array would be
-         *  [0, 0, 0, 1, 1]
-         *
-         *  Now, whether we access the list in ascending or descending order, we know which bucket
-         *  to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first
-         *  item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex
-         *  that correspond to indices in this lookup index array, allowing us to calculate the
-         *  offset of the specific item we want from within a specific bucket.
-         */
-        private void buildLookupIndex() {
-            int numBuckets = mBuckets.length;
-            mUnifiedLookupIndex = new int[mNumObjects + numBuckets];
-            int currentUnifiedIndexEntry = 0;
-            int nextUnifiedEntry;
-
-            mMtpObjects = new MtpObjectInfo[mNumObjects];
-            int currentItemsEntry = 0;
-            for (int i = 0; i < numBuckets; i++) {
-                DateBucket bucket = mBuckets[i];
-                nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1;
-                Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
-                bucket.unifiedStartIndex = currentUnifiedIndexEntry;
-                bucket.unifiedEndIndex = nextUnifiedEntry - 1;
-                currentUnifiedIndexEntry = nextUnifiedEntry;
-
-                bucket.itemsStartIndex = currentItemsEntry;
-                bucket.numItems = bucket.tempElementsList.size();
-                for (int j = 0; j < bucket.numItems; j++) {
-                    mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
-                    currentItemsEntry++;
-                }
-                bucket.tempElementsList = null;
-            }
-        }
-
-        private void copyResults() {
-            MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex;
-            MtpDeviceIndex.this.mMtpObjects = mMtpObjects;
-            MtpDeviceIndex.this.mBuckets = mBuckets;
-            mUnifiedLookupIndex = null;
-            mMtpObjects = null;
-            mBuckets = null;
-        }
-
-        @Override
-        public void run() {
-            try {
-                indexDevice();
-            } catch (IndexingException e) {
-                synchronized (MtpDeviceIndex.this) {
-                    resetState();
-                    if (mProgressListener != null) {
-                        mProgressListener.onIndexFinish();
-                    }
-                }
-            }
-        }
-
-        private void indexDevice() throws IndexingException {
-            synchronized (MtpDeviceIndex.this) {
-                mProgress = Progress.Started;
-            }
-            mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
-            for (int storageId : mDevice.getStorageIds()) {
-                if (mDevice != getDevice()) throw new IndexingException();
-                Stack<Integer> pendingDirectories = new Stack<Integer>();
-                pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
-                while (!pendingDirectories.isEmpty()) {
-                    if (mDevice != getDevice()) throw new IndexingException();
-                    int dirHandle = pendingDirectories.pop();
-                    for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
-                        MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
-                        if (objectInfo == null) throw new IndexingException();
-                        int format = objectInfo.getFormat();
-                        if (format == MtpConstants.FORMAT_ASSOCIATION) {
-                            pendingDirectories.add(objectHandle);
-                        } else if (SUPPORTED_IMAGE_FORMATS.contains(format)
-                                || SUPPORTED_VIDEO_FORMATS.contains(format)) {
-                            addObject(objectInfo);
-                        }
-                    }
-                }
-            }
-            Collection<DateBucket> values = mBucketsTemp.values();
-            mBucketsTemp = null;
-            mBuckets = values.toArray(new DateBucket[values.size()]);
-            values = null;
-            synchronized (MtpDeviceIndex.this) {
-                mProgress = Progress.Sorting;
-                if (mProgressListener != null) {
-                    mProgressListener.onSorting();
-                }
-            }
-            sortAll();
-            buildLookupIndex();
-            synchronized (MtpDeviceIndex.this) {
-                if (mDevice != getDevice()) throw new IndexingException();
-                copyResults();
-
-                /*
-                 * In order for getBuckets to operate in constant time for descending
-                 * order, we must precompute a reversed array of the buckets, mainly
-                 * because the android.widget.SectionIndexer interface which adapters
-                 * that call getBuckets implement depends on section numbers to be
-                 * ascending relative to the scroll position, so we must have this for
-                 * descending order or the scrollbar goes crazy.
-                 */
-                computeReversedBuckets();
-
-                mProgress = Progress.Finished;
-                if (mProgressListener != null) {
-                    mProgressListener.onIndexFinish();
-                }
-            }
-        }
-
-        private SimpleDate mDateInstance = new SimpleDate();
-
-        private void addObject(MtpObjectInfo objectInfo) {
-            mNumObjects++;
-            mDateInstance.setTimestamp(objectInfo.getDateCreated());
-            DateBucket bucket = mBucketsTemp.get(mDateInstance);
-            if (bucket == null) {
-                bucket = new DateBucket(mDateInstance, objectInfo);
-                mBucketsTemp.put(mDateInstance, bucket);
-                mDateInstance = new SimpleDate(); // only create new date
-                                                  // objects when they are used
-                return;
-            } else {
-                bucket.tempElementsList.add(objectInfo);
-            }
-            if (mProgressListener != null) {
-                mProgressListener.onObjectIndexed(objectInfo, mNumObjects);
-            }
-        }
-
-        private void sortAll() {
-            Arrays.sort(mBuckets);
-            for (DateBucket bucket : mBuckets) {
-                bucket.sortElements(sMtpObjectComparator);
-            }
-        }
-
-    }
-
-    private void computeReversedBuckets() {
-        mCachedReverseBuckets = new Object[mBuckets.length];
-        for (int i = 0; i < mCachedReverseBuckets.length; i++) {
-            mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i];
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/ingest/SimpleDate.java b/src/com/android/gallery3d/ingest/SimpleDate.java
deleted file mode 100644 (file)
index 05db2cd..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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;
-    }
-}
index 6783f23..dc8723f 100644 (file)
 
 package com.android.gallery3d.ingest.adapter;
 
+import android.annotation.TargetApi;
+import android.os.Build;
+
 import java.util.ArrayList;
 import java.util.Collection;
 
+/**
+ * Helper to keep checked state in sync.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
 public abstract class CheckBroker {
-    private Collection<OnCheckedChangedListener> mListeners =
-            new ArrayList<OnCheckedChangedListener>();
+  private Collection<OnCheckedChangedListener> mListeners =
+      new ArrayList<OnCheckedChangedListener>();
 
-    public interface OnCheckedChangedListener {
-        public void onCheckedChanged(int position, boolean isChecked);
-        public void onBulkCheckedChanged();
-    }
+  /**
+   * Listener for item checked state changes.
+   */
+  public interface OnCheckedChangedListener {
+    public void onCheckedChanged(int position, boolean isChecked);
 
-    public abstract void setItemChecked(int position, boolean checked);
+    public void onBulkCheckedChanged();
+  }
 
-    public void onCheckedChange(int position, boolean checked) {
-        if (isItemChecked(position) != checked) {
-            for (OnCheckedChangedListener l : mListeners) {
-                l.onCheckedChanged(position, checked);
-            }
-        }
+  public abstract void setItemChecked(int position, boolean checked);
+
+  public void onCheckedChange(int position, boolean checked) {
+    if (isItemChecked(position) != checked) {
+      for (OnCheckedChangedListener l : mListeners) {
+        l.onCheckedChanged(position, checked);
+      }
     }
+  }
 
-    public void onBulkCheckedChange() {
-        for (OnCheckedChangedListener l : mListeners) {
-            l.onBulkCheckedChanged();
-        }
+  public void onBulkCheckedChange() {
+    for (OnCheckedChangedListener l : mListeners) {
+      l.onBulkCheckedChanged();
     }
+  }
 
-    public abstract boolean isItemChecked(int position);
+  public abstract boolean isItemChecked(int position);
 
-    public void registerOnCheckedChangeListener(OnCheckedChangedListener l) {
-        mListeners.add(l);
-    }
+  public void registerOnCheckedChangeListener(OnCheckedChangedListener l) {
+    mListeners.add(l);
+  }
 
-    public void unregisterOnCheckedChangeListener(OnCheckedChangedListener l) {
-        mListeners.remove(l);
-    }
+  public void unregisterOnCheckedChangeListener(OnCheckedChangedListener l) {
+    mListeners.remove(l);
+  }
 }
index e8dd69f..c3ce59f 100644 (file)
 
 package com.android.gallery3d.ingest.adapter;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.IngestObjectInfo;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.data.SimpleDate;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.MtpThumbnailTileView;
+
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
-import android.mtp.MtpObjectInfo;
+import android.os.Build;
 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;
-
+/**
+ * Adapter for MTP thumbnail grid.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
 public class MtpAdapter extends BaseAdapter implements SectionIndexer {
-    public static final int ITEM_TYPE_MEDIA = 0;
-    public static final int ITEM_TYPE_BUCKET = 1;
-
-    private Context mContext;
-    private MtpDeviceIndex mModel;
-    private SortOrder mSortOrder = SortOrder.Descending;
-    private LayoutInflater mInflater;
-    private int mGeneration = 0;
-
-    public MtpAdapter(Activity context) {
-        super();
-        mContext = context;
-        mInflater = LayoutInflater.from(context);
-    }
-
-    public void setMtpDeviceIndex(MtpDeviceIndex index) {
-        mModel = index;
-        notifyDataSetChanged();
-    }
-
-    public MtpDeviceIndex getMtpDeviceIndex() {
-        return mModel;
-    }
-
-    @Override
-    public void notifyDataSetChanged() {
-        mGeneration++;
-        super.notifyDataSetChanged();
-    }
-
-    @Override
-    public void notifyDataSetInvalidated() {
-        mGeneration++;
-        super.notifyDataSetInvalidated();
-    }
-
-    public boolean deviceConnected() {
-        return (mModel != null) && (mModel.getDevice() != null);
-    }
-
-    public boolean indexReady() {
-        return (mModel != null) && mModel.indexReady();
-    }
-
-    @Override
-    public int getCount() {
-        return mModel != null ? mModel.size() : 0;
-    }
-
-    @Override
-    public Object getItem(int position) {
-        return mModel.get(position, mSortOrder);
-    }
-
-    @Override
-    public boolean areAllItemsEnabled() {
-        return true;
-    }
-
-    @Override
-    public boolean isEnabled(int position) {
-        return true;
-    }
-
-    @Override
-    public long getItemId(int position) {
-        return position;
-    }
-
-    @Override
-    public int getViewTypeCount() {
-        return 2;
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        // If the position is the first in its section, then it corresponds to
-        // a title tile, if not it's a media tile
-        if (position == getPositionForSection(getSectionForPosition(position))) {
-            return ITEM_TYPE_BUCKET;
-        } else {
-            return ITEM_TYPE_MEDIA;
-        }
-    }
-
-    public boolean itemAtPositionIsBucket(int position) {
-        return getItemViewType(position) == ITEM_TYPE_BUCKET;
-    }
-
-    public boolean itemAtPositionIsMedia(int position) {
-        return getItemViewType(position) == ITEM_TYPE_MEDIA;
-    }
-
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        int type = getItemViewType(position);
-        if (type == ITEM_TYPE_MEDIA) {
-            MtpThumbnailTileView imageView;
-            if (convertView == null) {
-                imageView = (MtpThumbnailTileView) mInflater.inflate(
-                        R.layout.ingest_thumbnail, parent, false);
-            } else {
-                imageView = (MtpThumbnailTileView) convertView;
-            }
-            imageView.setMtpDeviceAndObjectInfo(mModel.getDevice(), (MtpObjectInfo)getItem(position), mGeneration);
-            return imageView;
-        } else {
-            DateTileView dateTile;
-            if (convertView == null) {
-                dateTile = (DateTileView) mInflater.inflate(
-                        R.layout.ingest_date_tile, parent, false);
-            } else {
-                dateTile = (DateTileView) convertView;
-            }
-            dateTile.setDate((SimpleDate)getItem(position));
-            return dateTile;
-        }
-    }
-
-    @Override
-    public int getPositionForSection(int section) {
-        if (getCount() == 0) {
-            return 0;
-        }
-        int numSections = getSections().length;
-        if (section >= numSections) {
-            section = numSections - 1;
-        }
-        return mModel.getFirstPositionForBucketNumber(section, mSortOrder);
-    }
-
-    @Override
-    public int getSectionForPosition(int position) {
-        int count = getCount();
-        if (count == 0) {
-            return 0;
-        }
-        if (position >= count) {
-            position = count - 1;
-        }
-        return mModel.getBucketNumberForPosition(position, mSortOrder);
-    }
-
-    @Override
-    public Object[] getSections() {
-        return getCount() > 0 ? mModel.getBuckets(mSortOrder) : null;
-    }
-
-    public SortOrder getSortOrder() {
-        return mSortOrder;
-    }
-
-    public int translatePositionWithoutLabels(int position) {
-        if (mModel == null) return -1;
-        return mModel.getPositionFromPositionWithoutLabels(position, mSortOrder);
-    }
+  public static final int ITEM_TYPE_MEDIA = 0;
+  public static final int ITEM_TYPE_BUCKET = 1;
+
+  @SuppressWarnings("unused")
+  private Context mContext;
+  private MtpDeviceIndex mModel;
+  private SortOrder mSortOrder = SortOrder.DESCENDING;
+  private LayoutInflater mInflater;
+  private int mGeneration = 0;
+
+  public MtpAdapter(Activity context) {
+    super();
+    mContext = context;
+    mInflater = LayoutInflater.from(context);
+  }
+
+  public void setMtpDeviceIndex(MtpDeviceIndex index) {
+    mModel = index;
+    notifyDataSetChanged();
+  }
+
+  public MtpDeviceIndex getMtpDeviceIndex() {
+    return mModel;
+  }
+
+  @Override
+  public void notifyDataSetChanged() {
+    mGeneration++;
+    super.notifyDataSetChanged();
+  }
+
+  @Override
+  public void notifyDataSetInvalidated() {
+    mGeneration++;
+    super.notifyDataSetInvalidated();
+  }
+
+  public boolean deviceConnected() {
+    return (mModel != null) && mModel.isDeviceConnected();
+  }
+
+  public boolean indexReady() {
+    return (mModel != null) && mModel.isIndexReady();
+  }
+
+  @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(),
+          (IngestObjectInfo) getItem(position), mGeneration);
+      return imageView;
+    } else {
+      DateTileView dateTile;
+      if (convertView == null) {
+        dateTile = (DateTileView) mInflater.inflate(
+            R.layout.ingest_date_tile, parent, false);
+      } else {
+        dateTile = (DateTileView) convertView;
+      }
+      dateTile.setDate((SimpleDate) getItem(position));
+      return dateTile;
+    }
+  }
+
+  @Override
+  public int getPositionForSection(int section) {
+    if (getCount() == 0) {
+      return 0;
+    }
+    int numSections = getSections().length;
+    if (section >= numSections) {
+      section = numSections - 1;
+    }
+    return mModel.getFirstPositionForBucketNumber(section, mSortOrder);
+  }
+
+  @Override
+  public int getSectionForPosition(int position) {
+    int count = getCount();
+    if (count == 0) {
+      return 0;
+    }
+    if (position >= count) {
+      position = count - 1;
+    }
+    return mModel.getBucketNumberForPosition(position, mSortOrder);
+  }
+
+  @Override
+  public Object[] getSections() {
+    return getCount() > 0 ? mModel.getBuckets(mSortOrder) : null;
+  }
+
+  public SortOrder getSortOrder() {
+    return mSortOrder;
+  }
+
+  public int translatePositionWithoutLabels(int position) {
+    if (mModel == null) {
+      return -1;
+    }
+    return mModel.getPositionFromPositionWithoutLabels(position, mSortOrder);
+  }
 }
index 9e7abc0..9fe650c 100644 (file)
 
 package com.android.gallery3d.ingest.adapter;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.IngestObjectInfo;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.ui.MtpFullscreenView;
+
+import android.annotation.TargetApi;
 import android.content.Context;
-import android.mtp.MtpObjectInfo;
+import android.os.Build;
 import android.support.v4.view.PagerAdapter;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.MtpDeviceIndex;
-import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
-import com.android.gallery3d.ingest.ui.MtpFullscreenView;
-
+/**
+ * Adapter for full-screen MTP pager.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
 public class MtpPagerAdapter extends PagerAdapter {
 
-    private LayoutInflater mInflater;
-    private int mGeneration = 0;
-    private CheckBroker mBroker;
-    private MtpDeviceIndex mModel;
-    private SortOrder mSortOrder = SortOrder.Descending;
+  private LayoutInflater mInflater;
+  private int mGeneration = 0;
+  private CheckBroker mBroker;
+  private MtpDeviceIndex mModel;
+  private SortOrder mSortOrder = SortOrder.DESCENDING;
 
-    private MtpFullscreenView mReusableView = null;
+  private MtpFullscreenView mReusableView = null;
 
-    public MtpPagerAdapter(Context context, CheckBroker broker) {
-        super();
-        mInflater = LayoutInflater.from(context);
-        mBroker = broker;
-    }
+  public MtpPagerAdapter(Context context, CheckBroker broker) {
+    super();
+    mInflater = LayoutInflater.from(context);
+    mBroker = broker;
+  }
 
-    public void setMtpDeviceIndex(MtpDeviceIndex index) {
-        mModel = index;
-        notifyDataSetChanged();
-    }
+  public void setMtpDeviceIndex(MtpDeviceIndex index) {
+    mModel = index;
+    notifyDataSetChanged();
+  }
 
-    @Override
-    public int getCount() {
-        return mModel != null ? mModel.sizeWithoutLabels() : 0;
-    }
+  @Override
+  public int getCount() {
+    return mModel != null ? mModel.sizeWithoutLabels() : 0;
+  }
 
-    @Override
-    public void notifyDataSetChanged() {
-        mGeneration++;
-        super.notifyDataSetChanged();
-    }
+  @Override
+  public void notifyDataSetChanged() {
+    mGeneration++;
+    super.notifyDataSetChanged();
+  }
 
-    public int translatePositionWithLabels(int position) {
-        if (mModel == null) return -1;
-        return mModel.getPositionWithoutLabelsFromPosition(position, mSortOrder);
+  public int translatePositionWithLabels(int position) {
+    if (mModel == null) {
+      return -1;
     }
+    return mModel.getPositionWithoutLabelsFromPosition(position, mSortOrder);
+  }
 
-    @Override
-    public void finishUpdate(ViewGroup container) {
-        mReusableView = null;
-        super.finishUpdate(container);
-    }
+  @Override
+  public void finishUpdate(ViewGroup container) {
+    mReusableView = null;
+    super.finishUpdate(container);
+  }
 
-    @Override
-    public boolean isViewFromObject(View view, Object object) {
-        return view == object;
-    }
+  @Override
+  public boolean isViewFromObject(View view, Object object) {
+    return view == object;
+  }
 
-    @Override
-    public void destroyItem(ViewGroup container, int position, Object object) {
-        MtpFullscreenView v = (MtpFullscreenView)object;
-        container.removeView(v);
-        mBroker.unregisterOnCheckedChangeListener(v);
-        mReusableView = v;
-    }
+  @Override
+  public void destroyItem(ViewGroup container, int position, Object object) {
+    MtpFullscreenView v = (MtpFullscreenView) object;
+    container.removeView(v);
+    mBroker.unregisterOnCheckedChangeListener(v);
+    mReusableView = v;
+  }
 
-    @Override
-    public Object instantiateItem(ViewGroup container, int position) {
-        MtpFullscreenView v;
-        if (mReusableView != null) {
-            v = mReusableView;
-            mReusableView = null;
-        } else {
-            v = (MtpFullscreenView) mInflater.inflate(R.layout.ingest_fullsize, container, false);
-        }
-        MtpObjectInfo i = mModel.getWithoutLabels(position, mSortOrder);
-        v.getImageView().setMtpDeviceAndObjectInfo(mModel.getDevice(), i, mGeneration);
-        v.setPositionAndBroker(position, mBroker);
-        container.addView(v);
-        return v;
+  @Override
+  public Object instantiateItem(ViewGroup container, int position) {
+    MtpFullscreenView v;
+    if (mReusableView != null) {
+      v = mReusableView;
+      mReusableView = null;
+    } else {
+      v = (MtpFullscreenView) mInflater.inflate(R.layout.ingest_fullsize, container, false);
     }
+    IngestObjectInfo i = mModel.getWithoutLabels(position, mSortOrder);
+    v.getImageView().setMtpDeviceAndObjectInfo(mModel.getDevice(), i, mGeneration);
+    v.setPositionAndBroker(position, mBroker);
+    container.addView(v);
+    return v;
+  }
 }
index bbc90f6..c436fa7 100644 (file)
 
 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 (file)
index 0000000..85eedb3
--- /dev/null
@@ -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 (file)
index 0000000..ee2a7d0
--- /dev/null
@@ -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 (file)
index 0000000..2527383
--- /dev/null
@@ -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;
+  }
+}
index c6504a5..3295828 100644 (file)
 
 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 (file)
index 0000000..cc6c9ce
--- /dev/null
@@ -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 (file)
index 0000000..b21ad83
--- /dev/null
@@ -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 (file)
index 0000000..3227589
--- /dev/null
@@ -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 (file)
index 0000000..2476f80
--- /dev/null
@@ -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;
+  }
+}
index 52fe9b8..cd31e82 100644 (file)
 
 package com.android.gallery3d.ingest.ui;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.SimpleDate;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.SimpleDate;
 
 import java.text.DateFormatSymbols;
 import java.util.Locale;
 
+/**
+ * Displays a date in a square tile.
+ */
 public class DateTileView extends FrameLayout {
-    private static String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths();
-    private static Locale sLocale;
+  private static String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths();
+  private static Locale sLocale;
 
-    static {
-        refreshLocale();
-    }
+  static {
+    refreshLocale();
+  }
 
-    public static boolean refreshLocale() {
-        Locale currentLocale = Locale.getDefault();
-        if (!currentLocale.equals(sLocale)) {
-            sLocale = currentLocale;
-            sMonthNames = DateFormatSymbols.getInstance(sLocale).getShortMonths();
-            return true;
-        } else {
-            return false;
-        }
+  public static boolean refreshLocale() {
+    Locale currentLocale = Locale.getDefault();
+    if (!currentLocale.equals(sLocale)) {
+      sLocale = currentLocale;
+      sMonthNames = DateFormatSymbols.getInstance(sLocale).getShortMonths();
+      return true;
+    } else {
+      return false;
     }
+  }
 
-    private TextView mDateTextView;
-    private TextView mMonthTextView;
-    private TextView mYearTextView;
-    private int mMonth = -1;
-    private int mYear = -1;
-    private int mDate = -1;
-    private String[] mMonthNames = sMonthNames;
+  private TextView mDateTextView;
+  private TextView mMonthTextView;
+  private TextView mYearTextView;
+  private int mMonth = -1;
+  private int mYear = -1;
+  private int mDate = -1;
+  private String[] mMonthNames = sMonthNames;
 
-    public DateTileView(Context context) {
-        super(context);
-    }
+  public DateTileView(Context context) {
+    super(context);
+  }
 
-    public DateTileView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
+  public DateTileView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+  }
 
-    public DateTileView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
+  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
+  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);
-    }
+  @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(SimpleDate date) {
+    setDate(date.getDay(), date.getMonth(), date.getYear());
+  }
 
-    public void setDate(int date, int month, int year) {
-        if (date != mDate) {
-            mDate = date;
-            mDateTextView.setText(mDate > 9 ? Integer.toString(mDate) : "0" + mDate);
-        }
-        if (mMonthNames != sMonthNames) {
-            mMonthNames = sMonthNames;
-            if (month == mMonth) {
-                mMonthTextView.setText(mMonthNames[mMonth]);
-            }
-        }
-        if (month != mMonth) {
-            mMonth = month;
-            mMonthTextView.setText(mMonthNames[mMonth]);
-        }
-        if (year != mYear) {
-            mYear = year;
-            mYearTextView.setText(Integer.toString(mYear));
-        }
+  public void setDate(int date, int month, int year) {
+    if (date != mDate) {
+      mDate = date;
+      mDateTextView.setText(mDate > 9 ? Integer.toString(mDate) : "0" + mDate);
+    }
+    if (mMonthNames != sMonthNames) {
+      mMonthNames = sMonthNames;
+      if (month == mMonth) {
+        mMonthTextView.setText(mMonthNames[mMonth]);
+      }
+    }
+    if (month != mMonth) {
+      mMonth = month;
+      mMonthTextView.setText(mMonthNames[mMonth]);
+    }
+    if (year != mYear) {
+      mYear = year;
+      mYearTextView.setText(Integer.toString(mYear));
     }
+  }
 }
index c821259..7bafa7c 100644 (file)
@@ -21,38 +21,40 @@ import android.util.AttributeSet;
 import android.widget.GridView;
 
 /**
- * This just extends GridView with the ability to listen for calls
- * to clearChoices()
+ * Extends GridView with the ability to listen for calls to clearChoices()
  */
 public class IngestGridView extends GridView {
 
-    public interface OnClearChoicesListener {
-        public void onClearChoices();
-    }
+  /**
+   * Listener for all checked choices being cleared.
+   */
+  public interface OnClearChoicesListener {
+    public void onClearChoices();
+  }
 
-    private OnClearChoicesListener mOnClearChoicesListener = null;
+  private OnClearChoicesListener mOnClearChoicesListener = null;
 
-    public IngestGridView(Context context) {
-        super(context);
-    }
+  public IngestGridView(Context context) {
+    super(context);
+  }
 
-    public IngestGridView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
+  public IngestGridView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+  }
 
-    public IngestGridView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
+  public IngestGridView(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+  }
 
-    public void setOnClearChoicesListener(OnClearChoicesListener l) {
-        mOnClearChoicesListener = l;
-    }
+  public void setOnClearChoicesListener(OnClearChoicesListener l) {
+    mOnClearChoicesListener = l;
+  }
 
-    @Override
-    public void clearChoices() {
-        super.clearChoices();
-        if (mOnClearChoicesListener != null) {
-            mOnClearChoicesListener.onClearChoices();
-        }
+  @Override
+  public void clearChoices() {
+    super.clearChoices();
+    if (mOnClearChoicesListener != null) {
+      mOnClearChoicesListener.onClearChoices();
     }
+  }
 }
index 8d3884d..00785dd 100644 (file)
 
 package com.android.gallery3d.ingest.ui;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.widget.CheckBox;
 import android.widget.Checkable;
 import android.widget.CompoundButton;
-import android.widget.CompoundButton.OnCheckedChangeListener;
 import android.widget.RelativeLayout;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.adapter.CheckBroker;
-
+/**
+ * View for displaying an MTP-image and associated controls full-screen
+ */
 public class MtpFullscreenView extends RelativeLayout implements Checkable,
     CompoundButton.OnCheckedChangeListener, CheckBroker.OnCheckedChangedListener {
 
-    private MtpImageView mImageView;
-    private CheckBox mCheckbox;
-    private int mPosition = -1;
-    private CheckBroker mBroker;
-
-    public MtpFullscreenView(Context context) {
-        super(context);
-    }
-
-    public MtpFullscreenView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public MtpFullscreenView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mImageView = (MtpImageView) findViewById(R.id.ingest_fullsize_image);
-        mCheckbox = (CheckBox) findViewById(R.id.ingest_fullsize_image_checkbox);
-        mCheckbox.setOnCheckedChangeListener(this);
+  private MtpImageView mImageView;
+  private CheckBox mCheckbox;
+  private int mPosition = -1;
+  private CheckBroker mBroker;
+
+  public MtpFullscreenView(Context context) {
+    super(context);
+  }
+
+  public MtpFullscreenView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+  }
+
+  public MtpFullscreenView(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+  }
+
+  @Override
+  protected void onFinishInflate() {
+    super.onFinishInflate();
+    mImageView = (MtpImageView) findViewById(R.id.ingest_fullsize_image);
+    mCheckbox = (CheckBox) findViewById(R.id.ingest_fullsize_image_checkbox);
+    mCheckbox.setOnCheckedChangeListener(this);
+  }
+
+  @Override
+  public boolean isChecked() {
+    return mCheckbox.isChecked();
+  }
+
+  @Override
+  public void setChecked(boolean checked) {
+    mCheckbox.setChecked(checked);
+  }
+
+  @Override
+  public void toggle() {
+    mCheckbox.toggle();
+  }
+
+  @Override
+  public void onDetachedFromWindow() {
+    setPositionAndBroker(-1, null);
+    super.onDetachedFromWindow();
+  }
+
+  public MtpImageView getImageView() {
+    return mImageView;
+  }
+
+  public int getPosition() {
+    return mPosition;
+  }
+
+  public void setPositionAndBroker(int position, CheckBroker b) {
+    if (mBroker != null) {
+      mBroker.unregisterOnCheckedChangeListener(this);
     }
-
-    @Override
-    public boolean isChecked() {
-        return mCheckbox.isChecked();
-    }
-
-    @Override
-    public void setChecked(boolean checked) {
-        mCheckbox.setChecked(checked);
-    }
-
-    @Override
-    public void toggle() {
-        mCheckbox.toggle();
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        setPositionAndBroker(-1, null);
-        super.onDetachedFromWindow();
-    }
-
-    public MtpImageView getImageView() {
-        return mImageView;
-    }
-
-    public int getPosition() {
-        return mPosition;
-    }
-
-    public void setPositionAndBroker(int position, CheckBroker b) {
-        if (mBroker != null) {
-            mBroker.unregisterOnCheckedChangeListener(this);
-        }
-        mPosition = position;
-        mBroker = b;
-        if (mBroker != null) {
-            setChecked(mBroker.isItemChecked(position));
-            mBroker.registerOnCheckedChangeListener(this);
-        }
+    mPosition = position;
+    mBroker = b;
+    if (mBroker != null) {
+      setChecked(mBroker.isItemChecked(position));
+      mBroker.registerOnCheckedChangeListener(this);
     }
+  }
 
-    @Override
-    public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
-        if (mBroker != null) mBroker.setItemChecked(mPosition, isChecked);
+  @Override
+  public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
+    if (mBroker != null) {
+      mBroker.setItemChecked(mPosition, isChecked);
     }
+  }
 
-    @Override
-    public void onCheckedChanged(int position, boolean isChecked) {
-        if (position == mPosition) {
-            setChecked(isChecked);
-        }
+  @Override
+  public void onCheckedChanged(int position, boolean isChecked) {
+    if (position == mPosition) {
+      setChecked(isChecked);
     }
+  }
 
-    @Override
-    public void onBulkCheckedChanged() {
-        if(mBroker != null) setChecked(mBroker.isItemChecked(mPosition));
+  @Override
+  public void onBulkCheckedChanged() {
+    if (mBroker != null) {
+      setChecked(mBroker.isItemChecked(mPosition));
     }
+  }
 }
index 80c1051..5362efd 100644 (file)
 
 package com.android.gallery3d.ingest.ui;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.BitmapWithMetadata;
+import com.android.gallery3d.ingest.data.IngestObjectInfo;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+import com.android.gallery3d.ingest.data.MtpDeviceIndex;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.drawable.Drawable;
 import android.mtp.MtpDevice;
-import android.mtp.MtpObjectInfo;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -29,252 +34,264 @@ import android.os.Message;
 import android.util.AttributeSet;
 import android.widget.ImageView;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.MtpDeviceIndex;
-import com.android.gallery3d.ingest.data.BitmapWithMetadata;
-import com.android.gallery3d.ingest.data.MtpBitmapFetch;
-
 import java.lang.ref.WeakReference;
 
+/**
+ * View for images from an MTP devices
+ */
 public class MtpImageView extends ImageView {
-    // We will use the thumbnail for images larger than this threshold
-    private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes
+  // We will use the thumbnail for images larger than this threshold
+  private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes
 
-    private int mObjectHandle;
-    private int mGeneration;
+  private int mObjectHandle;
+  private int mGeneration;
 
-    private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this);
-    private Object mFetchLock = new Object();
-    private boolean mFetchPending = false;
-    private MtpObjectInfo mFetchObjectInfo;
-    private MtpDevice mFetchDevice;
-    private Object mFetchResult;
-    private Drawable mOverlayIcon;
-    private boolean mShowOverlayIcon;
+  private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this);
+  private Object mFetchLock = new Object();
+  private boolean mFetchPending = false;
+  private IngestObjectInfo mFetchObjectInfo;
+  private MtpDevice mFetchDevice;
+  private Object mFetchResult;
+  private Drawable mOverlayIcon;
+  private boolean mShowOverlayIcon;
 
-    private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread();
-    private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler();
+  private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread();
+  private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler();
 
-    private void init() {
-         showPlaceholder();
-    }
+  private void init() {
+    showPlaceholder();
+  }
 
-    public MtpImageView(Context context) {
-        super(context);
-        init();
-    }
+  public MtpImageView(Context context) {
+    super(context);
+    init();
+  }
 
-    public MtpImageView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
-    }
+  public MtpImageView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    init();
+  }
 
-    public MtpImageView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        init();
-    }
+  public MtpImageView(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+    init();
+  }
 
-    private void showPlaceholder() {
-        setImageResource(android.R.color.transparent);
-    }
+  private void showPlaceholder() {
+    setImageResource(android.R.color.transparent);
+  }
 
-    public void setMtpDeviceAndObjectInfo(MtpDevice device, MtpObjectInfo object, int gen) {
-        int handle = object.getObjectHandle();
-        if (handle == mObjectHandle && gen == mGeneration) {
-            return;
-        }
-        cancelLoadingAndClear();
-        showPlaceholder();
-        mGeneration = gen;
-        mObjectHandle = handle;
-        mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat());
-        if (mShowOverlayIcon && mOverlayIcon == null) {
-            mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play);
-            updateOverlayIconBounds();
-        }
-        synchronized (mFetchLock) {
-            mFetchObjectInfo = object;
-            mFetchDevice = device;
-            if (mFetchPending) return;
-            mFetchPending = true;
-            sFetchHandler.sendMessage(
-                    sFetchHandler.obtainMessage(0, mWeakReference));
-        }
+  public void setMtpDeviceAndObjectInfo(MtpDevice device, IngestObjectInfo object, int gen) {
+    int handle = object.getObjectHandle();
+    if (handle == mObjectHandle && gen == mGeneration) {
+      return;
+    }
+    cancelLoadingAndClear();
+    showPlaceholder();
+    mGeneration = gen;
+    mObjectHandle = handle;
+    mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat());
+    if (mShowOverlayIcon && mOverlayIcon == null) {
+      mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play);
+      updateOverlayIconBounds();
     }
+    synchronized (mFetchLock) {
+      mFetchObjectInfo = object;
+      mFetchDevice = device;
+      if (mFetchPending) {
+        return;
+      }
+      mFetchPending = true;
+      sFetchHandler.sendMessage(
+          sFetchHandler.obtainMessage(0, mWeakReference));
+    }
+  }
 
-    protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
-        if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE
-                && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) {
-            return MtpBitmapFetch.getFullsize(device, info);
-        } else {
-            return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0);
-        }
+  protected Object fetchMtpImageDataFromDevice(MtpDevice device, IngestObjectInfo info) {
+    if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE
+        && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) {
+      return MtpBitmapFetch.getFullsize(device, info);
+    } else {
+      return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0);
     }
+  }
 
-    private float mLastBitmapWidth;
-    private float mLastBitmapHeight;
-    private int mLastRotationDegrees;
-    private Matrix mDrawMatrix = new Matrix();
+  private float mLastBitmapWidth;
+  private float mLastBitmapHeight;
+  private int mLastRotationDegrees;
+  private Matrix mDrawMatrix = new Matrix();
 
-    private void updateDrawMatrix() {
-        mDrawMatrix.reset();
-        float dwidth;
-        float dheight;
-        float vheight = getHeight();
-        float vwidth = getWidth();
-        float scale;
-        boolean rotated90 = (mLastRotationDegrees % 180 != 0);
-        if (rotated90) {
-            dwidth = mLastBitmapHeight;
-            dheight = mLastBitmapWidth;
-        } else {
-            dwidth = mLastBitmapWidth;
-            dheight = mLastBitmapHeight;
-        }
-        if (dwidth <= vwidth && dheight <= vheight) {
-            scale = 1.0f;
-        } else {
-            scale = Math.min(vwidth / dwidth, vheight / dheight);
-        }
-        mDrawMatrix.setScale(scale, scale);
-        if (rotated90) {
-            mDrawMatrix.postTranslate(-dheight * scale * 0.5f,
-                    -dwidth * scale * 0.5f);
-            mDrawMatrix.postRotate(mLastRotationDegrees);
-            mDrawMatrix.postTranslate(dwidth * scale * 0.5f,
-                    dheight * scale * 0.5f);
-        }
-        mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f,
-                (vheight - dheight * scale) * 0.5f);
-        if (!rotated90 && mLastRotationDegrees > 0) {
-            // rotated by a multiple of 180
-            mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2);
-        }
-        setImageMatrix(mDrawMatrix);
+  private void updateDrawMatrix() {
+    mDrawMatrix.reset();
+    float dwidth;
+    float dheight;
+    float vheight = getHeight();
+    float vwidth = getWidth();
+    float scale;
+    boolean rotated90 = (mLastRotationDegrees % 180 != 0);
+    if (rotated90) {
+      dwidth = mLastBitmapHeight;
+      dheight = mLastBitmapWidth;
+    } else {
+      dwidth = mLastBitmapWidth;
+      dheight = mLastBitmapHeight;
+    }
+    if (dwidth <= vwidth && dheight <= vheight) {
+      scale = 1.0f;
+    } else {
+      scale = Math.min(vwidth / dwidth, vheight / dheight);
+    }
+    mDrawMatrix.setScale(scale, scale);
+    if (rotated90) {
+      mDrawMatrix.postTranslate(-dheight * scale * 0.5f,
+          -dwidth * scale * 0.5f);
+      mDrawMatrix.postRotate(mLastRotationDegrees);
+      mDrawMatrix.postTranslate(dwidth * scale * 0.5f,
+          dheight * scale * 0.5f);
     }
+    mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f,
+        (vheight - dheight * scale) * 0.5f);
+    if (!rotated90 && mLastRotationDegrees > 0) {
+      // rotated by a multiple of 180
+      mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2);
+    }
+    setImageMatrix(mDrawMatrix);
+  }
 
-    private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4;
+  private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4;
 
-    private void updateOverlayIconBounds() {
-        int iheight = mOverlayIcon.getIntrinsicHeight();
-        int iwidth = mOverlayIcon.getIntrinsicWidth();
-        int vheight = getHeight();
-        int vwidth = getWidth();
-        float scale_height = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR);
-        float scale_width = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR);
-        if (scale_height >= 1f && scale_width >= 1f) {
-            mOverlayIcon.setBounds((vwidth - iwidth) / 2,
-                    (vheight - iheight) / 2,
-                    (vwidth + iwidth) / 2,
-                    (vheight + iheight) / 2);
-        } else {
-            float scale = Math.min(scale_height, scale_width);
-            mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2,
-                    (int) (vheight - scale * iheight) / 2,
-                    (int) (vwidth + scale * iwidth) / 2,
-                    (int) (vheight + scale * iheight) / 2);
-        }
+  private void updateOverlayIconBounds() {
+    int iheight = mOverlayIcon.getIntrinsicHeight();
+    int iwidth = mOverlayIcon.getIntrinsicWidth();
+    int vheight = getHeight();
+    int vwidth = getWidth();
+    float scaleHeight = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR);
+    float scaleWidth = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR);
+    if (scaleHeight >= 1f && scaleWidth >= 1f) {
+      mOverlayIcon.setBounds((vwidth - iwidth) / 2,
+          (vheight - iheight) / 2,
+          (vwidth + iwidth) / 2,
+          (vheight + iheight) / 2);
+    } else {
+      float scale = Math.min(scaleHeight, scaleWidth);
+      mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2,
+          (int) (vheight - scale * iheight) / 2,
+          (int) (vwidth + scale * iwidth) / 2,
+          (int) (vheight + scale * iheight) / 2);
     }
+  }
 
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        if (changed && getScaleType() == ScaleType.MATRIX) {
-            updateDrawMatrix();
-        }
-        if (mShowOverlayIcon && changed && mOverlayIcon != null) {
-            updateOverlayIconBounds();
-        }
+  @Override
+  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+    super.onLayout(changed, left, top, right, bottom);
+    if (changed && getScaleType() == ScaleType.MATRIX) {
+      updateDrawMatrix();
     }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-        if (mShowOverlayIcon && mOverlayIcon != null) {
-            mOverlayIcon.draw(canvas);
-        }
+    if (mShowOverlayIcon && changed && mOverlayIcon != null) {
+      updateOverlayIconBounds();
     }
+  }
 
-    protected void onMtpImageDataFetchedFromDevice(Object result) {
-        BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata)result;
-        if (getScaleType() == ScaleType.MATRIX) {
-            mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight();
-            mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth();
-            mLastRotationDegrees = bitmapWithMetadata.rotationDegrees;
-            updateDrawMatrix();
-        } else {
-            setRotation(bitmapWithMetadata.rotationDegrees);
-        }
-        setAlpha(0f);
-        setImageBitmap(bitmapWithMetadata.bitmap);
-        animate().alpha(1f);
+  @Override
+  protected void onDraw(Canvas canvas) {
+    super.onDraw(canvas);
+    if (mShowOverlayIcon && mOverlayIcon != null) {
+      mOverlayIcon.draw(canvas);
     }
+  }
 
-    protected void cancelLoadingAndClear() {
-        synchronized (mFetchLock) {
-            mFetchDevice = null;
-            mFetchObjectInfo = null;
-            mFetchResult = null;
-        }
-        animate().cancel();
-        setImageResource(android.R.color.transparent);
+  protected void onMtpImageDataFetchedFromDevice(Object result) {
+    BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata) result;
+    if (getScaleType() == ScaleType.MATRIX) {
+      mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight();
+      mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth();
+      mLastRotationDegrees = bitmapWithMetadata.rotationDegrees;
+      updateDrawMatrix();
+    } else {
+      setRotation(bitmapWithMetadata.rotationDegrees);
     }
+    setAlpha(0f);
+    setImageBitmap(bitmapWithMetadata.bitmap);
+    animate().alpha(1f);
+  }
 
-    @Override
-    public void onDetachedFromWindow() {
-        cancelLoadingAndClear();
-        super.onDetachedFromWindow();
+  protected void cancelLoadingAndClear() {
+    synchronized (mFetchLock) {
+      mFetchDevice = null;
+      mFetchObjectInfo = null;
+      mFetchResult = null;
     }
+    animate().cancel();
+    setImageResource(android.R.color.transparent);
+  }
 
-    private static class FetchImageHandler extends Handler {
-        public FetchImageHandler(Looper l) {
-            super(l);
-        }
+  @Override
+  public void onDetachedFromWindow() {
+    cancelLoadingAndClear();
+    super.onDetachedFromWindow();
+  }
 
-        public static FetchImageHandler createOnNewThread() {
-            HandlerThread t = new HandlerThread("MtpImageView Fetch");
-            t.start();
-            return new FetchImageHandler(t.getLooper());
-        }
+  private static class FetchImageHandler extends Handler {
+    public FetchImageHandler(Looper l) {
+      super(l);
+    }
 
-        @Override
-        public void handleMessage(Message msg) {
-            @SuppressWarnings("unchecked")
-            MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
-            if (parent == null) return;
-            MtpObjectInfo objectInfo;
-            MtpDevice device;
-            synchronized (parent.mFetchLock) {
-                parent.mFetchPending = false;
-                device = parent.mFetchDevice;
-                objectInfo = parent.mFetchObjectInfo;
-            }
-            if (device == null) return;
-            Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo);
-            if (result == null) return;
-            synchronized (parent.mFetchLock) {
-                if (parent.mFetchObjectInfo != objectInfo) return;
-                parent.mFetchResult = result;
-                parent.mFetchDevice = null;
-                parent.mFetchObjectInfo = null;
-                sFetchCompleteHandler.sendMessage(
-                        sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference));
-            }
-        }
+    public static FetchImageHandler createOnNewThread() {
+      HandlerThread t = new HandlerThread("MtpImageView Fetch");
+      t.start();
+      return new FetchImageHandler(t.getLooper());
     }
 
-    private static class ShowImageHandler extends Handler {
-        @Override
-        public void handleMessage(Message msg) {
-            @SuppressWarnings("unchecked")
-            MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
-            if (parent == null) return;
-            Object result;
-            synchronized (parent.mFetchLock) {
-                result = parent.mFetchResult;
-            }
-            if (result == null) return;
-            parent.onMtpImageDataFetchedFromDevice(result);
+    @Override
+    public void handleMessage(Message msg) {
+      @SuppressWarnings("unchecked")
+      MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+      if (parent == null) {
+        return;
+      }
+      IngestObjectInfo objectInfo;
+      MtpDevice device;
+      synchronized (parent.mFetchLock) {
+        parent.mFetchPending = false;
+        device = parent.mFetchDevice;
+        objectInfo = parent.mFetchObjectInfo;
+      }
+      if (device == null) {
+        return;
+      }
+      Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo);
+      if (result == null) {
+        return;
+      }
+      synchronized (parent.mFetchLock) {
+        if (parent.mFetchObjectInfo != objectInfo) {
+          return;
         }
+        parent.mFetchResult = result;
+        parent.mFetchDevice = null;
+        parent.mFetchObjectInfo = null;
+        sFetchCompleteHandler.sendMessage(
+            sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference));
+      }
+    }
+  }
+
+  private static class ShowImageHandler extends Handler {
+    @Override
+    public void handleMessage(Message msg) {
+      @SuppressWarnings("unchecked")
+      MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+      if (parent == null) {
+        return;
+      }
+      Object result;
+      synchronized (parent.mFetchLock) {
+        result = parent.mFetchResult;
+      }
+      if (result == null) {
+        return;
+      }
+      parent.onMtpImageDataFetchedFromDevice(result);
     }
+  }
 }
index 3307e78..844a750 100644 (file)
 
 package com.android.gallery3d.ingest.ui;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.IngestObjectInfo;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.mtp.MtpDevice;
-import android.mtp.MtpObjectInfo;
 import android.util.AttributeSet;
 import android.widget.Checkable;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.data.MtpBitmapFetch;
-
 
+/**
+ * View for thumbnail images from an MTP device
+ */
 public class MtpThumbnailTileView extends MtpImageView implements Checkable {
 
-    private Paint mForegroundPaint;
-    private boolean mIsChecked;
-    private Bitmap mBitmap;
-
-    private void init() {
-        mForegroundPaint = new Paint();
-        mForegroundPaint.setColor(getResources().getColor(R.color.ingest_highlight_semitransparent));
-    }
-
-    public MtpThumbnailTileView(Context context) {
-        super(context);
-        init();
-    }
-
-    public MtpThumbnailTileView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
-    }
-
-    public MtpThumbnailTileView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        init();
+  private Paint mForegroundPaint;
+  private boolean mIsChecked;
+  private Bitmap mBitmap;
+
+  private void init() {
+    mForegroundPaint = new Paint();
+    mForegroundPaint.setColor(
+        getResources().getColor(R.color.ingest_highlight_semitransparent));
+  }
+
+  public MtpThumbnailTileView(Context context) {
+    super(context);
+    init();
+  }
+
+  public MtpThumbnailTileView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    init();
+  }
+
+  public MtpThumbnailTileView(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+    init();
+  }
+
+  @Override
+  public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+    // Force this to be square
+    super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+  }
+
+  @Override
+  protected Object fetchMtpImageDataFromDevice(MtpDevice device, IngestObjectInfo info) {
+    return MtpBitmapFetch.getThumbnail(device, info);
+  }
+
+  @Override
+  protected void onMtpImageDataFetchedFromDevice(Object result) {
+    mBitmap = (Bitmap) result;
+    setImageBitmap(mBitmap);
+  }
+
+  @Override
+  public void draw(Canvas canvas) {
+    super.draw(canvas);
+    if (isChecked()) {
+      canvas.drawRect(canvas.getClipBounds(), mForegroundPaint);
     }
-
-    @Override
-    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        // Force this to be square
-        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
-    }
-
-    @Override
-    protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
-        return MtpBitmapFetch.getThumbnail(device, info);
-    }
-
-    @Override
-    protected void onMtpImageDataFetchedFromDevice(Object result) {
-        mBitmap = (Bitmap)result;
-        setImageBitmap(mBitmap);
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        super.draw(canvas);
-        if (isChecked()) {
-            canvas.drawRect(canvas.getClipBounds(), mForegroundPaint);
-        }
+  }
+
+  @Override
+  public boolean isChecked() {
+    return mIsChecked;
+  }
+
+  @Override
+  public void setChecked(boolean checked) {
+    if (mIsChecked != checked) {
+      mIsChecked = checked;
+      invalidate();
     }
-
-    @Override
-    public boolean isChecked() {
-        return mIsChecked;
-    }
-
-    @Override
-    public void setChecked(boolean checked) {
-        mIsChecked = checked;
-    }
-
-    @Override
-    public void toggle() {
-        setChecked(!mIsChecked);
-    }
-
-    @Override
-    protected void cancelLoadingAndClear() {
-        super.cancelLoadingAndClear();
-        if (mBitmap != null) {
-            MtpBitmapFetch.recycleThumbnail(mBitmap);
-            mBitmap = null;
-        }
+  }
+
+  @Override
+  public void toggle() {
+    setChecked(!mIsChecked);
+  }
+
+  @Override
+  protected void cancelLoadingAndClear() {
+    super.cancelLoadingAndClear();
+    if (mBitmap != null) {
+      MtpBitmapFetch.recycleThumbnail(mBitmap);
+      mBitmap = null;
     }
+  }
 }