Messaging: Use ActivityResultContracts for media and contacts

Instead of handling showing and picking the media ourselves,
use existing AcitivityResultContracts to pick images, videos
or contacts

Change-Id: I20079ad94882cc6bbfeae8d57ca0e4aa30612c01
This commit is contained in:
Michael W
2024-12-26 15:54:37 +01:00
parent 7f9087a49e
commit e335f6bec8
24 changed files with 148 additions and 1481 deletions
@@ -36,7 +36,6 @@ import com.android.messaging.datamodel.data.ConversationData.ConversationDataLis
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.LaunchConversationData;
import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
import com.android.messaging.datamodel.data.MediaPickerData;
@@ -81,8 +80,6 @@ public abstract class DataModel {
public abstract MediaPickerData createMediaPickerData(final Context context);
public abstract GalleryGridItemData createGalleryGridItemData();
public abstract LaunchConversationData createLaunchConversationData(
LaunchConversationDataListener listener);
@@ -36,7 +36,6 @@ import com.android.messaging.datamodel.data.ConversationData.ConversationDataLis
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.LaunchConversationData;
import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
import com.android.messaging.datamodel.data.MediaPickerData;
@@ -113,11 +112,6 @@ public class DataModelImpl extends DataModel {
return new MediaPickerData(context);
}
@Override
public GalleryGridItemData createGalleryGridItemData() {
return new GalleryGridItemData();
}
@Override
public LaunchConversationData createLaunchConversationData(
final LaunchConversationDataListener listener) {
@@ -1,53 +0,0 @@
/*
* Copyright (C) 2015 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.messaging.datamodel;
import android.content.Context;
import android.net.Uri;
import android.provider.MediaStore.Files;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.MediaColumns;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.google.common.base.Joiner;
/**
* A BoundCursorLoader that reads local media on the device.
*/
public class GalleryBoundCursorLoader extends BoundCursorLoader {
public static final String MEDIA_SCANNER_VOLUME_EXTERNAL = "external";
private static final Uri STORAGE_URI = Files.getContentUri(MEDIA_SCANNER_VOLUME_EXTERNAL);
private static final String SORT_ORDER = MediaColumns.DATE_MODIFIED + " DESC";
private static final String SELECTION = createSelection(
MessagePartData.ACCEPTABLE_GALLERY_MEDIA_TYPES,
new Integer[] {
FileColumns.MEDIA_TYPE_IMAGE,
FileColumns.MEDIA_TYPE_VIDEO,
FileColumns.MEDIA_TYPE_AUDIO
});
public GalleryBoundCursorLoader(final String bindingId, final Context context) {
super(bindingId, context, STORAGE_URI, GalleryGridItemData.MEDIA_PROJECTION, SELECTION,
null, SORT_ORDER);
}
private static String createSelection(final String[] mimeTypes, Integer[] mediaTypes) {
return MediaColumns.MIME_TYPE + " IN ('" + Joiner.on("','").join(mimeTypes) + "') AND "
+ FileColumns.MEDIA_TYPE + " IN (" + Joiner.on(',').join(mediaTypes) + ")";
}
}
@@ -1,157 +0,0 @@
/*
* Copyright (C) 2015 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.messaging.datamodel.data;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
import com.android.messaging.datamodel.media.FileImageRequestDescriptor;
import com.android.messaging.datamodel.media.ImageRequest;
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
import com.android.messaging.datamodel.media.VideoThumbnailRequestDescriptor;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.UriUtil;
/**
* Provides data for GalleryGridItemView
*/
public class GalleryGridItemData {
public static final String[] MEDIA_PROJECTION = new String[] {
MediaColumns._ID,
MediaColumns.DATA,
MediaColumns.WIDTH,
MediaColumns.HEIGHT,
MediaColumns.MIME_TYPE,
MediaColumns.DATE_MODIFIED,
MediaColumns.DISPLAY_NAME};
public static final String[] SPECIAL_ITEM_COLUMNS = new String[] {
BaseColumns._ID
};
private static final int INDEX_ID = 0;
// For local media gallery.
private static final int INDEX_DATA_PATH = 1;
private static final int INDEX_WIDTH = 2;
private static final int INDEX_HEIGHT = 3;
private static final int INDEX_MIME_TYPE = 4;
private static final int INDEX_DATE_MODIFIED = 5;
private static final int INDEX_DISPLAY_NAME = 6;
/** A special item's id for picking a media from document picker */
public static final String ID_DOCUMENT_PICKER_ITEM = "-1";
private UriImageRequestDescriptor mImageData;
private String mContentType;
private boolean mIsDocumentPickerItem;
private long mDateSeconds;
private String mFileName;
private Uri mAudioUri;
public GalleryGridItemData() {
}
public void bind(final Cursor cursor, final int desiredWidth, final int desiredHeight) {
mIsDocumentPickerItem = TextUtils.equals(cursor.getString(INDEX_ID),
ID_DOCUMENT_PICKER_ITEM);
if (mIsDocumentPickerItem) {
mImageData = null;
mContentType = null;
} else {
mContentType = cursor.getString(INDEX_MIME_TYPE);
final String filePath = cursor.getString(INDEX_DATA_PATH);
final String dateModified = cursor.getString(INDEX_DATE_MODIFIED);
mDateSeconds = !TextUtils.isEmpty(dateModified) ? Long.parseLong(dateModified) : -1;
if (ContentType.isAudioType(mContentType)) {
mImageData = null;
mAudioUri = UriUtil.getUriForResourceFile(filePath);
mFileName = cursor.getString(INDEX_DISPLAY_NAME);
} else { // For image and video types
int sourceWidth = cursor.getInt(INDEX_WIDTH);
int sourceHeight = cursor.getInt(INDEX_HEIGHT);
// Guard against bad data
if (sourceWidth <= 0) {
sourceWidth = ImageRequest.UNSPECIFIED_SIZE;
}
if (sourceHeight <= 0) {
sourceHeight = ImageRequest.UNSPECIFIED_SIZE;
}
if (ContentType.isVideoType(mContentType)) {
mImageData = new VideoThumbnailRequestDescriptor(
cursor.getLong(INDEX_ID),
desiredWidth,
desiredHeight,
sourceWidth,
sourceHeight);
} else {
mImageData = new FileImageRequestDescriptor(
filePath,
desiredWidth,
desiredHeight,
sourceWidth,
sourceHeight,
true /* canUseThumbnail */,
true /* allowCompression */,
true /* isStatic */);
}
}
}
}
public boolean isDocumentPickerItem() {
return mIsDocumentPickerItem;
}
public Uri getImageUri() {
return ContentType.isAudioType(mContentType) ? mAudioUri : mImageData.uri;
}
public UriImageRequestDescriptor getImageRequestDescriptor() {
return mImageData;
}
public MessagePartData constructMessagePartData(final Rect startRect) {
Assert.isTrue(!mIsDocumentPickerItem);
return ContentType.isAudioType(mContentType)
? new MediaPickerMessagePartData(startRect, mContentType, mAudioUri, 0, 0)
: new MediaPickerMessagePartData(startRect, mContentType, mImageData.uri,
mImageData.sourceWidth, mImageData.sourceHeight);
}
/**
* @return The date in seconds. This can be negative if we could not retreive date info
*/
public long getDateSeconds() {
return mDateSeconds;
}
public String getContentType() {
return mContentType;
}
public String getFileName() {
return mFileName;
}
}
@@ -18,144 +18,19 @@
package com.android.messaging.datamodel.data;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.GalleryBoundCursorLoader;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.LogUtil;
/**
* Services data needs for MediaPicker.
*/
public class MediaPickerData extends BindableData {
public interface MediaPickerDataListener {
void onMediaPickerDataUpdated(MediaPickerData mediaPickerData, Object data, int loaderId);
}
private static final String BINDING_ID = "bindingId";
private final Context mContext;
private LoaderManager mLoaderManager;
private final GalleryLoaderCallbacks mGalleryLoaderCallbacks;
private MediaPickerDataListener mListener;
public MediaPickerData(final Context context) {
mContext = context;
mGalleryLoaderCallbacks = new GalleryLoaderCallbacks();
}
public static final int GALLERY_MEDIA_LOADER = 1;
/**
* A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
*/
private class GalleryLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
@NonNull
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
final String bindingId = args.getString(BINDING_ID);
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
switch (id) {
case GALLERY_MEDIA_LOADER:
return new GalleryBoundCursorLoader(bindingId, mContext);
default:
Assert.fail("Unknown loader id for gallery picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding the media picker");
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case GALLERY_MEDIA_LOADER:
mListener.onMediaPickerDataUpdated(MediaPickerData.this, data,
GALLERY_MEDIA_LOADER);
break;
default:
Assert.fail("Unknown loader id for gallery picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding the media picker");
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case GALLERY_MEDIA_LOADER:
mListener.onMediaPickerDataUpdated(MediaPickerData.this, null,
GALLERY_MEDIA_LOADER);
break;
default:
Assert.fail("Unknown loader id for media picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding the media picker");
}
}
}
public void startLoader(final int loaderId, final BindingBase<MediaPickerData> binding,
@Nullable Bundle args, final MediaPickerDataListener listener) {
if (args == null) {
args = new Bundle();
}
args.putString(BINDING_ID, binding.getBindingId());
if (loaderId == GALLERY_MEDIA_LOADER) {
mLoaderManager.initLoader(loaderId, args, mGalleryLoaderCallbacks).forceLoad();
} else {
Assert.fail("Unsupported loader id for media picker!");
}
mListener = listener;
}
public void destroyLoader(final int loaderId) {
mLoaderManager.destroyLoader(loaderId);
}
public void init(final LoaderManager loaderManager) {
mLoaderManager = loaderManager;
}
@Override
protected void unregisterListeners() {
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(GALLERY_MEDIA_LOADER);
mLoaderManager = null;
}
}
/**
@@ -176,4 +51,8 @@ public class MediaPickerData extends BindableData {
selectedIndex);
}
@Override
protected void unregisterListeners() {
}
}
@@ -1,37 +0,0 @@
/*
* Copyright (C) 2015 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.messaging.datamodel.media;
import android.content.Context;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.UriUtil;
public class VideoThumbnailRequestDescriptor extends UriImageRequestDescriptor {
public VideoThumbnailRequestDescriptor(final long id, int desiredWidth, int desiredHeight,
int sourceWidth, int sourceHeight) {
super(UriUtil.getContentUriForMediaStoreId(id), desiredWidth, desiredHeight, sourceWidth,
sourceHeight, false /* canCompress */, false /* isStatic */,
false /* cropToCircle */,
ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
}
@Override
public MediaRequest<ImageResource> buildSyncMediaRequest(Context context) {
return new VideoThumbnailRequest(context, this);
}
}
@@ -165,20 +165,6 @@ public abstract class UIIntents {
*/
public abstract void launchAddContactActivity(final Context context, final String destination);
/**
* Launch an activity to show the document picker to pick an image/video/audio.
*
* @param fragment the requesting fragment
*/
public abstract void launchDocumentImagePicker(final Fragment fragment);
/**
* Launch an activity to show the contacts list to pick one.
*
* @param fragment the requesting fragment
*/
public abstract void launchContactCardPicker(final Fragment fragment);
/**
* Launch an activity to show people & options for a given conversation.
*/
@@ -223,12 +209,6 @@ public abstract class UIIntents {
*/
public abstract void launchSaveVCardToContactsActivity(Context context, Uri vcardUri);
/**
* Launch an activity to let the user select & unselect the list of attachments to send.
*/
public abstract void launchAttachmentChooserActivity(final Activity activity,
final String conversationId, final int requestCode);
/**
* Launch full screen video viewer.
*/
@@ -35,7 +35,6 @@ import android.provider.MediaStore;
import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.text.TextUtils;
@@ -52,7 +51,6 @@ import com.android.messaging.sms.MmsSmsUtils;
import com.android.messaging.ui.appsettings.ApplicationSettingsActivity;
import com.android.messaging.ui.appsettings.PerSubscriptionSettingsActivity;
import com.android.messaging.ui.appsettings.SettingsActivity;
import com.android.messaging.ui.attachmentchooser.AttachmentChooserActivity;
import com.android.messaging.ui.conversation.ConversationActivity;
import com.android.messaging.ui.conversation.LaunchConversationActivity;
import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity;
@@ -216,29 +214,6 @@ public class UIIntentsImpl extends UIIntents {
context.startActivity(intent);
}
@Override
public void launchDocumentImagePicker(final Fragment fragment) {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.putExtra(Intent.EXTRA_MIME_TYPES, MessagePartData.ACCEPTABLE_GALLERY_MEDIA_TYPES);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(ContentType.ANY_TYPE);
fragment.startActivityForResult(intent, REQUEST_PICK_MEDIA_FROM_DOCUMENT_PICKER);
}
@Override
public void launchContactCardPicker(final Fragment fragment) {
final Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType(Contacts.CONTENT_TYPE);
try {
fragment.startActivityForResult(intent, REQUEST_PICK_CONTACT_CARD);
} catch (final ActivityNotFoundException ex) {
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex);
UiUtils.showToastAtBottom(R.string.activity_not_found_message);
}
}
@Override
public void launchPeopleAndOptionsActivity(final Activity activity,
final String conversationId) {
@@ -290,14 +265,6 @@ public class UIIntentsImpl extends UIIntents {
startExternalActivity(context, intent);
}
@Override
public void launchAttachmentChooserActivity(final Activity activity,
final String conversationId, final int requestCode) {
final Intent intent = new Intent(activity, AttachmentChooserActivity.class);
intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
activity.startActivityForResult(intent, requestCode);
}
@Override
public void launchFullScreenVideoViewer(final Context context, final Uri videoUri) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -240,7 +240,7 @@ public class ConversationActivity extends BugleActionBarActivity
ContactPickerFragment.FRAGMENT_TAG);
}
private ConversationFragment getConversationFragment() {
public ConversationFragment getConversationFragment() {
return (ConversationFragment) getSupportFragmentManager().findFragmentByTag(
ConversationFragment.FRAGMENT_TAG);
}
@@ -366,22 +366,4 @@ public class ConversationActivity extends BugleActionBarActivity
public boolean shouldResumeComposeMessage() {
return mUiState.shouldResumeComposeMessage();
}
@SuppressWarnings("MissingSuperCall") // TODO: fix me
@Override
protected void onActivityResult(final int requestCode, final int resultCode,
final Intent data) {
if (requestCode == ConversationFragment.REQUEST_CHOOSE_ATTACHMENTS &&
resultCode == RESULT_OK) {
final ConversationFragment conversationFragment = getConversationFragment();
if (conversationFragment != null) {
conversationFragment.onAttachmentChoosen();
} else {
LogUtil.e(LogUtil.BUGLE_TAG, "ConversationFragment is missing after launching " +
"AttachmentChooserActivity!");
}
} else if (resultCode == FINISH_RESULT_CODE) {
finish();
}
}
}
@@ -49,6 +49,10 @@ import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
@@ -87,6 +91,7 @@ import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.animation.PopupTransitionAnimation;
import com.android.messaging.ui.attachmentchooser.AttachmentChooserActivity;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost;
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost;
@@ -399,6 +404,25 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
};
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
final ConversationFragment conversationFragment = getConversationFragment();
if (conversationFragment != null) {
conversationFragment.onAttachmentChoosen();
} else {
LogUtil.e(LogUtil.BUGLE_TAG,
"ConversationFragment is missing after launching " +
"AttachmentChooserActivity!");
}
}
});
public ConversationFragment getConversationFragment() {
return (ConversationFragment) getParentFragmentManager().findFragmentByTag(
ConversationFragment.FRAGMENT_TAG);
}
/**
* {@inheritDoc} from Fragment
*/
@@ -429,8 +453,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
* loading when onActivityCreated() is called, which is guaranteed to happen after both.
*/
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onViewCreated(@NonNull View view, final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Delay showing the message list until the participant list is loaded.
mRecyclerView.setVisibility(View.INVISIBLE);
mBinding.ensureBound();
@@ -1448,7 +1472,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
getActivity(), tooManyVideos);
}
public static void warnOfExceedingMessageLimit(final boolean sending,
public void warnOfExceedingMessageLimit(final boolean sending,
final ComposeMessageView composeMessageView, final String conversationId,
final Activity activity, final boolean tooManyVideos) {
final AlertDialog.Builder builder =
@@ -1478,10 +1502,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
showAttachmentChooser(mConversationId, getActivity());
}
public static void showAttachmentChooser(final String conversationId,
public void showAttachmentChooser(final String conversationId,
final Activity activity) {
UIIntents.get().launchAttachmentChooserActivity(activity,
conversationId, REQUEST_CHOOSE_ATTACHMENTS);
final Intent intent = new Intent(activity, AttachmentChooserActivity.class);
intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
mLauncher.launch(intent);
}
private void updateActionAndStatusBarColor(final ActionBar actionBar) {
@@ -90,11 +90,6 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
return isInConversationListSelectMode();
}
@SuppressWarnings("MissingSuperCall") // TODO: fix me
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
}
@Override
public void onActionBarDelete(final Collection<SelectedConversation> conversations) {
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
@@ -19,6 +19,7 @@ package com.android.messaging.ui.mediapicker;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
@@ -28,12 +29,16 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;
/**
* Chooser which allows the user to select an existing contact from contacts apps on this device.
@@ -43,9 +48,42 @@ import com.android.messaging.util.SafeAsyncTask;
class ContactMediaChooser extends MediaChooser {
private View mEnabledView;
private View mMissingPermissionView;
private final ActivityResultLauncher<Intent> mPickerLauncher;
ContactMediaChooser(final MediaPicker mediaPicker) {
super(mediaPicker);
mPickerLauncher = mediaPicker.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() != Activity.RESULT_OK ||
result.getData() == null) {
return;
}
Uri contactUri = result.getData().getData();
if (contactUri != null) {
String lookupKey = null;
try (final Cursor c = getContext().getContentResolver().query(
contactUri,
new String[]{Contacts.LOOKUP_KEY},
null,
null,
null)) {
if (c != null) {
c.moveToFirst();
lookupKey = c.getString(0);
}
}
final Uri vCardUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI,
lookupKey);
if (vCardUri != null) {
SafeAsyncTask.executeOnThreadPool(() -> {
final PendingAttachmentData pendingItem =
PendingAttachmentData.createPendingAttachmentData(
ContentType.TEXT_X_VCARD.toLowerCase(), vCardUri);
mMediaPicker.dispatchPendingItemAdded(pendingItem);
});
}
}
});
}
@Override
@@ -80,7 +118,14 @@ class ContactMediaChooser extends MediaChooser {
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
mEnabledView.setOnClickListener(v -> {
// Launch an external picker to pick a contact as attachment.
UIIntents.get().launchContactCardPicker(mMediaPicker);
final Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
try {
mPickerLauncher.launch(intent);
} catch (final ActivityNotFoundException ex) {
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex);
UiUtils.showToastAtBottom(R.string.activity_not_found_message);
}
});
return view;
}
@@ -104,35 +149,4 @@ class ContactMediaChooser extends MediaChooser {
mMissingPermissionView.setVisibility(permissionGranted ? View.GONE : View.VISIBLE);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == UIIntents.REQUEST_PICK_CONTACT_CARD
&& resultCode == Activity.RESULT_OK) {
Uri contactUri = data.getData();
if (contactUri != null) {
String lookupKey = null;
try (final Cursor c = getContext().getContentResolver().query(
contactUri,
new String[] {Contacts.LOOKUP_KEY},
null,
null,
null)) {
if (c != null) {
c.moveToFirst();
lookupKey = c.getString(0);
}
}
final Uri vCardUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
if (vCardUri != null) {
SafeAsyncTask.executeOnThreadPool(() -> {
final PendingAttachmentData pendingItem =
PendingAttachmentData.createPendingAttachmentData(
ContentType.TEXT_X_VCARD.toLowerCase(), vCardUri);
mMediaPicker.dispatchPendingItemAdded(pendingItem);
});
}
}
}
}
}
@@ -20,16 +20,23 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.fragment.app.Fragment;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.FileUtil;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.SafeAsyncTask;
import java.util.ArrayList;
import java.util.List;
/**
* Wraps around the functionalities to allow the user to pick an image/video/audio from the document
* picker. Instances of this class must be tied to a Fragment which is able to delegate activity
@@ -58,6 +65,8 @@ public class DocumentImagePicker {
private static final String EXTRA_PHOTO_URL = "photo_url";
private final ActivityResultLauncher<PickVisualMediaRequest> mPickMultipleMedia;
/**
* Creates a new instance of DocumentImagePicker.
* @param activity The activity that owns the picker, or the activity that hosts the owning
@@ -67,39 +76,29 @@ public class DocumentImagePicker {
final SelectionListener listener) {
mFragment = fragment;
mListener = listener;
mPickMultipleMedia = mFragment.registerForActivityResult(
new ActivityResultContracts.PickMultipleVisualMedia(
BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT), uris -> {
// Callback is invoked after the user selects media items or closes the
// photo picker.
if (!uris.isEmpty()) {
onDocumentsPicked(uris);
}
});
}
/**
* Intent out to open an image/video from document picker.
*/
public void launchPicker() {
UIIntents.get().launchDocumentImagePicker(mFragment);
mPickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
.build());
}
/**
* Must be called from the fragment/activity's onActivityResult().
*/
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
// Sometimes called after media item has been picked from the document picker.
String url = data.getStringExtra(EXTRA_PHOTO_URL);
if (url == null) {
// we're using the builtin photo picker which supplies the return
// url as it's "data"
url = data.getDataString();
if (url == null) {
final Bundle extras = data.getExtras();
if (extras != null) {
final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
if (uri != null) {
url = uri.toString();
}
}
}
}
// Guard against null uri cases for when the activity returns a null/invalid intent.
if (url != null) {
final Uri uri = Uri.parse(url);
public void onDocumentsPicked(List<Uri> uris) {
for (Uri uri: uris) {
prepareDocumentForAttachment(uri);
}
}
@@ -1,62 +0,0 @@
/*
* Copyright (C) 2015 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.messaging.ui.mediapicker;
import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import com.android.messaging.R;
import com.android.messaging.ui.mediapicker.GalleryGridItemView.HostInterface;
import com.android.messaging.util.Assert;
/**
* Bridges between the image cursor loaded by GalleryBoundCursorLoader and the GalleryGridView.
*/
public class GalleryGridAdapter extends CursorAdapter {
private GalleryGridItemView.HostInterface mGgivHostInterface;
public GalleryGridAdapter(final Context context, final Cursor cursor) {
super(context, cursor, 0);
}
public void setHostInterface(final HostInterface ggivHostInterface) {
mGgivHostInterface = ggivHostInterface;
}
/**
* {@inheritDoc}
*/
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
Assert.isTrue(view instanceof GalleryGridItemView);
final GalleryGridItemView galleryImageView = (GalleryGridItemView) view;
galleryImageView.bind(cursor, mGgivHostInterface);
}
/**
* {@inheritDoc}
*/
@Override
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
final LayoutInflater layoutInflater = LayoutInflater.from(context);
return layoutInflater.inflate(R.layout.gallery_grid_item_view, parent, false);
}
}
@@ -1,206 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2024 The LineageOS 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.messaging.ui.mediapicker;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.messaging.R;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.ui.AsyncImageView;
import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.util.ContentType;
import com.google.common.annotations.VisibleForTesting;
import java.util.concurrent.TimeUnit;
/**
* Shows an item in the gallery picker grid view. Hosts an FileImageView with a checkbox.
*/
public class GalleryGridItemView extends FrameLayout {
/**
* Implemented by the owner of this GalleryGridItemView instance to communicate on media
* picking and selection events.
*/
public interface HostInterface {
void onItemClicked(View view, GalleryGridItemData data, boolean longClick);
boolean isItemSelected(GalleryGridItemData data);
boolean isMultiSelectEnabled();
}
@VisibleForTesting
final
GalleryGridItemData mData;
private AsyncImageView mImageView;
private CheckBox mCheckBox;
private RelativeLayout mAdditionalInfo;
private ImageView mIcon;
private LinearLayout mFileInfo;
private TextView mFileName;
private TextView mFileType;
private HostInterface mHostInterface;
private final OnClickListener mOnClickListener = new OnClickListener() {
@Override
public void onClick(final View v) {
mHostInterface.onItemClicked(GalleryGridItemView.this, mData, false /*longClick*/);
}
};
public GalleryGridItemView(final Context context, final AttributeSet attrs) {
super(context, attrs);
mData = DataModel.get().createGalleryGridItemData();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = (AsyncImageView) findViewById(R.id.thumbnail);
mCheckBox = (CheckBox) findViewById(R.id.checkbox);
mCheckBox.setOnClickListener(mOnClickListener);
mAdditionalInfo = (RelativeLayout) findViewById(R.id.additional_info);
mIcon = (ImageView) findViewById(R.id.icon);
mFileInfo = (LinearLayout) findViewById(R.id.file_info);
mFileName = (TextView) findViewById(R.id.file_name);
mFileType = (TextView) findViewById(R.id.file_type);
setOnClickListener(mOnClickListener);
final OnLongClickListener longClickListener = v -> {
mHostInterface.onItemClicked(v, mData, true /* longClick */);
return true;
};
setOnLongClickListener(longClickListener);
mCheckBox.setOnLongClickListener(longClickListener);
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
// Enlarge the clickable region for the checkbox to fill the entire view.
final Rect region = new Rect(0, 0, getWidth(), getHeight());
setTouchDelegate(new TouchDelegate(region, mCheckBox) {
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setPressed(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setPressed(false);
break;
}
return super.onTouchEvent(event);
}
});
}
});
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
// The grid view auto-fit the columns, so we want to let the height match the width
// to make the image square.
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
public void bind(final Cursor cursor, final HostInterface hostInterface) {
final int desiredSize = getResources()
.getDimensionPixelSize(R.dimen.gallery_image_cell_size);
mData.bind(cursor, desiredSize, desiredSize);
mHostInterface = hostInterface;
updateViewState();
}
private void updateViewState() {
updateImageView();
if (mHostInterface.isMultiSelectEnabled() && !mData.isDocumentPickerItem()) {
mCheckBox.setVisibility(VISIBLE);
mCheckBox.setClickable(true);
mCheckBox.setChecked(mHostInterface.isItemSelected(mData));
} else {
mCheckBox.setVisibility(GONE);
mCheckBox.setClickable(false);
}
}
private void updateImageView() {
if (mData.isDocumentPickerItem()) {
setBackgroundColor(ConversationDrawables.get().getConversationThemeColor());
mIcon.setImageResource(R.drawable.ic_photo_library_light);
mIcon.clearColorFilter();
mImageView.setVisibility(GONE);
mIcon.setVisibility(VISIBLE);
mFileInfo.setVisibility(GONE);
mAdditionalInfo.setVisibility(VISIBLE);
} else {
final String contentType = mData.getContentType();
Resources.Theme theme = getContext().getTheme();
if (ContentType.isAudioType(contentType)) {
setBackgroundColor(
getResources().getColor(R.color.gallery_image_default_background, theme));
mIcon.setImageResource(R.drawable.ic_music);
mIcon.setColorFilter(
ConversationDrawables.get().getConversationThemeColor(),
PorterDuff.Mode.SRC_IN);
mFileName.setText(mData.getFileName());
String[] type = contentType.split("/");
mFileType.setText(type[1].toUpperCase() + " " + type[0]);
mImageView.setVisibility(GONE);
mIcon.setVisibility(VISIBLE);
mFileInfo.setVisibility(VISIBLE);
mAdditionalInfo.setVisibility(VISIBLE);
} else { // For image and video types
mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
setBackgroundColor(
getResources().getColor(R.color.gallery_image_default_background, theme));
mImageView.setImageResourceId(mData.getImageRequestDescriptor());
mImageView.setVisibility(VISIBLE);
if (ContentType.isVideoType(mData.getContentType())) {
mIcon.setImageResource(R.drawable.ic_play_light);
mIcon.clearColorFilter();
mIcon.setVisibility(VISIBLE);
} else {
mIcon.setVisibility(GONE);
}
mFileInfo.setVisibility(GONE);
mAdditionalInfo.setVisibility(VISIBLE);
final long dateSeconds = mData.getDateSeconds();
final boolean isValidDate = (dateSeconds > 0);
final int templateId = isValidDate ?
R.string.mediapicker_gallery_image_item_description :
R.string.mediapicker_gallery_image_item_description_no_date;
String contentDescription = String.format(getResources().getString(templateId),
dateSeconds * TimeUnit.SECONDS.toMillis(1));
mImageView.setContentDescription(contentDescription);
}
}
}
}
@@ -1,317 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2024 The LineageOS 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.messaging.ui.mediapicker;
import android.content.Context;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.collection.ArrayMap;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import com.android.messaging.R;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
import com.android.messaging.ui.PersistentInstanceState;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import java.util.Iterator;
import java.util.Map;
/**
* Shows a list of galley mediae from external storage in a GridView with multi-select capabilities,
* and with the option to intent out to a standalone media picker.
*/
public class GalleryGridView extends MediaPickerGridView implements
GalleryGridItemView.HostInterface,
PersistentInstanceState,
DraftMessageDataListener {
/**
* Implemented by the owner of this GalleryGridView instance to communicate on media picking and
* multi-media selection events.
*/
public interface GalleryGridViewListener {
void onDocumentPickerItemClicked();
void onItemSelected(MessagePartData item);
void onItemUnselected(MessagePartData item);
void onConfirmSelection();
void onUpdate();
}
private GalleryGridViewListener mListener;
// TODO: Consider putting this into the data model object if we add more states.
private final ArrayMap<Uri, MessagePartData> mSelectedImages;
private boolean mIsMultiSelectMode = false;
private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel;
public GalleryGridView(final Context context, final AttributeSet attrs) {
super(context, attrs);
mSelectedImages = new ArrayMap<>();
}
public void setHostInterface(final GalleryGridViewListener hostInterface) {
mListener = hostInterface;
}
public void setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel) {
mDraftMessageDataModel = BindingBase.createBindingReference(dataModel);
mDraftMessageDataModel.getData().addListener(this);
}
@Override
public void onItemClicked(final View view, final GalleryGridItemData data,
final boolean longClick) {
if (data.isDocumentPickerItem()) {
mListener.onDocumentPickerItemClicked();
} else if (ContentType.isMediaType(data.getContentType())) {
if (longClick) {
// Turn on multi-select mode when an item is long-pressed.
setMultiSelectEnabled(true);
}
final Rect startRect = new Rect();
view.getGlobalVisibleRect(startRect);
if (isMultiSelectEnabled()) {
toggleItemSelection(startRect, data);
} else {
mListener.onItemSelected(data.constructMessagePartData(startRect));
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG,
"Selected item has invalid contentType " + data.getContentType());
}
}
@Override
public boolean isItemSelected(final GalleryGridItemData data) {
return mSelectedImages.containsKey(data.getImageUri());
}
int getSelectionCount() {
return mSelectedImages.size();
}
@Override
public boolean isMultiSelectEnabled() {
return mIsMultiSelectMode;
}
private void toggleItemSelection(final Rect startRect, final GalleryGridItemData data) {
Assert.isTrue(isMultiSelectEnabled());
if (isItemSelected(data)) {
final MessagePartData item = mSelectedImages.remove(data.getImageUri());
mListener.onItemUnselected(item);
if (mSelectedImages.size() == 0) {
// No media is selected any more, turn off multi-select mode.
setMultiSelectEnabled(false);
}
} else {
final MessagePartData item = data.constructMessagePartData(startRect);
mSelectedImages.put(data.getImageUri(), item);
mListener.onItemSelected(item);
}
invalidateViews();
}
private void toggleMultiSelect() {
mIsMultiSelectMode = !mIsMultiSelectMode;
invalidateViews();
}
private void setMultiSelectEnabled(final boolean enabled) {
if (mIsMultiSelectMode != enabled) {
toggleMultiSelect();
}
}
private boolean canToggleMultiSelect() {
// We allow the user to toggle multi-select mode only when nothing has selected. If
// something has been selected, we show a confirm button instead.
return mSelectedImages.size() == 0;
}
public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) {
inflater.inflate(R.menu.gallery_picker_menu, menu);
final MenuItem toggleMultiSelect = menu.findItem(R.id.action_multiselect);
final MenuItem confirmMultiSelect = menu.findItem(R.id.action_confirm_multiselect);
final boolean canToggleMultiSelect = canToggleMultiSelect();
toggleMultiSelect.setVisible(canToggleMultiSelect);
confirmMultiSelect.setVisible(!canToggleMultiSelect);
}
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_multiselect:
Assert.isTrue(canToggleMultiSelect());
toggleMultiSelect();
return true;
case R.id.action_confirm_multiselect:
Assert.isTrue(!canToggleMultiSelect());
mListener.onConfirmSelection();
return true;
}
return false;
}
@Override
public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
mDraftMessageDataModel.ensureBound(data);
// Whenever attachment changed, refresh selection state to remove those that are not
// selected.
if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) ==
DraftMessageData.ATTACHMENTS_CHANGED) {
refreshImageSelectionStateOnAttachmentChange();
}
}
@Override
public void onDraftAttachmentLimitReached(final DraftMessageData data) {
mDraftMessageDataModel.ensureBound(data);
// Whenever draft attachment limit is reach, refresh selection state to remove those
// not actually added to draft.
refreshImageSelectionStateOnAttachmentChange();
}
@Override
public void onDraftAttachmentLoadFailed() {
// Nothing to do since the failed attachment gets removed automatically.
}
private void refreshImageSelectionStateOnAttachmentChange() {
boolean changed = false;
final Iterator<Map.Entry<Uri, MessagePartData>> iterator =
mSelectedImages.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Uri, MessagePartData> entry = iterator.next();
if (!mDraftMessageDataModel.getData().containsAttachment(entry.getKey())) {
iterator.remove();
changed = true;
}
}
if (changed) {
mListener.onUpdate();
invalidateViews();
}
}
@Override // PersistentInstanceState
public Parcelable saveState() {
return onSaveInstanceState();
}
@Override // PersistentInstanceState
public void restoreState(final Parcelable restoredState) {
onRestoreInstanceState(restoredState);
invalidateViews();
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState savedState = new SavedState(superState);
savedState.isMultiSelectMode = mIsMultiSelectMode;
savedState.selectedImages = mSelectedImages.values()
.toArray(new MessagePartData[mSelectedImages.size()]);
return savedState;
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
final SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
mIsMultiSelectMode = savedState.isMultiSelectMode;
mSelectedImages.clear();
for (int i = 0; i < savedState.selectedImages.length; i++) {
final MessagePartData selectedImage = savedState.selectedImages[i];
mSelectedImages.put(selectedImage.getContentUri(), selectedImage);
}
}
@Override // PersistentInstanceState
public void resetState() {
mSelectedImages.clear();
mIsMultiSelectMode = false;
invalidateViews();
}
public static class SavedState extends BaseSavedState {
boolean isMultiSelectMode;
MessagePartData[] selectedImages;
SavedState(final Parcelable superState) {
super(superState);
}
private SavedState(final Parcel in) {
super(in);
isMultiSelectMode = in.readInt() == 1 ? true : false;
// Read parts
final int partCount = in.readInt();
selectedImages = new MessagePartData[partCount];
for (int i = 0; i < partCount; i++) {
selectedImages[i] = ((MessagePartData) in.readParcelable(
MessagePartData.class.getClassLoader()));
}
}
@Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeInt(isMultiSelectMode ? 1 : 0);
// Write parts
out.writeInt(selectedImages.length);
for (final MessagePartData image : selectedImages) {
out.writeParcelable(image, flags);
}
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<>() {
@Override
public SavedState createFromParcel(final Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
}
}
@@ -17,46 +17,21 @@
package com.android.messaging.ui.mediapicker;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import androidx.appcompat.app.ActionBar;
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 com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.MediaPickerData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.MediaPickerData.MediaPickerDataListener;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.Assert;
import com.android.messaging.util.OsUtil;
/**
* Chooser which allows the user to select one or more existing images or videos or audios.
*/
class GalleryMediaChooser extends MediaChooser implements
GalleryGridView.GalleryGridViewListener, MediaPickerDataListener {
private final GalleryGridAdapter mAdapter;
private GalleryGridView mGalleryGridView;
private View mMissingPermissionView;
class GalleryMediaChooser extends MediaChooser {
/** Handles picking a media from the document picker. */
private final DocumentImagePicker mDocumentImagePicker;
GalleryMediaChooser(final MediaPicker mediaPicker) {
super(mediaPicker);
mAdapter = new GalleryGridAdapter(Factory.get().getApplicationContext(), null);
mDocumentImagePicker = new DocumentImagePicker(mMediaPicker, data -> {
if (mBindingRef.isBound()) {
mMediaPicker.dispatchPendingItemAdded(data);
@@ -71,17 +46,6 @@ class GalleryMediaChooser extends MediaChooser implements
| MediaPicker.MEDIA_TYPE_AUDIO);
}
@Override
public View destroyView() {
mGalleryGridView.setAdapter(null);
mAdapter.setHostInterface(null);
// The loader is started only if startMediaPickerDataLoader() is called
if (OsUtil.hasStoragePermission()) {
mBindingRef.getData().destroyLoader(MediaPickerData.GALLERY_MEDIA_LOADER);
}
return super.destroyView();
}
@Override
public int getIconResource() {
return R.drawable.ic_image_light;
@@ -92,45 +56,6 @@ class GalleryMediaChooser extends MediaChooser implements
return R.string.mediapicker_galleryChooserDescription;
}
@Override
public boolean canSwipeDown() {
return mGalleryGridView.canSwipeDown();
}
@Override
public void onItemSelected(final MessagePartData item) {
mMediaPicker.dispatchItemsSelected(item, !mGalleryGridView.isMultiSelectEnabled());
}
@Override
public void onItemUnselected(final MessagePartData item) {
mMediaPicker.dispatchItemUnselected(item);
}
@Override
public void onConfirmSelection() {
// The user may only confirm if multiselect is enabled.
Assert.isTrue(mGalleryGridView.isMultiSelectEnabled());
mMediaPicker.dispatchConfirmItemSelection();
}
@Override
public void onUpdate() {
mMediaPicker.invalidateOptionsMenu();
}
@Override
public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) {
if (mView != null) {
mGalleryGridView.onCreateOptionsMenu(inflater, menu);
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
return (mView != null) ? mGalleryGridView.onOptionsItemSelected(item) : false;
}
@Override
protected View createView(final ViewGroup container) {
final LayoutInflater inflater = getLayoutInflater();
@@ -139,17 +64,11 @@ class GalleryMediaChooser extends MediaChooser implements
container /* root */,
false /* attachToRoot */);
mGalleryGridView = (GalleryGridView) view.findViewById(R.id.gallery_grid_view);
mAdapter.setHostInterface(mGalleryGridView);
mGalleryGridView.setAdapter(mAdapter);
mGalleryGridView.setHostInterface(this);
mGalleryGridView.setDraftMessageDataModel(mMediaPicker.getDraftMessageDataModel());
if (OsUtil.hasStoragePermission()) {
startMediaPickerDataLoader();
}
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
updateForPermissionState(OsUtil.hasStoragePermission());
final View enabledView = view.findViewById(R.id.mediapicker_enabled);
enabledView.setOnClickListener(v -> {
// Launch an external picker to pick item from document picker as attachment.
mDocumentImagePicker.launchPicker();
});
return view;
}
@@ -157,99 +76,4 @@ class GalleryMediaChooser extends MediaChooser implements
int getActionBarTitleResId() {
return R.string.mediapicker_gallery_title;
}
@Override
public void onDocumentPickerItemClicked() {
// Launch an external picker to pick item from document picker as attachment.
mDocumentImagePicker.launchPicker();
}
@Override
void updateActionBar(final ActionBar actionBar) {
super.updateActionBar(actionBar);
if (mGalleryGridView == null) {
return;
}
final int selectionCount = mGalleryGridView.getSelectionCount();
if (selectionCount > 0 && mGalleryGridView.isMultiSelectEnabled()) {
actionBar.setTitle(getContext().getResources().getString(
R.string.mediapicker_gallery_title_selection,
selectionCount));
}
}
@Override
public void onMediaPickerDataUpdated(final MediaPickerData mediaPickerData, final Object data,
final int loaderId) {
mBindingRef.ensureBound(mediaPickerData);
Assert.equals(MediaPickerData.GALLERY_MEDIA_LOADER, loaderId);
Cursor rawCursor = null;
if (data instanceof Cursor) {
rawCursor = (Cursor) data;
}
// Before delivering the cursor, wrap around the local gallery cursor
// with an extra item for document picker integration in the front.
final MatrixCursor specialItemsCursor =
new MatrixCursor(GalleryGridItemData.SPECIAL_ITEM_COLUMNS);
specialItemsCursor.addRow(new Object[] { GalleryGridItemData.ID_DOCUMENT_PICKER_ITEM });
final MergeCursor cursor =
new MergeCursor(new Cursor[] { specialItemsCursor, rawCursor });
mAdapter.swapCursor(cursor);
}
@Override
public void onResume() {
if (OsUtil.hasStoragePermission()) {
// Work around a bug in MediaStore where cursors querying the Files provider don't get
// updated for changes to Images.Media or Video.Media.
startMediaPickerDataLoader();
}
}
@Override
protected void setSelected(final boolean selected) {
super.setSelected(selected);
if (selected && !OsUtil.hasStoragePermission()) {
mMediaPicker.requestPermissions(
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
MediaPicker.GALLERY_PERMISSION_REQUEST_CODE);
}
}
private void startMediaPickerDataLoader() {
mBindingRef
.getData()
.startLoader(MediaPickerData.GALLERY_MEDIA_LOADER, mBindingRef, null, this);
}
@Override
protected void onRequestPermissionsResult(
final int requestCode, final String[] permissions, final int[] grantResults) {
if (requestCode == MediaPicker.GALLERY_PERMISSION_REQUEST_CODE) {
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (permissionGranted) {
startMediaPickerDataLoader();
}
updateForPermissionState(permissionGranted);
}
}
private void updateForPermissionState(final boolean granted) {
// onRequestPermissionsResult can sometimes get called before createView().
if (mGalleryGridView == null) {
return;
}
mGalleryGridView.setVisibility(granted ? View.VISIBLE : View.GONE);
mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE);
}
@Override
protected void onActivityResult(
final int requestCode, final int resultCode, final Intent data) {
if (requestCode == UIIntents.REQUEST_PICK_MEDIA_FROM_DOCUMENT_PICKER
&& resultCode == Activity.RESULT_OK) {
mDocumentImagePicker.onActivityResult(requestCode, resultCode, data);
}
}
}
@@ -18,7 +18,6 @@
package com.android.messaging.ui.mediapicker;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -197,9 +196,6 @@ abstract class MediaChooser extends BasePagerViewHolder
public void stopTouchHandling() {
}
protected void onActivityResult(
final int requestCode, final int resultCode, final Intent data) {}
@Override
public int getConversationSelfSubId() {
return mMediaPicker.getConversationSelfSubId();
@@ -19,7 +19,6 @@ package com.android.messaging.ui.mediapicker;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
@@ -34,7 +33,6 @@ import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
@@ -45,10 +43,10 @@ import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
import com.android.messaging.datamodel.data.MediaPickerData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.FixedViewPagerAdapter;
import com.android.messaging.util.AccessibilityUtil;
@@ -202,7 +200,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding.getData().init(LoaderManager.getInstance(this));
}
@Override
@@ -283,12 +280,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
}
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mSelectedChooser.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onDestroy() {
super.onDestroy();
@@ -442,7 +433,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
if (mBinding.isBound() && getActivity() != null) {
mBinding.unbind();
mBinding.bind(DataModel.get().createMediaPickerData(getActivity()));
mBinding.getData().init(LoaderManager.getInstance(this));
}
}
@@ -27,7 +27,6 @@ import androidx.annotation.NonNull;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.GalleryBoundCursorLoader;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.google.common.io.ByteStreams;
@@ -136,18 +135,6 @@ public class UriUtil {
TextUtils.equals(MEDIA_STORE_URI_KLP, uriAuthority));
}
/**
* Gets the content:// style URI for the given MediaStore row Id in the files table on the
* external volume.
*
* @param id the MediaStore row Id to get the URI for
* @return the URI to the files table on the external storage.
*/
public static Uri getContentUriForMediaStoreId(final long id) {
return MediaStore.Files.getContentUri(
GalleryBoundCursorLoader.MEDIA_SCANNER_VOLUME_EXTERNAL, id);
}
/**
* Gets the size in bytes for the content uri. Currently we only support content in the
* scratch space.