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.res.Resources; 26 import android.net.Uri; 27 import android.text.TextUtils; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewGroup; 31 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 35 import com.android.intentresolver.ContentTypeHint; 36 import com.android.intentresolver.data.model.ChooserRequest; 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 55 /** 56 * Delegate to build the default system action buttons to display in the preview layout, if/when 57 * they're determined to be appropriate for the particular preview we display. 58 * TODO: clarify why action buttons are part of preview logic. 59 */ 60 public interface ActionFactory { 61 /** 62 * @return Runnable to be run when an edit button is clicked (if available). 63 */ 64 @Nullable getEditButtonRunnable()65 Runnable getEditButtonRunnable(); 66 67 /** 68 * @return Runnable to be run when a copy button is clicked (if available). 69 */ 70 @Nullable getCopyButtonRunnable()71 Runnable getCopyButtonRunnable(); 72 73 /** Create custom actions */ createCustomActions()74 List<ActionRow.Action> createCustomActions(); 75 76 /** 77 * Provides a share modification action, if any. 78 */ 79 @Nullable getModifyShareAction()80 default ActionRow.Action getModifyShareAction() { 81 return null; 82 } 83 84 /** 85 * <p> 86 * Creates an exclude-text action that can be called when the user changes shared text 87 * status in the Media + Text preview. 88 * </p> 89 * <p> 90 * <code>true</code> argument value indicates that the text should be excluded. 91 * </p> 92 */ getExcludeSharedTextAction()93 Consumer<Boolean> getExcludeSharedTextAction(); 94 } 95 96 @VisibleForTesting 97 final ContentPreviewUi mContentPreviewUi; 98 private final Supplier</*@Nullable*/ActionRow.Action> mModifyShareActionFactory; 99 private View mHeadlineParent; 100 ChooserContentPreviewUi( CoroutineScope scope, PreviewDataProvider previewData, ChooserRequest chooserRequest, ImageLoader imageLoader, ActionFactory actionFactory, Supplier< ActionRow.Action> modifyShareActionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, @Nullable CharSequence metadata)101 public ChooserContentPreviewUi( 102 CoroutineScope scope, 103 PreviewDataProvider previewData, 104 ChooserRequest chooserRequest, 105 ImageLoader imageLoader, 106 ActionFactory actionFactory, 107 Supplier</*@Nullable*/ActionRow.Action> modifyShareActionFactory, 108 TransitionElementStatusCallback transitionElementStatusCallback, 109 HeadlineGenerator headlineGenerator, 110 ContentTypeHint contentTypeHint, 111 @Nullable CharSequence metadata) { 112 mScope = scope; 113 mModifyShareActionFactory = modifyShareActionFactory; 114 mContentPreviewUi = createContentPreview( 115 previewData, 116 chooserRequest, 117 DefaultMimeTypeClassifier.INSTANCE, 118 imageLoader, 119 actionFactory, 120 transitionElementStatusCallback, 121 headlineGenerator, 122 contentTypeHint, 123 metadata 124 ); 125 if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { 126 transitionElementStatusCallback.onAllTransitionElementsReady(); 127 } 128 } 129 createContentPreview( PreviewDataProvider previewData, ChooserRequest chooserRequest, MimeTypeClassifier typeClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, @Nullable CharSequence metadata )130 private ContentPreviewUi createContentPreview( 131 PreviewDataProvider previewData, 132 ChooserRequest chooserRequest, 133 MimeTypeClassifier typeClassifier, 134 ImageLoader imageLoader, 135 ActionFactory actionFactory, 136 TransitionElementStatusCallback transitionElementStatusCallback, 137 HeadlineGenerator headlineGenerator, 138 ContentTypeHint contentTypeHint, 139 @Nullable CharSequence metadata 140 ) { 141 int previewType = previewData.getPreviewType(); 142 if (previewType == CONTENT_PREVIEW_TEXT) { 143 return createTextPreview( 144 mScope, 145 chooserRequest.getTargetIntent().getClipData(), 146 chooserRequest.getSharedText(), 147 chooserRequest.getSharedTextTitle(), 148 actionFactory, 149 imageLoader, 150 headlineGenerator, 151 contentTypeHint, 152 metadata 153 ); 154 } 155 if (previewType == CONTENT_PREVIEW_FILE) { 156 FileContentPreviewUi fileContentPreviewUi = new FileContentPreviewUi( 157 previewData.getUriCount(), 158 actionFactory, 159 headlineGenerator, 160 metadata 161 ); 162 if (previewData.getUriCount() > 0) { 163 previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName); 164 } 165 return fileContentPreviewUi; 166 } 167 168 if (previewType == CONTENT_PREVIEW_PAYLOAD_SELECTION) { 169 transitionElementStatusCallback.onAllTransitionElementsReady(); // TODO 170 return new ShareouselContentPreviewUi(); 171 } 172 173 boolean isSingleImageShare = previewData.getUriCount() == 1 174 && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType()); 175 if (!TextUtils.isEmpty(chooserRequest.getSharedText())) { 176 FilesPlusTextContentPreviewUi previewUi = 177 new FilesPlusTextContentPreviewUi( 178 mScope, 179 isSingleImageShare, 180 previewData.getUriCount(), 181 chooserRequest.getSharedText(), 182 chooserRequest.getTargetType(), 183 actionFactory, 184 imageLoader, 185 typeClassifier, 186 headlineGenerator, 187 metadata, 188 chooserRequest.getCallerAllowsTextToggle() 189 ); 190 if (previewData.getUriCount() > 0) { 191 JavaFlowHelper.collectToList( 192 mScope, 193 previewData.getImagePreviewFileInfoFlow(), 194 previewUi::updatePreviewMetadata); 195 } 196 return previewUi; 197 } 198 199 return new UnifiedContentPreviewUi( 200 mScope, 201 isSingleImageShare, 202 chooserRequest.getTargetType(), 203 actionFactory, 204 imageLoader, 205 typeClassifier, 206 transitionElementStatusCallback, 207 previewData.getImagePreviewFileInfoFlow(), 208 previewData.getUriCount(), 209 headlineGenerator, 210 metadata 211 ); 212 } 213 getPreferredContentPreview()214 public int getPreferredContentPreview() { 215 return mContentPreviewUi.getType(); 216 } 217 218 /** 219 * Display a content preview of the specified {@code previewType} to preview the content of the 220 * specified {@code intent}. 221 */ displayContentPreview( Resources resources, LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent)222 public ViewGroup displayContentPreview( 223 Resources resources, 224 LayoutInflater layoutInflater, 225 ViewGroup parent, 226 View headlineViewParent) { 227 228 ViewGroup layout = 229 mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent); 230 mHeadlineParent = headlineViewParent; 231 ContentPreviewUi.displayModifyShareAction(mHeadlineParent, mModifyShareActionFactory.get()); 232 return layout; 233 } 234 235 /** 236 * Update Modify Share Action, if it is inflated. 237 */ updateModifyShareAction()238 public void updateModifyShareAction() { 239 ContentPreviewUi.displayModifyShareAction(mHeadlineParent, mModifyShareActionFactory.get()); 240 } 241 createTextPreview( CoroutineScope scope, ClipData previewData, @Nullable CharSequence sharingText, @Nullable CharSequence previewTitle, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, @Nullable CharSequence metadata )242 private static TextContentPreviewUi createTextPreview( 243 CoroutineScope scope, 244 ClipData previewData, 245 @Nullable CharSequence sharingText, 246 @Nullable CharSequence previewTitle, 247 ChooserContentPreviewUi.ActionFactory actionFactory, 248 ImageLoader imageLoader, 249 HeadlineGenerator headlineGenerator, 250 ContentTypeHint contentTypeHint, 251 @Nullable CharSequence metadata 252 ) { 253 Uri previewThumbnail = null; 254 if (previewData != null) { 255 if (previewData.getItemCount() > 0) { 256 ClipData.Item previewDataItem = previewData.getItemAt(0); 257 previewThumbnail = previewDataItem.getUri(); 258 } 259 } 260 261 return new TextContentPreviewUi( 262 scope, 263 sharingText, 264 previewTitle, 265 metadata, 266 previewThumbnail, 267 actionFactory, 268 imageLoader, 269 headlineGenerator, 270 contentTypeHint); 271 } 272 } 273