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