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
@@ -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));
}
}