• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_TEXT;
22 
23 import android.content.ClipData;
24 import android.content.ClipDescription;
25 import android.content.ContentInterface;
26 import android.content.Intent;
27 import android.content.res.Resources;
28 import android.net.Uri;
29 import android.os.RemoteException;
30 import android.view.LayoutInflater;
31 import android.view.ViewGroup;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.intentresolver.ImageLoader;
36 import com.android.intentresolver.flags.FeatureFlagRepository;
37 import com.android.intentresolver.widget.ActionRow;
38 import com.android.intentresolver.widget.ImagePreviewView;
39 import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.function.Consumer;
44 import java.util.stream.Collectors;
45 
46 /**
47  * Collection of helpers for building the content preview UI displayed in
48  * {@link com.android.intentresolver.ChooserActivity}.
49  *
50  * A content preview façade.
51  */
52 public final class ChooserContentPreviewUi {
53     /**
54      * Delegate to build the default system action buttons to display in the preview layout, if/when
55      * they're determined to be appropriate for the particular preview we display.
56      * TODO: clarify why action buttons are part of preview logic.
57      */
58     public interface ActionFactory {
59         /** Create an action that copies the share content to the clipboard. */
createCopyButton()60         ActionRow.Action createCopyButton();
61 
62         /** Create an action that opens the share content in a system-default editor. */
63         @Nullable
createEditButton()64         ActionRow.Action createEditButton();
65 
66         /** Create an "Share to Nearby" action. */
67         @Nullable
createNearbyButton()68         ActionRow.Action createNearbyButton();
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         Runnable 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     /**
92      * Testing shim to specify whether a given mime type is considered to be an "image."
93      *
94      * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
95      * then migrate {@link com.android.intentresolver.ChooserActivity#isImageType(String)} into this
96      * class.
97      */
98     public interface ImageMimeTypeClassifier {
99         /** @return whether the specified {@code mimeType} is classified as an "image" type. */
isImageType(String mimeType)100         boolean isImageType(String mimeType);
101     }
102 
103     private final ContentPreviewUi mContentPreviewUi;
104 
ChooserContentPreviewUi( Intent targetIntent, ContentInterface contentResolver, ImageMimeTypeClassifier imageClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, FeatureFlagRepository featureFlagRepository)105     public ChooserContentPreviewUi(
106             Intent targetIntent,
107             ContentInterface contentResolver,
108             ImageMimeTypeClassifier imageClassifier,
109             ImageLoader imageLoader,
110             ActionFactory actionFactory,
111             TransitionElementStatusCallback transitionElementStatusCallback,
112             FeatureFlagRepository featureFlagRepository) {
113 
114         mContentPreviewUi = createContentPreview(
115                 targetIntent,
116                 contentResolver,
117                 imageClassifier,
118                 imageLoader,
119                 actionFactory,
120                 transitionElementStatusCallback,
121                 featureFlagRepository);
122         if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) {
123             transitionElementStatusCallback.onAllTransitionElementsReady();
124         }
125     }
126 
createContentPreview( Intent targetIntent, ContentInterface contentResolver, ImageMimeTypeClassifier imageClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, FeatureFlagRepository featureFlagRepository)127     private ContentPreviewUi createContentPreview(
128             Intent targetIntent,
129             ContentInterface contentResolver,
130             ImageMimeTypeClassifier imageClassifier,
131             ImageLoader imageLoader,
132             ActionFactory actionFactory,
133             TransitionElementStatusCallback transitionElementStatusCallback,
134             FeatureFlagRepository featureFlagRepository) {
135         int type = findPreferredContentPreview(targetIntent, contentResolver, imageClassifier);
136         switch (type) {
137             case CONTENT_PREVIEW_TEXT:
138                 return createTextPreview(
139                         targetIntent, actionFactory, imageLoader, featureFlagRepository);
140 
141             case CONTENT_PREVIEW_FILE:
142                 return new FileContentPreviewUi(
143                         extractContentUris(targetIntent),
144                         actionFactory,
145                         imageLoader,
146                         contentResolver,
147                         featureFlagRepository);
148 
149             case CONTENT_PREVIEW_IMAGE:
150                 return createImagePreview(
151                         targetIntent,
152                         actionFactory,
153                         contentResolver,
154                         imageClassifier,
155                         imageLoader,
156                         transitionElementStatusCallback,
157                         featureFlagRepository);
158         }
159 
160         return new NoContextPreviewUi(type);
161     }
162 
getPreferredContentPreview()163     public int getPreferredContentPreview() {
164         return mContentPreviewUi.getType();
165     }
166 
167     /**
168      * Display a content preview of the specified {@code previewType} to preview the content of the
169      * specified {@code intent}.
170      */
displayContentPreview( Resources resources, LayoutInflater layoutInflater, ViewGroup parent)171     public ViewGroup displayContentPreview(
172             Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
173 
174         return mContentPreviewUi.display(resources, layoutInflater, parent);
175     }
176 
177     /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
178     @ContentPreviewType
findPreferredContentPreview( Intent targetIntent, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier)179     private static int findPreferredContentPreview(
180             Intent targetIntent,
181             ContentInterface resolver,
182             ImageMimeTypeClassifier imageClassifier) {
183         /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
184          * that broadly covers all data being shared, such as {@literal *}/* when sending an image
185          * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
186          * FILE, TEXT.  */
187         final String action = targetIntent.getAction();
188         final String type = targetIntent.getType();
189         final boolean isSend = Intent.ACTION_SEND.equals(action);
190         final boolean isSendMultiple = Intent.ACTION_SEND_MULTIPLE.equals(action);
191 
192         if (!(isSend || isSendMultiple)
193                 || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) {
194             return CONTENT_PREVIEW_TEXT;
195         }
196 
197         if (isSend) {
198             Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
199             return findPreferredContentPreview(uri, resolver, imageClassifier);
200         }
201 
202         List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
203         if (uris == null || uris.isEmpty()) {
204             return CONTENT_PREVIEW_TEXT;
205         }
206 
207         for (Uri uri : uris) {
208             // Defaulting to file preview when there are mixed image/file types is
209             // preferable, as it shows the user the correct number of items being shared
210             int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
211             if (uriPreviewType == CONTENT_PREVIEW_FILE) {
212                 return CONTENT_PREVIEW_FILE;
213             }
214         }
215 
216         return CONTENT_PREVIEW_IMAGE;
217     }
218 
219     @ContentPreviewType
findPreferredContentPreview( Uri uri, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier)220     private static int findPreferredContentPreview(
221             Uri uri, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier) {
222         if (uri == null) {
223             return CONTENT_PREVIEW_TEXT;
224         }
225 
226         String mimeType = null;
227         try {
228             mimeType = resolver.getType(uri);
229         } catch (RemoteException ignored) {
230         }
231         return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
232     }
233 
createTextPreview( Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, FeatureFlagRepository featureFlagRepository)234     private static TextContentPreviewUi createTextPreview(
235             Intent targetIntent,
236             ChooserContentPreviewUi.ActionFactory actionFactory,
237             ImageLoader imageLoader,
238             FeatureFlagRepository featureFlagRepository) {
239         CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
240         String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
241         ClipData previewData = targetIntent.getClipData();
242         Uri previewThumbnail = null;
243         if (previewData != null) {
244             if (previewData.getItemCount() > 0) {
245                 ClipData.Item previewDataItem = previewData.getItemAt(0);
246                 previewThumbnail = previewDataItem.getUri();
247             }
248         }
249         return new TextContentPreviewUi(
250                 sharingText,
251                 previewTitle,
252                 previewThumbnail,
253                 actionFactory,
254                 imageLoader,
255                 featureFlagRepository);
256     }
257 
createImagePreview( Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ContentInterface contentResolver, ChooserContentPreviewUi.ImageMimeTypeClassifier imageClassifier, ImageLoader imageLoader, ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback, FeatureFlagRepository featureFlagRepository)258     static ImageContentPreviewUi createImagePreview(
259             Intent targetIntent,
260             ChooserContentPreviewUi.ActionFactory actionFactory,
261             ContentInterface contentResolver,
262             ChooserContentPreviewUi.ImageMimeTypeClassifier imageClassifier,
263             ImageLoader imageLoader,
264             ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
265             FeatureFlagRepository featureFlagRepository) {
266         CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
267         String action = targetIntent.getAction();
268         // TODO: why don't we use image classifier for single-element ACTION_SEND?
269         final List<Uri> imageUris = Intent.ACTION_SEND.equals(action)
270                 ? extractContentUris(targetIntent)
271                 : extractContentUris(targetIntent)
272                         .stream()
273                         .filter(uri -> {
274                             String type = null;
275                             try {
276                                 type = contentResolver.getType(uri);
277                             } catch (RemoteException ignored) {
278                             }
279                             return imageClassifier.isImageType(type);
280                         })
281                         .collect(Collectors.toList());
282         return new ImageContentPreviewUi(
283                 imageUris,
284                 text,
285                 actionFactory,
286                 imageLoader,
287                 transitionElementStatusCallback,
288                 featureFlagRepository);
289     }
290 
extractContentUris(Intent targetIntent)291     private static List<Uri> extractContentUris(Intent targetIntent) {
292         List<Uri> uris = new ArrayList<>();
293         if (Intent.ACTION_SEND.equals(targetIntent.getAction())) {
294             Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
295             if (ContentPreviewUi.validForContentPreview(uri)) {
296                 uris.add(uri);
297             }
298         } else {
299             List<Uri> receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
300             if (receivedUris != null) {
301                 for (Uri uri : receivedUris) {
302                     if (ContentPreviewUi.validForContentPreview(uri)) {
303                         uris.add(uri);
304                     }
305                 }
306             }
307         }
308         return uris;
309     }
310 }
311