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-18 14:58:51 +01:00
parent 7f9087a49e
commit e335f6bec8
24 changed files with 148 additions and 1481 deletions

View File

@@ -59,6 +59,7 @@ android_app {
defaults: ["messaging_defaults"],
static_libs: [
"androidx.activity_activity",
"androidx.annotation_annotation",
"androidx.appcompat_appcompat",
"androidx.collection_collection",

View File

@@ -1,103 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<com.android.messaging.ui.mediapicker.GalleryGridItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/gallery_image_cell_size"
android:background="@color/gallery_image_default_background"
android:clickable="true">
<!-- Thumbnail for image and video contents. -->
<com.android.messaging.ui.AsyncImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<!-- Additional info such as icon, name and etc. -->
<RelativeLayout
android:id="@+id/additional_info"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone" >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/file_info" >
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/gallery_icon_size"
android:layout_height="@dimen/gallery_icon_size"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:background="@color/background_item_transparent"
android:visibility="gone" />
</FrameLayout>
<!-- File info for audio contents only -->
<LinearLayout
android:id="@+id/file_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:orientation="vertical"
android:visibility="gone" >
<TextView
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@android:style/TextAppearance.Material.Subhead" />
<TextView
android:id="@+id/file_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:gravity="right"
android:textAppearance="@android:style/TextAppearance.Material.Caption" />
</LinearLayout>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
android:background="@drawable/gallery_image_background_selector" />
<CheckBox
android:id="@+id/checkbox"
style="@style/GalleryGridItemViewCheckBoxStyle"
android:button="@drawable/gallery_checkbox_selector"
android:background="@null"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:paddingTop="4dp"
android:visibility="gone"
android:contentDescription="@string/gallery_checkbox_content_description" />
</com.android.messaging.ui.mediapicker.GalleryGridItemView>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
@@ -16,29 +17,35 @@
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<FrameLayout
android:id="@+id/mediapicker_enabled"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.android.messaging.ui.mediapicker.GalleryGridView
android:id="@+id/gallery_grid_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
android:columnWidth="@dimen/gallery_image_cell_size"
android:numColumns="auto_fit"
android:verticalSpacing="6dp"
android:horizontalSpacing="6dp"
android:stretchMode="columnWidth"
android:gravity="center"
android:clipToPadding="false"
android:background="@android:color/white" />
<!-- This view will hide all other views if the required permission is not granted -->
<TextView
android:id="@+id/missing_permission_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/enable_permission_procedure"
android:contentDescription="@string/enable_permission_procedure_description"
android:background="@android:color/white"
android:gravity="center"
android:visibility="gone" />
style="@style/ContactPickerHintText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center"
android:layout_marginTop="16dp"
android:text="@string/mediapicker_galleryChooserDescription"
android:importantForAccessibility="no" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="center"
android:src="@drawable/ic_image_light"
android:scaleX="1.33"
android:scaleY="1.33"
app:tint="@color/primary_color"
android:importantForAccessibility="no"
android:contentDescription="@null" />
</FrameLayout>
</FrameLayout>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:appcompat="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" >
<item
android:id="@+id/action_multiselect"
android:icon="@drawable/ic_checkbox_outline_light"
android:orderInCategory="100"
android:title="@string/action_multiselect"
appcompat:showAsAction="always"/>
<item
android:id="@+id/action_confirm_multiselect"
android:icon="@drawable/ic_checkmark_light"
android:orderInCategory="101"
android:title="@string/action_confirm_multiselect"
appcompat:showAsAction="always"/>
</menu>

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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() {
}
}

View File

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

View File

@@ -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.
*/

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.