1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.contentpreview; 18 19 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE; 20 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; 21 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION; 22 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT; 23 24 import android.content.ClipData; 25 import android.content.Intent; 26 import android.content.res.Resources; 27 import android.net.Uri; 28 import android.text.TextUtils; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 33 import androidx.annotation.Nullable; 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.intentresolver.ContentTypeHint; 37 import com.android.intentresolver.widget.ActionRow; 38 import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; 39 40 import kotlinx.coroutines.CoroutineScope; 41 42 import java.util.List; 43 import java.util.function.Consumer; 44 import java.util.function.Supplier; 45 46 /** 47 * Collection of helpers for building the content preview UI displayed in 48 * {@link com.android.intentresolver.ChooserActivity}. 49 * A content preview façade. 50 */ 51 public final class ChooserContentPreviewUi { 52 53 private final CoroutineScope mScope; 54 private final boolean mIsPayloadTogglingEnabled; 55 56 /** 57 * Delegate to build the default system action buttons to display in the preview layout, if/when 58 * they're determined to be appropriate for the particular preview we display. 59 * TODO: clarify why action buttons are part of preview logic. 60 */ 61 public interface ActionFactory { 62 /** 63 * @return Runnable to be run when an edit button is clicked (if available). 64 */ 65 @Nullable getEditButtonRunnable()66 Runnable getEditButtonRunnable(); 67 68 /** 69 * @return Runnable to be run when a copy button is clicked (if available). 70 */ 71 @Nullable getCopyButtonRunnable()72 Runnable getCopyButtonRunnable(); 73 74 /** Create custom actions */ createCustomActions()75 List<ActionRow.Action> createCustomActions(); 76 77 /** 78 * Provides a share modification action, if any. 79 */ 80 @Nullable getModifyShareAction()81 default ActionRow.Action getModifyShareAction() { 82 return null; 83 } 84 85 /** 86 * <p> 87 * Creates an exclude-text action that can be called when the user changes shared text 88 * status in the Media + Text preview. 89 * </p> 90 * <p> 91 * <code>true</code> argument value indicates that the text should be excluded. 92 * </p> 93 */ getExcludeSharedTextAction()94 Consumer<Boolean> getExcludeSharedTextAction(); 95 } 96 97 @VisibleForTesting 98 final ContentPreviewUi mContentPreviewUi; 99 private final Supplier</*@Nullable*/ActionRow.Action> mModifyShareActionFactory; 100 private View mHeadlineParent; 101 ChooserContentPreviewUi( CoroutineScope scope, PreviewDataProvider previewData, Intent targetIntent, ImageLoader imageLoader, ActionFactory actionFactory, Supplier< ActionRow.Action> modifyShareActionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, @Nullable CharSequence metadata, boolean isPayloadTogglingEnabled)102 public ChooserContentPreviewUi( 103 CoroutineScope scope, 104 PreviewDataProvider previewData, 105 Intent targetIntent, 106 ImageLoader imageLoader, 107 ActionFactory actionFactory, 108 Supplier</*@Nullable*/ActionRow.Action> modifyShareActionFactory, 109 TransitionElementStatusCallback transitionElementStatusCallback, 110 HeadlineGenerator headlineGenerator, 111 ContentTypeHint contentTypeHint, 112 @Nullable CharSequence metadata, 113 // TODO: replace with the FeatureFlag ref when v1 is gone 114 boolean isPayloadTogglingEnabled) { 115 mScope = scope; 116 mIsPayloadTogglingEnabled = isPayloadTogglingEnabled; 117 mModifyShareActionFactory = modifyShareActionFactory; 118 mContentPreviewUi = createContentPreview( 119 previewData, 120 targetIntent, 121 DefaultMimeTypeClassifier.INSTANCE, 122 imageLoader, 123 actionFactory, 124 transitionElementStatusCallback, 125 headlineGenerator, 126 contentTypeHint, 127 metadata 128 ); 129 if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { 130 transitionElementStatusCallback.onAllTransitionElementsReady(); 131 } 132 } 133 createContentPreview( PreviewDataProvider previewData, Intent targetIntent, MimeTypeClassifier typeClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, @Nullable CharSequence metadata )134 private ContentPreviewUi createContentPreview( 135 PreviewDataProvider previewData, 136 Intent targetIntent, 137 MimeTypeClassifier typeClassifier, 138 ImageLoader imageLoader, 139 ActionFactory actionFactory, 140 TransitionElementStatusCallback transitionElementStatusCallback, 141 HeadlineGenerator headlineGenerator, 142 ContentTypeHint contentTypeHint, 143 @Nullable CharSequence metadata 144 ) { 145 int previewType = previewData.getPreviewType(); 146 if (previewType == CONTENT_PREVIEW_TEXT) { 147 return createTextPreview( 148 mScope, 149 targetIntent, 150 actionFactory, 151 imageLoader, 152 headlineGenerator, 153 contentTypeHint, 154 metadata 155 ); 156 } 157 if (previewType == CONTENT_PREVIEW_FILE) { 158 FileContentPreviewUi fileContentPreviewUi = new FileContentPreviewUi( 159 previewData.getUriCount(), 160 actionFactory, 161 headlineGenerator, 162 metadata 163 ); 164 if (previewData.getUriCount() > 0) { 165 previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName); 166 } 167 return fileContentPreviewUi; 168 } 169 170 if (previewType == CONTENT_PREVIEW_PAYLOAD_SELECTION && mIsPayloadTogglingEnabled) { 171 transitionElementStatusCallback.onAllTransitionElementsReady(); // TODO 172 return new ShareouselContentPreviewUi(); 173 } 174 175 boolean isSingleImageShare = previewData.getUriCount() == 1 176 && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType()); 177 CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); 178 if (!TextUtils.isEmpty(text)) { 179 FilesPlusTextContentPreviewUi previewUi = 180 new FilesPlusTextContentPreviewUi( 181 mScope, 182 isSingleImageShare, 183 previewData.getUriCount(), 184 targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), 185 targetIntent.getType(), 186 actionFactory, 187 imageLoader, 188 typeClassifier, 189 headlineGenerator, 190 metadata 191 ); 192 if (previewData.getUriCount() > 0) { 193 JavaFlowHelper.collectToList( 194 mScope, 195 previewData.getImagePreviewFileInfoFlow(), 196 previewUi::updatePreviewMetadata); 197 } 198 return previewUi; 199 } 200 201 return new UnifiedContentPreviewUi( 202 mScope, 203 isSingleImageShare, 204 targetIntent.getType(), 205 actionFactory, 206 imageLoader, 207 typeClassifier, 208 transitionElementStatusCallback, 209 previewData.getImagePreviewFileInfoFlow(), 210 previewData.getUriCount(), 211 headlineGenerator, 212 metadata 213 ); 214 } 215 getPreferredContentPreview()216 public int getPreferredContentPreview() { 217 return mContentPreviewUi.getType(); 218 } 219 220 /** 221 * Display a content preview of the specified {@code previewType} to preview the content of the 222 * specified {@code intent}. 223 */ displayContentPreview( Resources resources, LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent)224 public ViewGroup displayContentPreview( 225 Resources resources, 226 LayoutInflater layoutInflater, 227 ViewGroup parent, 228 View headlineViewParent) { 229 230 ViewGroup layout = 231 mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent); 232 mHeadlineParent = headlineViewParent; 233 ContentPreviewUi.displayModifyShareAction(mHeadlineParent, mModifyShareActionFactory.get()); 234 return layout; 235 } 236 237 /** 238 * Update Modify Share Action, if it is inflated. 239 */ updateModifyShareAction()240 public void updateModifyShareAction() { 241 ContentPreviewUi.displayModifyShareAction(mHeadlineParent, mModifyShareActionFactory.get()); 242 } 243 createTextPreview( CoroutineScope scope, Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, @Nullable CharSequence metadata )244 private static TextContentPreviewUi createTextPreview( 245 CoroutineScope scope, 246 Intent targetIntent, 247 ChooserContentPreviewUi.ActionFactory actionFactory, 248 ImageLoader imageLoader, 249 HeadlineGenerator headlineGenerator, 250 ContentTypeHint contentTypeHint, 251 @Nullable CharSequence metadata 252 ) { 253 CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); 254 CharSequence previewTitle = targetIntent.getCharSequenceExtra(Intent.EXTRA_TITLE); 255 ClipData previewData = targetIntent.getClipData(); 256 Uri previewThumbnail = null; 257 if (previewData != null) { 258 if (previewData.getItemCount() > 0) { 259 ClipData.Item previewDataItem = previewData.getItemAt(0); 260 previewThumbnail = previewDataItem.getUri(); 261 } 262 } 263 264 return new TextContentPreviewUi( 265 scope, 266 sharingText, 267 previewTitle, 268 metadata, 269 previewThumbnail, 270 actionFactory, 271 imageLoader, 272 headlineGenerator, 273 contentTypeHint); 274 } 275 } 276