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 androidx.lifecycle.LifecycleKt.getCoroutineScope; 20 21 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE; 22 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; 23 import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT; 24 25 import android.content.ClipData; 26 import android.content.Intent; 27 import android.content.res.Resources; 28 import android.net.Uri; 29 import android.text.TextUtils; 30 import android.view.LayoutInflater; 31 import android.view.ViewGroup; 32 33 import androidx.annotation.Nullable; 34 import androidx.annotation.VisibleForTesting; 35 import androidx.lifecycle.Lifecycle; 36 37 import com.android.intentresolver.widget.ActionRow; 38 import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; 39 40 import java.util.List; 41 import java.util.function.Consumer; 42 43 /** 44 * Collection of helpers for building the content preview UI displayed in 45 * {@link com.android.intentresolver.ChooserActivity}. 46 * A content preview façade. 47 */ 48 public final class ChooserContentPreviewUi { 49 50 private final Lifecycle mLifecycle; 51 52 /** 53 * Delegate to build the default system action buttons to display in the preview layout, if/when 54 * they're determined to be appropriate for the particular preview we display. 55 * TODO: clarify why action buttons are part of preview logic. 56 */ 57 public interface ActionFactory { 58 /** 59 * @return Runnable to be run when an edit button is clicked (if available). 60 */ 61 @Nullable getEditButtonRunnable()62 Runnable getEditButtonRunnable(); 63 64 /** 65 * @return Runnable to be run when a copy button is clicked (if available). 66 */ 67 @Nullable getCopyButtonRunnable()68 Runnable getCopyButtonRunnable(); 69 70 /** Create custom actions */ createCustomActions()71 List<ActionRow.Action> createCustomActions(); 72 73 /** 74 * Provides a share modification action, if any. 75 */ 76 @Nullable getModifyShareAction()77 ActionRow.Action getModifyShareAction(); 78 79 /** 80 * <p> 81 * Creates an exclude-text action that can be called when the user changes shared text 82 * status in the Media + Text preview. 83 * </p> 84 * <p> 85 * <code>true</code> argument value indicates that the text should be excluded. 86 * </p> 87 */ getExcludeSharedTextAction()88 Consumer<Boolean> getExcludeSharedTextAction(); 89 } 90 91 @VisibleForTesting 92 final ContentPreviewUi mContentPreviewUi; 93 ChooserContentPreviewUi( Lifecycle lifecycle, PreviewDataProvider previewData, Intent targetIntent, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator)94 public ChooserContentPreviewUi( 95 Lifecycle lifecycle, 96 PreviewDataProvider previewData, 97 Intent targetIntent, 98 ImageLoader imageLoader, 99 ActionFactory actionFactory, 100 TransitionElementStatusCallback transitionElementStatusCallback, 101 HeadlineGenerator headlineGenerator) { 102 mLifecycle = lifecycle; 103 mContentPreviewUi = createContentPreview( 104 previewData, 105 targetIntent, 106 DefaultMimeTypeClassifier.INSTANCE, 107 imageLoader, 108 actionFactory, 109 transitionElementStatusCallback, 110 headlineGenerator); 111 if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { 112 transitionElementStatusCallback.onAllTransitionElementsReady(); 113 } 114 } 115 createContentPreview( PreviewDataProvider previewData, Intent targetIntent, MimeTypeClassifier typeClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator)116 private ContentPreviewUi createContentPreview( 117 PreviewDataProvider previewData, 118 Intent targetIntent, 119 MimeTypeClassifier typeClassifier, 120 ImageLoader imageLoader, 121 ActionFactory actionFactory, 122 TransitionElementStatusCallback transitionElementStatusCallback, 123 HeadlineGenerator headlineGenerator) { 124 125 int previewType = previewData.getPreviewType(); 126 if (previewType == CONTENT_PREVIEW_TEXT) { 127 return createTextPreview( 128 mLifecycle, 129 targetIntent, 130 actionFactory, 131 imageLoader, 132 headlineGenerator); 133 } 134 if (previewType == CONTENT_PREVIEW_FILE) { 135 FileContentPreviewUi fileContentPreviewUi = new FileContentPreviewUi( 136 previewData.getUriCount(), 137 actionFactory, 138 headlineGenerator); 139 if (previewData.getUriCount() > 0) { 140 previewData.getFirstFileName( 141 mLifecycle, fileContentPreviewUi::setFirstFileName); 142 } 143 return fileContentPreviewUi; 144 } 145 boolean isSingleImageShare = previewData.getUriCount() == 1 146 && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType()); 147 CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); 148 if (!TextUtils.isEmpty(text)) { 149 FilesPlusTextContentPreviewUi previewUi = 150 new FilesPlusTextContentPreviewUi( 151 mLifecycle, 152 isSingleImageShare, 153 previewData.getUriCount(), 154 targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), 155 targetIntent.getType(), 156 actionFactory, 157 imageLoader, 158 typeClassifier, 159 headlineGenerator); 160 if (previewData.getUriCount() > 0) { 161 JavaFlowHelper.collectToList( 162 getCoroutineScope(mLifecycle), 163 previewData.getImagePreviewFileInfoFlow(), 164 previewUi::updatePreviewMetadata); 165 } 166 return previewUi; 167 } 168 169 return new UnifiedContentPreviewUi( 170 getCoroutineScope(mLifecycle), 171 isSingleImageShare, 172 targetIntent.getType(), 173 actionFactory, 174 imageLoader, 175 typeClassifier, 176 transitionElementStatusCallback, 177 previewData.getImagePreviewFileInfoFlow(), 178 previewData.getUriCount(), 179 headlineGenerator); 180 } 181 getPreferredContentPreview()182 public int getPreferredContentPreview() { 183 return mContentPreviewUi.getType(); 184 } 185 186 /** 187 * Display a content preview of the specified {@code previewType} to preview the content of the 188 * specified {@code intent}. 189 */ displayContentPreview( Resources resources, LayoutInflater layoutInflater, ViewGroup parent)190 public ViewGroup displayContentPreview( 191 Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { 192 193 return mContentPreviewUi.display(resources, layoutInflater, parent); 194 } 195 createTextPreview( Lifecycle lifecycle, Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, HeadlineGenerator headlineGenerator)196 private static TextContentPreviewUi createTextPreview( 197 Lifecycle lifecycle, 198 Intent targetIntent, 199 ChooserContentPreviewUi.ActionFactory actionFactory, 200 ImageLoader imageLoader, 201 HeadlineGenerator headlineGenerator) { 202 CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); 203 String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); 204 ClipData previewData = targetIntent.getClipData(); 205 Uri previewThumbnail = null; 206 if (previewData != null) { 207 if (previewData.getItemCount() > 0) { 208 ClipData.Item previewDataItem = previewData.getItemAt(0); 209 previewThumbnail = previewDataItem.getUri(); 210 } 211 } 212 return new TextContentPreviewUi( 213 lifecycle, 214 sharingText, 215 previewTitle, 216 previewThumbnail, 217 actionFactory, 218 imageLoader, 219 headlineGenerator); 220 } 221 } 222