diff options
author | Bobby Georgescu <georgescu@google.com> | 2014-05-14 10:19:19 -0700 |
---|---|---|
committer | Bobby Georgescu <georgescu@google.com> | 2014-05-14 18:01:29 +0000 |
commit | f640d379259bb114a50e3200f49961b89d60f2c2 (patch) | |
tree | 9a76ad6315f4f77063dda28b7441e2904ca3cd04 /src/com/android/gallery3d | |
parent | 29e13812d006579106c147f87c859aec23dfbe11 (diff) | |
download | android_packages_apps_Gallery2-f640d379259bb114a50e3200f49961b89d60f2c2.tar.gz android_packages_apps_Gallery2-f640d379259bb114a50e3200f49961b89d60f2c2.tar.bz2 android_packages_apps_Gallery2-f640d379259bb114a50e3200f49961b89d60f2c2.zip |
Update ingest importer code
Change-Id: I0f3b0809deead2f49501a5309f0ddab9c911274f
Diffstat (limited to 'src/com/android/gallery3d')
22 files changed, 2922 insertions, 2292 deletions
diff --git a/src/com/android/gallery3d/ingest/ImportTask.java b/src/com/android/gallery3d/ingest/ImportTask.java deleted file mode 100644 index 7d2d641a5..000000000 --- a/src/com/android/gallery3d/ingest/ImportTask.java +++ /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(); - } - } -} diff --git a/src/com/android/gallery3d/ingest/IngestActivity.java b/src/com/android/gallery3d/ingest/IngestActivity.java index 687e9fd44..46173d686 100644 --- a/src/com/android/gallery3d/ingest/IngestActivity.java +++ b/src/com/android/gallery3d/ingest/IngestActivity.java @@ -16,6 +16,19 @@ 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); } + } } diff --git a/src/com/android/gallery3d/ingest/IngestService.java b/src/com/android/gallery3d/ingest/IngestService.java index 9d406b13e..98aa7f5c6 100644 --- a/src/com/android/gallery3d/ingest/IngestService.java +++ b/src/com/android/gallery3d/ingest/IngestService.java @@ -16,6 +16,13 @@ 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 index fed851e0e..000000000 --- a/src/com/android/gallery3d/ingest/MtpDeviceIndex.java +++ /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 index 05db2cde2..000000000 --- a/src/com/android/gallery3d/ingest/SimpleDate.java +++ /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; - } -} diff --git a/src/com/android/gallery3d/ingest/adapter/CheckBroker.java b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java index 6783f23c5..dc8723fa6 100644 --- a/src/com/android/gallery3d/ingest/adapter/CheckBroker.java +++ b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java @@ -16,41 +16,52 @@ 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); + } } diff --git a/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java index e8dd69f8c..c3ce59f4e 100644 --- a/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java +++ b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java @@ -16,177 +16,187 @@ 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); + } } diff --git a/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java index 9e7abc01d..9fe650c17 100644 --- a/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java +++ b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java @@ -16,87 +16,95 @@ 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; + } } diff --git a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java index bbc90f670..c436fa7b4 100644 --- a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java +++ b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java @@ -16,14 +16,20 @@ package com.android.gallery3d.ingest.data; +import android.annotation.TargetApi; import android.graphics.Bitmap; +import android.os.Build; +/** + * Encapsulates a Bitmap and some additional metadata. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) public class BitmapWithMetadata { - public Bitmap bitmap; - public int rotationDegrees; + public Bitmap bitmap; + public int rotationDegrees; - public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) { - this.bitmap = bitmap; - this.rotationDegrees = rotationDegrees; - } + public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) { + this.bitmap = bitmap; + this.rotationDegrees = rotationDegrees; + } } diff --git a/src/com/android/gallery3d/ingest/data/DateBucket.java b/src/com/android/gallery3d/ingest/data/DateBucket.java new file mode 100644 index 000000000..85eedb3bd --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/DateBucket.java @@ -0,0 +1,63 @@ +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.os.Build; + +/** + * Date bucket for {@link MtpDeviceIndex}. + * See {@link MtpDeviceIndexRunnable} for implementation notes. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +class DateBucket implements Comparable<DateBucket> { + final SimpleDate date; + final int unifiedStartIndex; + final int unifiedEndIndex; + final int itemsStartIndex; + final int numItems; + + public DateBucket(SimpleDate date, int unifiedStartIndex, int unifiedEndIndex, + int itemsStartIndex, int numItems) { + this.date = date; + this.unifiedStartIndex = unifiedStartIndex; + this.unifiedEndIndex = unifiedEndIndex; + this.itemsStartIndex = itemsStartIndex; + this.numItems = numItems; + } + + @Override + public String toString() { + return date.toString(); + } + + @Override + public int hashCode() { + return date.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof DateBucket)) { + return false; + } + DateBucket other = (DateBucket) obj; + if (date == null) { + if (other.date != null) { + return false; + } + } else if (!date.equals(other.date)) { + return false; + } + return true; + } + + @Override + public int compareTo(DateBucket another) { + return this.date.compareTo(another.date); + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/ingest/data/ImportTask.java b/src/com/android/gallery3d/ingest/data/ImportTask.java new file mode 100644 index 000000000..ee2a7d0e5 --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/ImportTask.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.content.Context; +import android.mtp.MtpDevice; +import android.os.Build; +import android.os.Environment; +import android.os.PowerManager; +import android.os.StatFs; +import android.util.Log; + +import java.io.File; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * Task that handles the copying of items from an MTP device. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +public class ImportTask implements Runnable { + + private static final String TAG = "ImportTask"; + + /** + * Import progress listener. + */ + public interface Listener { + void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful); + + void onImportFinish(Collection<IngestObjectInfo> objectsNotImported, int visitedCount); + } + + private static final String WAKELOCK_LABEL = "Google Photos MTP Import Task"; + + private Listener mListener; + private String mDestAlbumName; + private Collection<IngestObjectInfo> mObjectsToImport; + private MtpDevice mDevice; + private PowerManager.WakeLock mWakeLock; + + public ImportTask(MtpDevice device, Collection<IngestObjectInfo> objectsToImport, + String destAlbumName, Context context) { + mDestAlbumName = destAlbumName; + mObjectsToImport = objectsToImport; + mDevice = device; + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, WAKELOCK_LABEL); + } + + public void setListener(Listener listener) { + mListener = listener; + } + + @Override + public void run() { + mWakeLock.acquire(); + try { + List<IngestObjectInfo> objectsNotImported = new LinkedList<IngestObjectInfo>(); + int visited = 0; + int total = mObjectsToImport.size(); + mListener.onImportProgress(visited, total, null); + File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName); + dest.mkdirs(); + for (IngestObjectInfo object : mObjectsToImport) { + visited++; + String importedPath = null; + if (hasSpaceForSize(object.getCompressedSize())) { + importedPath = new File(dest, object.getName(mDevice)).getAbsolutePath(); + if (!mDevice.importFile(object.getObjectHandle(), importedPath)) { + importedPath = null; + } + } + if (importedPath == null) { + objectsNotImported.add(object); + } + if (mListener != null) { + mListener.onImportProgress(visited, total, importedPath); + } + } + if (mListener != null) { + mListener.onImportFinish(objectsNotImported, visited); + } + } finally { + mListener = null; + mWakeLock.release(); + } + } + + private static boolean hasSpaceForSize(long size) { + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + return false; + } + + String path = Environment.getExternalStorageDirectory().getPath(); + try { + StatFs stat = new StatFs(path); + return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size; + } catch (Exception e) { + Log.i(TAG, "Fail to access external storage", e); + } + return false; + } +} diff --git a/src/com/android/gallery3d/ingest/data/IngestObjectInfo.java b/src/com/android/gallery3d/ingest/data/IngestObjectInfo.java new file mode 100644 index 000000000..25273838b --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/IngestObjectInfo.java @@ -0,0 +1,114 @@ +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.os.Build; + +/** + * Holds the info needed for the in-memory index of MTP objects. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +public class IngestObjectInfo implements Comparable<IngestObjectInfo> { + + private int mHandle; + private long mDateCreated; + private int mFormat; + private int mCompressedSize; + + public IngestObjectInfo(MtpObjectInfo mtpObjectInfo) { + mHandle = mtpObjectInfo.getObjectHandle(); + mDateCreated = mtpObjectInfo.getDateCreated(); + mFormat = mtpObjectInfo.getFormat(); + mCompressedSize = mtpObjectInfo.getCompressedSize(); + } + + public IngestObjectInfo(int handle, long dateCreated, int format, int compressedSize) { + mHandle = handle; + mDateCreated = dateCreated; + mFormat = format; + mCompressedSize = compressedSize; + } + + public int getCompressedSize() { + return mCompressedSize; + } + + public int getFormat() { + return mFormat; + } + + public long getDateCreated() { + return mDateCreated; + } + + public int getObjectHandle() { + return mHandle; + } + + public String getName(MtpDevice device) { + if (device != null) { + MtpObjectInfo info = device.getObjectInfo(mHandle); + if (info != null) { + return info.getName(); + } + } + return null; + } + + @Override + public int compareTo(IngestObjectInfo another) { + long diff = getDateCreated() - another.getDateCreated(); + if (diff < 0) { + return -1; + } else if (diff == 0) { + return 0; + } else { + return 1; + } + } + + @Override + public String toString() { + return "IngestObjectInfo [mHandle=" + mHandle + ", mDateCreated=" + mDateCreated + + ", mFormat=" + mFormat + ", mCompressedSize=" + mCompressedSize + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mCompressedSize; + result = prime * result + (int) (mDateCreated ^ (mDateCreated >>> 32)); + result = prime * result + mFormat; + result = prime * result + mHandle; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof IngestObjectInfo)) { + return false; + } + IngestObjectInfo other = (IngestObjectInfo) obj; + if (mCompressedSize != other.mCompressedSize) { + return false; + } + if (mDateCreated != other.mDateCreated) { + return false; + } + if (mFormat != other.mFormat) { + return false; + } + if (mHandle != other.mHandle) { + return false; + } + return true; + } +} diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java index c6504a561..3295828f1 100644 --- a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java +++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java @@ -16,91 +16,98 @@ package com.android.gallery3d.ingest.data; +import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.mtp.MtpDevice; -import android.mtp.MtpObjectInfo; +import android.os.Build; import android.util.DisplayMetrics; import android.view.WindowManager; import com.android.gallery3d.data.Exif; import com.android.photos.data.GalleryBitmapPool; +/** + * Helper class for fetching bitmaps from MTP devices. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) public class MtpBitmapFetch { - private static int sMaxSize = 0; + private static int sMaxSize = 0; - public static void recycleThumbnail(Bitmap b) { - if (b != null) { - GalleryBitmapPool.getInstance().put(b); - } + public static void recycleThumbnail(Bitmap b) { + if (b != null) { + GalleryBitmapPool.getInstance().put(b); } + } - public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) { - byte[] imageBytes = device.getThumbnail(info.getObjectHandle()); - if (imageBytes == null) { - return null; - } - BitmapFactory.Options o = new BitmapFactory.Options(); - o.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); - if (o.outWidth == 0 || o.outHeight == 0) { - return null; - } - o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight); - o.inMutable = true; - o.inJustDecodeBounds = false; - o.inSampleSize = 1; - try { - return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); - } catch (IllegalArgumentException e) { - // BitmapFactory throws an exception rather than returning null - // when image decoding fails and an existing bitmap was supplied - // for recycling, even if the failure was not caused by the use - // of that bitmap. - return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); - } + public static Bitmap getThumbnail(MtpDevice device, IngestObjectInfo info) { + byte[] imageBytes = device.getThumbnail(info.getObjectHandle()); + if (imageBytes == null) { + return null; } - - public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) { - return getFullsize(device, info, sMaxSize); + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + if (o.outWidth == 0 || o.outHeight == 0) { + return null; } + o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight); + o.inMutable = true; + o.inJustDecodeBounds = false; + o.inSampleSize = 1; + try { + return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + } catch (IllegalArgumentException e) { + // BitmapFactory throws an exception rather than returning null + // when image decoding fails and an existing bitmap was supplied + // for recycling, even if the failure was not caused by the use + // of that bitmap. + return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + } + } - public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) { - byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize()); - if (imageBytes == null) { - return null; - } - Bitmap created; - if (maxSide > 0) { - BitmapFactory.Options o = new BitmapFactory.Options(); - o.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); - int w = o.outWidth; - int h = o.outHeight; - int comp = Math.max(h, w); - int sampleSize = 1; - while ((comp >> 1) >= maxSide) { - comp = comp >> 1; - sampleSize++; - } - o.inSampleSize = sampleSize; - o.inJustDecodeBounds = false; - created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); - } else { - created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); - } - if (created == null) { - return null; - } + public static BitmapWithMetadata getFullsize(MtpDevice device, IngestObjectInfo info) { + return getFullsize(device, info, sMaxSize); + } - return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes)); + public static BitmapWithMetadata getFullsize(MtpDevice device, IngestObjectInfo info, + int maxSide) { + byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize()); + if (imageBytes == null) { + return null; } - - public static void configureForContext(Context context) { - DisplayMetrics metrics = new DisplayMetrics(); - WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); - wm.getDefaultDisplay().getMetrics(metrics); - sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels); + Bitmap created; + if (maxSide > 0) { + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + int w = o.outWidth; + int h = o.outHeight; + int comp = Math.max(h, w); + int sampleSize = 1; + while ((comp >> 1) >= maxSide) { + comp = comp >> 1; + sampleSize++; + } + o.inSampleSize = sampleSize; + o.inJustDecodeBounds = false; + created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o); + } else { + created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); } + if (created == null) { + return null; + } + + int orientation = Exif.getOrientation(imageBytes); + return new BitmapWithMetadata(created, orientation); + } + + public static void configureForContext(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels); + } } diff --git a/src/com/android/gallery3d/ingest/data/MtpClient.java b/src/com/android/gallery3d/ingest/data/MtpClient.java new file mode 100644 index 000000000..cc6c9ce07 --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/MtpClient.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.mtp.MtpDevice; +import android.os.Build; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This class helps an application manage a list of connected MTP or PTP devices. + * It listens for MTP devices being attached and removed from the USB host bus + * and notifies the application when the MTP device list changes. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +public class MtpClient { + + private static final String TAG = "MtpClient"; + + private static final String ACTION_USB_PERMISSION = + "com.android.gallery3d.ingest.action.USB_PERMISSION"; + + private final Context mContext; + private final UsbManager mUsbManager; + private final ArrayList<Listener> mListeners = new ArrayList<Listener>(); + // mDevices contains all MtpDevices that have been seen by our client, + // so we can inform when the device has been detached. + // mDevices is also used for synchronization in this class. + private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>(); + // List of MTP devices we should not try to open for which we are currently + // asking for permission to open. + private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>(); + // List of MTP devices we should not try to open. + // We add devices to this list if the user canceled a permission request or we were + // unable to open the device. + private final ArrayList<String> mIgnoredDevices = new ArrayList<String>(); + + private final PendingIntent mPermissionIntent; + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + UsbDevice usbDevice = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + String deviceName = usbDevice.getDeviceName(); + + synchronized (mDevices) { + MtpDevice mtpDevice = mDevices.get(deviceName); + + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + if (mtpDevice != null) { + mDevices.remove(deviceName); + mRequestPermissionDevices.remove(deviceName); + mIgnoredDevices.remove(deviceName); + for (Listener listener : mListeners) { + listener.deviceRemoved(mtpDevice); + } + } + } else if (ACTION_USB_PERMISSION.equals(action)) { + mRequestPermissionDevices.remove(deviceName); + boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, + false); + Log.d(TAG, "ACTION_USB_PERMISSION: " + permission); + if (permission) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else { + // so we don't ask for permission again + mIgnoredDevices.add(deviceName); + } + } + } + } + }; + + /** + * An interface for being notified when MTP or PTP devices are attached + * or removed. In the current implementation, only PTP devices are supported. + */ + public interface Listener { + /** + * Called when a new device has been added + * + * @param device the new device that was added + */ + public void deviceAdded(MtpDevice device); + + /** + * Called when a new device has been removed + * + * @param device the device that was removed + */ + public void deviceRemoved(MtpDevice device); + } + + /** + * Tests to see if a {@link android.hardware.usb.UsbDevice} + * supports the PTP protocol (typically used by digital cameras) + * + * @param device the device to test + * @return true if the device is a PTP device. + */ + public static boolean isCamera(UsbDevice device) { + int count = device.getInterfaceCount(); + for (int i = 0; i < count; i++) { + UsbInterface intf = device.getInterface(i); + if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE && + intf.getInterfaceSubclass() == 1 && + intf.getInterfaceProtocol() == 1) { + return true; + } + } + return false; + } + + /** + * MtpClient constructor + * + * @param context the {@link android.content.Context} to use for the MtpClient + */ + public MtpClient(Context context) { + mContext = context; + mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, + new Intent(ACTION_USB_PERMISSION), 0); + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(ACTION_USB_PERMISSION); + context.registerReceiver(mUsbReceiver, filter); + } + + /** + * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP + * device and return an {@link android.mtp.MtpDevice} for it. + * + * @param usbDevice the device to open + * @return an MtpDevice for the device. + */ + private MtpDevice openDeviceLocked(UsbDevice usbDevice) { + String deviceName = usbDevice.getDeviceName(); + + // don't try to open devices that we have decided to ignore + // or are currently asking permission for + if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName) + && !mRequestPermissionDevices.contains(deviceName)) { + if (!mUsbManager.hasPermission(usbDevice)) { + mUsbManager.requestPermission(usbDevice, mPermissionIntent); + mRequestPermissionDevices.add(deviceName); + } else { + UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice); + if (connection != null) { + MtpDevice mtpDevice = new MtpDevice(usbDevice); + if (mtpDevice.open(connection)) { + mDevices.put(usbDevice.getDeviceName(), mtpDevice); + return mtpDevice; + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } + } + return null; + } + + /** + * Closes all resources related to the MtpClient object + */ + public void close() { + mContext.unregisterReceiver(mUsbReceiver); + } + + /** + * Registers a {@link com.android.gallery3d.data.MtpClient.Listener} interface to receive + * notifications when MTP or PTP devices are added or removed. + * + * @param listener the listener to register + */ + public void addListener(Listener listener) { + synchronized (mDevices) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + } + + /** + * Unregisters a {@link com.android.gallery3d.data.MtpClient.Listener} interface. + * + * @param listener the listener to unregister + */ + public void removeListener(Listener listener) { + synchronized (mDevices) { + mListeners.remove(listener); + } + } + + + /** + * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}. + * + * @return the list of MtpDevices + */ + public List<MtpDevice> getDeviceList() { + synchronized (mDevices) { + // Query the USB manager since devices might have attached + // before we added our listener. + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + if (mDevices.get(usbDevice.getDeviceName()) == null) { + openDeviceLocked(usbDevice); + } + } + + return new ArrayList<MtpDevice>(mDevices.values()); + } + } + + +} diff --git a/src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java b/src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java new file mode 100644 index 000000000..b21ad8355 --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/MtpDeviceIndex.java @@ -0,0 +1,433 @@ +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.mtp.MtpConstants; +import android.mtp.MtpDevice; +import android.os.Build; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Index of MTP media objects organized into "buckets," or groupings, based on the date + * they were created. + * + * When the index is created, the buckets are sorted in their natural + * order, and the items within the buckets sorted by the date they are taken. + * + * The index enables the access of items and bucket labels as one unified list. + * For example, let's say we have the following data in the index: + * [Bucket A]: [photo 1], [photo 2] + * [Bucket B]: [photo 3] + * + * Then the items can be thought of as being organized as a 5 element list: + * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3] + * + * The data can also be accessed in descending order, in which case the list + * would be a bit different from simply reversing the ascending list, since the + * bucket labels need to always be at the beginning: + * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1] + * + * The index enables all the following operations in constant time, both for + * ascending and descending views of the data: + * - get/getAscending/getDescending: get an item at a specified list position + * - size: get the total number of items (bucket labels and MTP objects) + * - getFirstPositionForBucketNumber + * - getBucketNumberForPosition + * - isFirstInBucket + * + * See {@link MtpDeviceIndexRunnable} for implementation notes. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +public class MtpDeviceIndex { + + /** + * Indexing progress listener. + */ + public interface ProgressListener { + /** + * A media item on the device was indexed. + * @param object The media item that was just indexed + * @param numVisited Number of items visited so far + */ + public void onObjectIndexed(IngestObjectInfo object, int numVisited); + + /** + * The metadata loaded from the device is being sorted. + */ + public void onSortingStarted(); + + /** + * The indexing is done and the index is ready to be used. + */ + public void onIndexingFinished(); + } + + /** + * Media sort orders. + */ + public enum SortOrder { + ASCENDING, DESCENDING + } + + /** Quicktime MOV container (not already defined in {@link MtpConstants}) **/ + public static final int FORMAT_MOV = 0x300D; + + public static final Set<Integer> SUPPORTED_IMAGE_FORMATS; + public static final Set<Integer> SUPPORTED_VIDEO_FORMATS; + + static { + Set<Integer> supportedImageFormats = new HashSet<Integer>(); + supportedImageFormats.add(MtpConstants.FORMAT_JFIF); + supportedImageFormats.add(MtpConstants.FORMAT_EXIF_JPEG); + supportedImageFormats.add(MtpConstants.FORMAT_PNG); + supportedImageFormats.add(MtpConstants.FORMAT_GIF); + supportedImageFormats.add(MtpConstants.FORMAT_BMP); + SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableSet(supportedImageFormats); + + Set<Integer> supportedVideoFormats = new HashSet<Integer>(); + supportedVideoFormats.add(MtpConstants.FORMAT_3GP_CONTAINER); + supportedVideoFormats.add(MtpConstants.FORMAT_AVI); + supportedVideoFormats.add(MtpConstants.FORMAT_MP4_CONTAINER); + supportedVideoFormats.add(MtpConstants.FORMAT_MPEG); + // TODO(georgescu): add FORMAT_MOV once Android Media Scanner supports .mov files + SUPPORTED_VIDEO_FORMATS = Collections.unmodifiableSet(supportedVideoFormats); + } + + private MtpDevice mDevice; + private long mGeneration; + private ProgressListener mProgressListener; + private volatile MtpDeviceIndexRunnable.Results mResults; + private final MtpDeviceIndexRunnable.Factory mIndexRunnableFactory; + + private static final MtpDeviceIndex sInstance = new MtpDeviceIndex( + MtpDeviceIndexRunnable.getFactory()); + + public static MtpDeviceIndex getInstance() { + return sInstance; + } + + protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) { + mIndexRunnableFactory = indexRunnableFactory; + } + + public synchronized MtpDevice getDevice() { + return mDevice; + } + + public synchronized boolean isDeviceConnected() { + return (mDevice != null); + } + + /** + * @param format Media format from {@link MtpConstants} + * @return Whether the format is supported by this index. + */ + public boolean isFormatSupported(int format) { + return SUPPORTED_IMAGE_FORMATS.contains(format) + || SUPPORTED_VIDEO_FORMATS.contains(format); + } + + /** + * Sets the MtpDevice that should be indexed and initializes state, but does + * not kick off the actual indexing task, which is instead done by using + * {@link #getIndexRunnable()} + * + * @param device The MtpDevice that should be indexed + */ + public synchronized void setDevice(MtpDevice device) { + if (device == mDevice) { + return; + } + mDevice = device; + resetState(); + } + + /** + * Provides a Runnable for the indexing task (assuming the state has already + * been correctly initialized by calling {@link #setDevice(MtpDevice)}). + * + * @return Runnable for the main indexing task + */ + public synchronized Runnable getIndexRunnable() { + if (!isDeviceConnected() || mResults != null) { + return null; + } + return mIndexRunnableFactory.createMtpDeviceIndexRunnable(this); + } + + /** + * @return Whether the index is ready to be used. + */ + public synchronized boolean isIndexReady() { + return mResults != null; + } + + /** + * @param listener + * @return Current progress (useful for configuring initial UI state) + */ + public synchronized void setProgressListener(ProgressListener listener) { + mProgressListener = listener; + } + + /** + * Make the listener null if it matches the argument + * + * @param listener Listener to unset, if currently registered + */ + public synchronized void unsetProgressListener(ProgressListener listener) { + if (mProgressListener == listener) { + mProgressListener = null; + } + } + + /** + * @return The total number of elements in the index (labels and items) + */ + public int size() { + MtpDeviceIndexRunnable.Results results = mResults; + return results != null ? results.unifiedLookupIndex.length : 0; + } + + /** + * @param position Index of item to fetch, where 0 is the first item in the + * specified order + * @param order + * @return the bucket label or IngestObjectInfo at the specified position and + * order + */ + public Object get(int position, SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (results == null) { + return null; + } + if (order == SortOrder.ASCENDING) { + DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]]; + if (bucket.unifiedStartIndex == position) { + return bucket.date; + } else { + return results.mtpObjects[bucket.itemsStartIndex + position - 1 + - bucket.unifiedStartIndex]; + } + } else { + int zeroIndex = results.unifiedLookupIndex.length - 1 - position; + DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]]; + if (bucket.unifiedEndIndex == zeroIndex) { + return bucket.date; + } else { + return results.mtpObjects[bucket.itemsStartIndex + zeroIndex + - bucket.unifiedStartIndex]; + } + } + } + + /** + * @param position Index of item to fetch from a view of the data that does not + * include labels and is in the specified order + * @return position-th item in specified order, when not including labels + */ + public IngestObjectInfo getWithoutLabels(int position, SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (results == null) { + return null; + } + if (order == SortOrder.ASCENDING) { + return results.mtpObjects[position]; + } else { + return results.mtpObjects[results.mtpObjects.length - 1 - position]; + } + } + + /** + * @param position Index of item to map from a view of the data that does not + * include labels and is in the specified order + * @param order + * @return position in a view of the data that does include labels, or -1 if the index isn't + * ready + */ + public int getPositionFromPositionWithoutLabels(int position, SortOrder order) { + /* Although this is O(log(number of buckets)), and thus should not be used + in hotspots, even if the attached device has items for every day for + a five-year timeframe, it would still only take 11 iterations at most, + so shouldn't be a huge issue. */ + MtpDeviceIndexRunnable.Results results = mResults; + if (results == null) { + return -1; + } + if (order == SortOrder.DESCENDING) { + position = results.mtpObjects.length - 1 - position; + } + int bucketNumber = 0; + int iMin = 0; + int iMax = results.buckets.length - 1; + while (iMax >= iMin) { + int iMid = (iMax + iMin) / 2; + if (results.buckets[iMid].itemsStartIndex + results.buckets[iMid].numItems + <= position) { + iMin = iMid + 1; + } else if (results.buckets[iMid].itemsStartIndex > position) { + iMax = iMid - 1; + } else { + bucketNumber = iMid; + break; + } + } + int mappedPos = results.buckets[bucketNumber].unifiedStartIndex + position + - results.buckets[bucketNumber].itemsStartIndex + 1; + if (order == SortOrder.DESCENDING) { + mappedPos = results.unifiedLookupIndex.length - mappedPos; + } + return mappedPos; + } + + /** + * @param position Index of item to map from a view of the data that + * includes labels and is in the specified order + * @param order + * @return position in a view of the data that does not include labels, or -1 if the index isn't + * ready + */ + public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (results == null) { + return -1; + } + if (order == SortOrder.ASCENDING) { + DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]]; + if (bucket.unifiedStartIndex == position) { + position++; + } + return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex; + } else { + int zeroIndex = results.unifiedLookupIndex.length - 1 - position; + DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]]; + if (bucket.unifiedEndIndex == zeroIndex) { + zeroIndex--; + } + return results.mtpObjects.length - 1 - bucket.itemsStartIndex + - zeroIndex + bucket.unifiedStartIndex; + } + } + + /** + * @return The number of media items in the index + */ + public int sizeWithoutLabels() { + MtpDeviceIndexRunnable.Results results = mResults; + return results != null ? results.mtpObjects.length : 0; + } + + /** + * @param bucketNumber Index of bucket in the specified order + * @param order + * @return position of bucket's first item in a view of the data that includes labels + */ + public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (order == SortOrder.ASCENDING) { + return results.buckets[bucketNumber].unifiedStartIndex; + } else { + return results.unifiedLookupIndex.length + - results.buckets[results.buckets.length - 1 - bucketNumber].unifiedEndIndex + - 1; + } + } + + /** + * @param position Index of item in the view of the data that includes labels and is in + * the specified order + * @param order + * @return Index of the bucket that contains the specified item + */ + public int getBucketNumberForPosition(int position, SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (order == SortOrder.ASCENDING) { + return results.unifiedLookupIndex[position]; + } else { + return results.buckets.length - 1 + - results.unifiedLookupIndex[results.unifiedLookupIndex.length - 1 + - position]; + } + } + + /** + * @param position Index of item in the view of the data that includes labels and is in + * the specified order + * @param order + * @return Whether the specified item is the first item in its bucket + */ + public boolean isFirstInBucket(int position, SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (order == SortOrder.ASCENDING) { + return results.buckets[results.unifiedLookupIndex[position]].unifiedStartIndex + == position; + } else { + position = results.unifiedLookupIndex.length - 1 - position; + return results.buckets[results.unifiedLookupIndex[position]].unifiedEndIndex + == position; + } + } + + /** + * @param order + * @return Array of buckets in the specified order + */ + public DateBucket[] getBuckets(SortOrder order) { + MtpDeviceIndexRunnable.Results results = mResults; + if (results == null) { + return null; + } + return (order == SortOrder.ASCENDING) ? results.buckets : results.reversedBuckets; + } + + protected void resetState() { + mGeneration++; + mResults = null; + } + + /** + * @param device + * @param generation + * @return whether the index is at the given generation and the given device is connected + */ + protected boolean isAtGeneration(MtpDevice device, long generation) { + return (mGeneration == generation) && (mDevice == device); + } + + protected synchronized boolean setIndexingResults(MtpDevice device, long generation, + MtpDeviceIndexRunnable.Results results) { + if (!isAtGeneration(device, generation)) { + return false; + } + mResults = results; + onIndexFinish(true /*successful*/); + return true; + } + + protected synchronized void onIndexFinish(boolean successful) { + if (!successful) { + resetState(); + } + if (mProgressListener != null) { + mProgressListener.onIndexingFinished(); + } + } + + protected synchronized void onSorting() { + if (mProgressListener != null) { + mProgressListener.onSortingStarted(); + } + } + + protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) { + if (mProgressListener != null) { + mProgressListener.onObjectIndexed(object, numVisited); + } + } + + protected long getGeneration() { + return mGeneration; + } +} diff --git a/src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java b/src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java new file mode 100644 index 000000000..32275898e --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/MtpDeviceIndexRunnable.java @@ -0,0 +1,186 @@ +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.mtp.MtpConstants; +import android.mtp.MtpDevice; +import android.mtp.MtpObjectInfo; +import android.os.Build; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.Stack; +import java.util.TreeMap; + +/** + * Runnable used by the {@link MtpDeviceIndex} to populate its index. + * + * Implementation note: this is the way the index supports a lot of its operations in + * constant time and respecting the need to have bucket names always come before items + * in that bucket when accessing the list sequentially, both in ascending and descending + * orders. + * + * Let's say the data we have in the index is the following: + * [Bucket A]: [photo 1], [photo 2] + * [Bucket B]: [photo 3] + * + * In this case, the lookup index array would be + * [0, 0, 0, 1, 1] + * + * Now, whether we access the list in ascending or descending order, we know which bucket + * to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first + * item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex + * that correspond to indices in this lookup index array, allowing us to calculate the + * offset of the specific item we want from within a specific bucket. + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +public class MtpDeviceIndexRunnable implements Runnable { + + /** + * MtpDeviceIndexRunnable factory. + */ + public static class Factory { + public MtpDeviceIndexRunnable createMtpDeviceIndexRunnable(MtpDeviceIndex index) { + return new MtpDeviceIndexRunnable(index); + } + } + + static class Results { + final int[] unifiedLookupIndex; + final IngestObjectInfo[] mtpObjects; + final DateBucket[] buckets; + final DateBucket[] reversedBuckets; + + public Results( + int[] unifiedLookupIndex, IngestObjectInfo[] mtpObjects, DateBucket[] buckets) { + this.unifiedLookupIndex = unifiedLookupIndex; + this.mtpObjects = mtpObjects; + this.buckets = buckets; + this.reversedBuckets = new DateBucket[buckets.length]; + for (int i = 0; i < buckets.length; i++) { + this.reversedBuckets[i] = buckets[buckets.length - 1 - i]; + } + } + } + + private final MtpDevice mDevice; + protected final MtpDeviceIndex mIndex; + private final long mIndexGeneration; + + private static Factory sDefaultFactory = new Factory(); + + public static Factory getFactory() { + return sDefaultFactory; + } + + /** + * Exception thrown when a problem occurred during indexing. + */ + @SuppressWarnings("serial") + public class IndexingException extends RuntimeException {} + + MtpDeviceIndexRunnable(MtpDeviceIndex index) { + mIndex = index; + mDevice = index.getDevice(); + mIndexGeneration = index.getGeneration(); + } + + @Override + public void run() { + try { + indexDevice(); + } catch (IndexingException e) { + mIndex.onIndexFinish(false /*successful*/); + } + } + + private void indexDevice() throws IndexingException { + SortedMap<SimpleDate, List<IngestObjectInfo>> bucketsTemp = + new TreeMap<SimpleDate, List<IngestObjectInfo>>(); + int numObjects = addAllObjects(bucketsTemp); + mIndex.onSorting(); + int numBuckets = bucketsTemp.size(); + DateBucket[] buckets = new DateBucket[numBuckets]; + IngestObjectInfo[] mtpObjects = new IngestObjectInfo[numObjects]; + int[] unifiedLookupIndex = new int[numObjects + numBuckets]; + int currentUnifiedIndexEntry = 0; + int currentItemsEntry = 0; + int nextUnifiedEntry, unifiedStartIndex, numBucketObjects, unifiedEndIndex, itemsStartIndex; + + int i = 0; + for (Map.Entry<SimpleDate, List<IngestObjectInfo>> bucketTemp : bucketsTemp.entrySet()) { + List<IngestObjectInfo> objects = bucketTemp.getValue(); + Collections.sort(objects); + numBucketObjects = objects.size(); + + nextUnifiedEntry = currentUnifiedIndexEntry + numBucketObjects + 1; + Arrays.fill(unifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i); + unifiedStartIndex = currentUnifiedIndexEntry; + unifiedEndIndex = nextUnifiedEntry - 1; + currentUnifiedIndexEntry = nextUnifiedEntry; + + itemsStartIndex = currentItemsEntry; + for (int j = 0; j < numBucketObjects; j++) { + mtpObjects[currentItemsEntry] = objects.get(j); + currentItemsEntry++; + } + buckets[i] = new DateBucket(bucketTemp.getKey(), unifiedStartIndex, unifiedEndIndex, + itemsStartIndex, numBucketObjects); + i++; + } + if (!mIndex.setIndexingResults(mDevice, mIndexGeneration, + new Results(unifiedLookupIndex, mtpObjects, buckets))) { + throw new IndexingException(); + } + } + + private SimpleDate mDateInstance = new SimpleDate(); + + protected void addObject(IngestObjectInfo objectInfo, + SortedMap<SimpleDate, List<IngestObjectInfo>> bucketsTemp, int numObjects) { + mDateInstance.setTimestamp(objectInfo.getDateCreated()); + List<IngestObjectInfo> bucket = bucketsTemp.get(mDateInstance); + if (bucket == null) { + bucket = new ArrayList<IngestObjectInfo>(); + bucketsTemp.put(mDateInstance, bucket); + mDateInstance = new SimpleDate(); // only create new date objects when they are used + } + bucket.add(objectInfo); + mIndex.onObjectIndexed(objectInfo, numObjects); + } + + protected int addAllObjects(SortedMap<SimpleDate, List<IngestObjectInfo>> bucketsTemp) + throws IndexingException { + int numObjects = 0; + for (int storageId : mDevice.getStorageIds()) { + if (!mIndex.isAtGeneration(mDevice, mIndexGeneration)) { + throw new IndexingException(); + } + Stack<Integer> pendingDirectories = new Stack<Integer>(); + pendingDirectories.add(0xFFFFFFFF); // start at the root of the device + while (!pendingDirectories.isEmpty()) { + if (!mIndex.isAtGeneration(mDevice, mIndexGeneration)) { + throw new IndexingException(); + } + int dirHandle = pendingDirectories.pop(); + for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) { + MtpObjectInfo mtpObjectInfo = mDevice.getObjectInfo(objectHandle); + if (mtpObjectInfo == null) { + throw new IndexingException(); + } + int format = mtpObjectInfo.getFormat(); + if (format == MtpConstants.FORMAT_ASSOCIATION) { + pendingDirectories.add(objectHandle); + } else if (mIndex.isFormatSupported(format)) { + numObjects++; + addObject(new IngestObjectInfo(mtpObjectInfo), bucketsTemp, numObjects); + } + } + } + } + return numObjects; + } +} diff --git a/src/com/android/gallery3d/ingest/data/SimpleDate.java b/src/com/android/gallery3d/ingest/data/SimpleDate.java new file mode 100644 index 000000000..2476f80ed --- /dev/null +++ b/src/com/android/gallery3d/ingest/data/SimpleDate.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ingest.data; + +import android.annotation.TargetApi; +import android.os.Build; + +import java.text.DateFormat; +import java.util.Calendar; + +/** + * Represents a date (year, month, day) + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +public class SimpleDate implements Comparable<SimpleDate> { + public int month; // MM + public int day; // DD + public int year; // YYYY + private long timestamp; + private String mCachedStringRepresentation; + + public SimpleDate() { + } + + public SimpleDate(long timestamp) { + setTimestamp(timestamp); + } + + private static Calendar sCalendarInstance = Calendar.getInstance(); + + public void setTimestamp(long timestamp) { + synchronized (sCalendarInstance) { + // TODO(georgescu): find a more efficient way to convert a timestamp to a date? + sCalendarInstance.setTimeInMillis(timestamp); + this.day = sCalendarInstance.get(Calendar.DATE); + this.month = sCalendarInstance.get(Calendar.MONTH); + this.year = sCalendarInstance.get(Calendar.YEAR); + this.timestamp = timestamp; + mCachedStringRepresentation = + DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + day; + result = prime * result + month; + result = prime * result + year; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof SimpleDate)) { + return false; + } + SimpleDate other = (SimpleDate) obj; + if (year != other.year) { + return false; + } + if (month != other.month) { + return false; + } + if (day != other.day) { + return false; + } + return true; + } + + @Override + public int compareTo(SimpleDate other) { + int yearDiff = this.year - other.getYear(); + if (yearDiff != 0) { + return yearDiff; + } else { + int monthDiff = this.month - other.getMonth(); + if (monthDiff != 0) { + return monthDiff; + } else { + return this.day - other.getDay(); + } + } + } + + public int getDay() { + return day; + } + + public int getMonth() { + return month; + } + + public int getYear() { + return year; + } + + @Override + public String toString() { + if (mCachedStringRepresentation == null) { + mCachedStringRepresentation = + DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp); + } + return mCachedStringRepresentation; + } +} diff --git a/src/com/android/gallery3d/ingest/ui/DateTileView.java b/src/com/android/gallery3d/ingest/ui/DateTileView.java index 52fe9b85b..cd31e82a9 100644 --- a/src/com/android/gallery3d/ingest/ui/DateTileView.java +++ b/src/com/android/gallery3d/ingest/ui/DateTileView.java @@ -16,92 +16,96 @@ 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)); } + } } diff --git a/src/com/android/gallery3d/ingest/ui/IngestGridView.java b/src/com/android/gallery3d/ingest/ui/IngestGridView.java index c821259fe..7bafa7c4e 100644 --- a/src/com/android/gallery3d/ingest/ui/IngestGridView.java +++ b/src/com/android/gallery3d/ingest/ui/IngestGridView.java @@ -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(); } + } } diff --git a/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java index 8d3884dc6..00785dde0 100644 --- a/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java +++ b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java @@ -16,100 +16,106 @@ 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)); } + } } diff --git a/src/com/android/gallery3d/ingest/ui/MtpImageView.java b/src/com/android/gallery3d/ingest/ui/MtpImageView.java index 80c105126..5362efd70 100644 --- a/src/com/android/gallery3d/ingest/ui/MtpImageView.java +++ b/src/com/android/gallery3d/ingest/ui/MtpImageView.java @@ -16,12 +16,17 @@ 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); } + } } diff --git a/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java index 3307e78aa..844a75024 100644 --- a/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java +++ b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java @@ -16,91 +16,98 @@ 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; } + } } |