From fb6132b0b3079153fd1d15acf1dc3c6100aa2e00 Mon Sep 17 00:00:00 2001 From: Bobby Georgescu Date: Thu, 7 Mar 2013 14:15:57 -0800 Subject: [PATCH] Add sharing support Change-Id: I2c7f18b0d6dd057c1fba96c1133d563f32abf55c --- .../com/android/gallery3d/common/ApiHelper.java | 2 + res/menu/gallery_multiselect.xml | 7 + src/com/android/photos/GalleryActivity.java | 8 + src/com/android/photos/PhotoSetFragment.java | 166 +++++++++++++++------ src/com/android/photos/SelectionManager.java | 126 ++++++++++++++++ .../photos/adapters/PhotoThumbnailAdapter.java | 81 ++++++++++ src/com/android/photos/data/PhotoSetLoader.java | 5 + .../android/photos/shims/BitmapJobDrawable.java | 16 ++ src/com/android/photos/shims/MediaItemsLoader.java | 2 + 9 files changed, 366 insertions(+), 47 deletions(-) create mode 100644 res/menu/gallery_multiselect.xml create mode 100644 src/com/android/photos/SelectionManager.java create mode 100644 src/com/android/photos/adapters/PhotoThumbnailAdapter.java diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java index 4200ec75e..13d48fd2c 100644 --- a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java +++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java @@ -39,6 +39,8 @@ public class ApiHelper { public static final int JELLY_BEAN_MR1 = 17; } + public static final boolean AT_LEAST_16 = Build.VERSION.SDK_INT >= 16; + public static final boolean USE_888_PIXEL_FORMAT = Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; diff --git a/res/menu/gallery_multiselect.xml b/res/menu/gallery_multiselect.xml new file mode 100644 index 000000000..3fbb6a7e5 --- /dev/null +++ b/res/menu/gallery_multiselect.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/com/android/photos/GalleryActivity.java b/src/com/android/photos/GalleryActivity.java index 46b5140fb..2335658d1 100644 --- a/src/com/android/photos/GalleryActivity.java +++ b/src/com/android/photos/GalleryActivity.java @@ -33,6 +33,7 @@ public class GalleryActivity extends Activity { private final String FTAG_PHOTOSET = "PhotoSet"; private final String FTAG_ALBUMSET = "AlbumSet"; + private SelectionManager mSelectionManager; @Override protected void onCreate(Bundle savedInstanceState) { @@ -41,6 +42,13 @@ public class GalleryActivity extends Activity { setupActionBar(); } + protected SelectionManager getSelectionManager() { + if (mSelectionManager == null) { + mSelectionManager = new SelectionManager(this); + } + return mSelectionManager; + } + private void setupActionBar() { ActionBar ab = getActionBar(); ab.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java index 18587fcf3..25d80360d 100644 --- a/src/com/android/photos/PhotoSetFragment.java +++ b/src/com/android/photos/PhotoSetFragment.java @@ -18,46 +18,58 @@ package com.android.photos; import android.app.Fragment; import android.app.LoaderManager.LoaderCallbacks; -import android.content.Context; import android.content.Intent; import android.content.Loader; import android.database.Cursor; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.util.SparseBooleanArray; +import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; +import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; -import android.widget.CursorAdapter; import android.widget.GridView; -import android.widget.ImageView; +import android.widget.ShareActionProvider; +import android.widget.ShareActionProvider.OnShareTargetSelectedListener; import com.android.gallery3d.R; import com.android.gallery3d.app.Gallery; +import com.android.gallery3d.data.MediaItem; +import com.android.photos.adapters.PhotoThumbnailAdapter; import com.android.photos.data.PhotoSetLoader; import com.android.photos.shims.LoaderCompatShim; import com.android.photos.shims.MediaItemsLoader; -import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; public class PhotoSetFragment extends Fragment implements LoaderCallbacks, - OnItemClickListener { + OnItemClickListener, SelectionManager.SelectedUriSource, MultiChoiceModeListener, + OnShareTargetSelectedListener { private static final int LOADER_PHOTOSET = 1; private GridView mPhotoSetView; private View mEmptyView; - private ThumbnailAdapter mAdapter; + private boolean mInitialLoadComplete = false; private LoaderCompatShim mLoaderCompatShim; + private PhotoThumbnailAdapter mAdapter; + private SelectionManager mSelectionManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mAdapter = new ThumbnailAdapter(getActivity()); + GalleryActivity activity = (GalleryActivity) getActivity(); + mSelectionManager = activity.getSelectionManager(); + mAdapter = new PhotoThumbnailAdapter(activity); } @Override @@ -71,6 +83,8 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks onCreateLoader(int id, Bundle args) { // TODO: Switch to PhotoSetLoader MediaItemsLoader loader = new MediaItemsLoader(getActivity()); - mAdapter.setDrawableFactory(loader); mInitialLoadComplete = false; mLoaderCompatShim = loader; + mAdapter.setDrawableFactory(mLoaderCompatShim); return loader; } @@ -115,54 +129,112 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks mSelectedUris = new HashSet(); + private ArrayList mSelectedUrisArray = new ArrayList(); + + @Override + public ArrayList getSelectedShareableUris() { + mSelectedUrisArray.clear(); + mSelectedUrisArray.addAll(mSelectedUris); + return mSelectedUrisArray; + } + + public ArrayList getSelectedShareableUrisUncached() { + mSelectedUrisArray.clear(); + SparseBooleanArray selected = mPhotoSetView.getCheckedItemPositions(); + + for (int i = 0; i < selected.size(); i++) { + if (selected.valueAt(i)) { + Cursor item = mAdapter.getItem(selected.keyAt(i)); + int supported = item.getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS); + if ((supported & MediaItem.SUPPORT_SHARE) > 0) { + mSelectedUrisArray.add(mLoaderCompatShim.uriForItem(item)); + } + } + } + + return mSelectedUrisArray; + } + @Override public void onLoaderReset(Loader loader) { } - private static class ThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter { - private LayoutInflater mInflater; - private LoaderCompatShim mDrawableFactory; - public ThumbnailAdapter(Context context) { - super(context, null, false); - mInflater = LayoutInflater.from(context); - } + private ShareActionProvider mShareActionProvider; + private ActionMode mActionMode; + private boolean mSharePending = false; - public void setDrawableFactory(LoaderCompatShim factory) { - mDrawableFactory = factory; - } + private void updateSelectedTitle(ActionMode mode) { + int count = mPhotoSetView.getCheckedItemCount(); + mode.setTitle(getResources().getQuantityString( + R.plurals.number_of_items_selected, count, count)); + } - @Override - public void bindView(View view, Context context, Cursor cursor) { - ImageView iv = (ImageView) view; - Drawable recycle = iv.getDrawable(); - Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle); - if (recycle != drawable) { - iv.setImageDrawable(drawable); - } - } + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + updateSelectedTitle(mode); + Cursor item = mAdapter.getItem(position); - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = mInflater.inflate(R.layout.photo_set_item, parent, false); - LayoutParams params = view.getLayoutParams(); - int columnWidth = ((GridView) parent).getColumnWidth(); - params.height = columnWidth; - view.setLayoutParams(params); - return view; + if (checked) { + mSelectedUris.add(mLoaderCompatShim.uriForItem(item)); + } else { + mSelectedUris.remove(mLoaderCompatShim.uriForItem(item)); } - @Override - public float getIntrinsicAspectRatio(int position) { - Cursor cursor = getItem(position); - float width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH); - float height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT); - return width / height; - } + mSelectionManager.onItemSelectedStateChanged(mShareActionProvider, + item.getInt(PhotoSetLoader.INDEX_MEDIA_TYPE), + item.getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS), + checked); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mSelectionManager.setSelectedUriSource(PhotoSetFragment.this); + mActionMode = mode; + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.gallery_multiselect, menu); + MenuItem menuItem = menu.findItem(R.id.menu_share); + mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider(); + mShareActionProvider.setOnShareTargetSelectedListener(this); + updateSelectedTitle(mode); + return true; + } - @Override - public Cursor getItem(int position) { - return (Cursor) super.getItem(position); + @Override + public void onDestroyActionMode(ActionMode mode) { + mSelectedUris.clear(); + if (mSharePending) { + // onDestroyActionMode gets called when the share target was selected, + // but apparently before the ArrayList is serialized in the intent + // so we can't clear the old one here. + mSelectedUrisArray = new ArrayList(); + mSharePending = false; + } else { + mSelectedUrisArray.clear(); } + mSelectionManager.onClearSelection(); + mSelectionManager.setSelectedUriSource(null); + mShareActionProvider = null; + mActionMode = null; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + updateSelectedTitle(mode); + return false; + } + + @Override + public boolean onShareTargetSelected(ShareActionProvider provider, Intent intent) { + mSharePending = true; + mActionMode.finish(); + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; } } diff --git a/src/com/android/photos/SelectionManager.java b/src/com/android/photos/SelectionManager.java new file mode 100644 index 000000000..d7e61d19a --- /dev/null +++ b/src/com/android/photos/SelectionManager.java @@ -0,0 +1,126 @@ +/* + * 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.photos; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.nfc.NfcEvent; +import android.provider.MediaStore.Files.FileColumns; +import android.widget.ShareActionProvider; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.GalleryUtils; + +import java.util.ArrayList; + +public class SelectionManager implements NfcAdapter.CreateBeamUrisCallback { + private Activity mActivity; + private NfcAdapter mNfcAdapter; + private SelectedUriSource mUriSource; + private Intent mShareIntent = new Intent(); + + public interface SelectedUriSource { + public ArrayList getSelectedShareableUris(); + } + + @TargetApi(16) + public SelectionManager(Activity activity) { + mActivity = activity; + if (ApiHelper.AT_LEAST_16) { + mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity); + mNfcAdapter.setBeamPushUrisCallback(this, mActivity); + } + } + + public void setSelectedUriSource(SelectedUriSource source) { + mUriSource = source; + } + + private int mSelectedTotalCount = 0; + private int mSelectedShareableCount = 0; + private int mSelectedShareableImageCount = 0; + private int mSelectedShareableVideoCount = 0; + private int mSelectedDeletableCount = 0; + + private ArrayList mCachedShareableUris = null; + + public void onItemSelectedStateChanged(ShareActionProvider share, + int itemType, int itemSupportedOperations, boolean selected) { + int increment = selected ? 1 : -1; + + mSelectedTotalCount += increment; + mCachedShareableUris = null; + + if ((itemSupportedOperations & MediaItem.SUPPORT_DELETE) > 0) { + mSelectedDeletableCount += increment; + } + if ((itemSupportedOperations & MediaItem.SUPPORT_SHARE) > 0) { + mSelectedShareableCount += increment; + if (itemType == FileColumns.MEDIA_TYPE_IMAGE) { + mSelectedShareableImageCount += increment; + } else if (itemType == FileColumns.MEDIA_TYPE_VIDEO) { + mSelectedShareableVideoCount += increment; + } + } + + mShareIntent.removeExtra(Intent.EXTRA_STREAM); + if (mSelectedShareableCount == 0) { + mShareIntent.setAction(null).setType(null); + } else if (mSelectedShareableCount >= 1) { + mCachedShareableUris = mUriSource.getSelectedShareableUris(); + if (mSelectedShareableImageCount == mSelectedShareableCount) { + mShareIntent.setType(GalleryUtils.MIME_TYPE_IMAGE); + } else if (mSelectedShareableVideoCount == mSelectedShareableCount) { + mShareIntent.setType(GalleryUtils.MIME_TYPE_VIDEO); + } else { + mShareIntent.setType(GalleryUtils.MIME_TYPE_ALL); + } + if (mSelectedShareableCount == 1) { + mShareIntent.setAction(Intent.ACTION_SEND); + mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris.get(0)); + } else { + mShareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris); + } + } + share.setShareIntent(mShareIntent); + + // TODO update deletability, editability, etc. + } + + public void onClearSelection() { + mSelectedTotalCount = 0; + mSelectedShareableCount = 0; + mSelectedShareableImageCount = 0; + mSelectedShareableVideoCount = 0; + mSelectedDeletableCount = 0; + mCachedShareableUris = null; + mShareIntent.removeExtra(Intent.EXTRA_STREAM); + mShareIntent.setAction(null).setType(null); + } + + @Override + public Uri[] createBeamUris(NfcEvent event) { + // This will have been preceded by a call to onItemSelectedStateChange + if (mCachedShareableUris == null) return null; + return mCachedShareableUris.toArray(new Uri[mCachedShareableUris.size()]); + } +} diff --git a/src/com/android/photos/adapters/PhotoThumbnailAdapter.java b/src/com/android/photos/adapters/PhotoThumbnailAdapter.java new file mode 100644 index 000000000..5715795da --- /dev/null +++ b/src/com/android/photos/adapters/PhotoThumbnailAdapter.java @@ -0,0 +1,81 @@ +/* + * 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.photos.adapters; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.CursorAdapter; +import android.widget.GridView; +import android.widget.ImageView; + +import com.android.gallery3d.R; +import com.android.photos.data.PhotoSetLoader; +import com.android.photos.shims.LoaderCompatShim; +import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter; + + +public class PhotoThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter { + private LayoutInflater mInflater; + private LoaderCompatShim mDrawableFactory; + + public PhotoThumbnailAdapter(Context context) { + super(context, null, false); + mInflater = LayoutInflater.from(context); + } + + public void setDrawableFactory(LoaderCompatShim factory) { + mDrawableFactory = factory; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ImageView iv = (ImageView) view; + Drawable recycle = iv.getDrawable(); + Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle); + if (recycle != drawable) { + iv.setImageDrawable(drawable); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = mInflater.inflate(R.layout.photo_set_item, parent, false); + LayoutParams params = view.getLayoutParams(); + int columnWidth = ((GridView) parent).getColumnWidth(); + params.height = columnWidth; + view.setLayoutParams(params); + return view; + } + + @Override + public float getIntrinsicAspectRatio(int position) { + Cursor cursor = getItem(position); + float width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH); + float height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT); + return width / height; + } + + @Override + public Cursor getItem(int position) { + return (Cursor) super.getItem(position); + } +} \ No newline at end of file diff --git a/src/com/android/photos/data/PhotoSetLoader.java b/src/com/android/photos/data/PhotoSetLoader.java index 78662cd6c..72c8e93cc 100644 --- a/src/com/android/photos/data/PhotoSetLoader.java +++ b/src/com/android/photos/data/PhotoSetLoader.java @@ -31,6 +31,8 @@ import com.android.photos.shims.LoaderCompatShim; public class PhotoSetLoader extends CursorLoader implements LoaderCompatShim { + public static final String SUPPORTED_OPERATIONS = "supported_operations"; + private static final Uri CONTENT_URI = Files.getContentUri("external"); public static final String[] PROJECTION = new String[] { FileColumns._ID, @@ -39,7 +41,9 @@ public class PhotoSetLoader extends CursorLoader implements LoaderCompatShim implements LoaderC mappedMediaType = FileColumns.MEDIA_TYPE_VIDEO; } row[PhotoSetLoader.INDEX_MEDIA_TYPE] = mappedMediaType; + row[PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS] = + item.getSupportedOperations(); cursor.addRow(row); mediaItems.append(index, item); } -- 2.11.0