1 /*
2  * Copyright (C) 2023 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 
22 import android.content.res.Resources;
23 import android.net.Uri;
24 import android.text.util.Linkify;
25 import android.util.PluralsMessageFormatter;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.CheckBox;
30 import android.widget.ImageView;
31 import android.widget.TextView;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.intentresolver.R;
36 import com.android.intentresolver.widget.ActionRow;
37 import com.android.intentresolver.widget.ScrollableImagePreviewView;
38 
39 import kotlinx.coroutines.CoroutineScope;
40 
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.function.Consumer;
44 
45 /**
46  * FilesPlusTextContentPreviewUi is shown when the user is sending 1 or more files along with
47  * non-empty EXTRA_TEXT. The text can be toggled with a checkbox. If a single image file is being
48  * shared, it is shown in a preview (otherwise the headline summary is the sole indication of the
49  * file content).
50  */
51 class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
52     private final CoroutineScope mScope;
53     @Nullable
54     private final String mIntentMimeType;
55     private final CharSequence mText;
56     private final ChooserContentPreviewUi.ActionFactory mActionFactory;
57     private final ImageLoader mImageLoader;
58     private final MimeTypeClassifier mTypeClassifier;
59     private final HeadlineGenerator mHeadlineGenerator;
60     @Nullable
61     private final CharSequence mMetadata;
62     private final boolean mIsSingleImage;
63     private final int mFileCount;
64     private ViewGroup mContentPreviewView;
65     private View mHeadliveView;
66     private boolean mIsMetadataUpdated = false;
67     @Nullable
68     private Uri mFirstFilePreviewUri;
69     private boolean mAllImages;
70     private boolean mAllVideos;
71     // TODO(b/285309527): make this a flag
72     private static final boolean SHOW_TOGGLE_CHECKMARK = false;
73 
FilesPlusTextContentPreviewUi( CoroutineScope scope, boolean isSingleImage, int fileCount, CharSequence text, @Nullable String intentMimeType, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, MimeTypeClassifier typeClassifier, HeadlineGenerator headlineGenerator, @Nullable CharSequence metadata)74     FilesPlusTextContentPreviewUi(
75             CoroutineScope scope,
76             boolean isSingleImage,
77             int fileCount,
78             CharSequence text,
79             @Nullable String intentMimeType,
80             ChooserContentPreviewUi.ActionFactory actionFactory,
81             ImageLoader imageLoader,
82             MimeTypeClassifier typeClassifier,
83             HeadlineGenerator headlineGenerator,
84             @Nullable CharSequence metadata) {
85         if (isSingleImage && fileCount != 1) {
86             throw new IllegalArgumentException(
87                     "fileCount = " + fileCount + " and isSingleImage = true");
88         }
89         mScope = scope;
90         mIntentMimeType = intentMimeType;
91         mFileCount = fileCount;
92         mIsSingleImage = isSingleImage;
93         mText = text;
94         mActionFactory = actionFactory;
95         mImageLoader = imageLoader;
96         mTypeClassifier = typeClassifier;
97         mHeadlineGenerator = headlineGenerator;
98         mMetadata = metadata;
99     }
100 
101     @Override
getType()102     public int getType() {
103         return mIsSingleImage ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
104     }
105 
106     @Override
display( Resources resources, LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent)107     public ViewGroup display(
108             Resources resources,
109             LayoutInflater layoutInflater,
110             ViewGroup parent,
111             View headlineViewParent) {
112         return displayInternal(layoutInflater, parent, headlineViewParent);
113     }
114 
updatePreviewMetadata(List<FileInfo> files)115     public void updatePreviewMetadata(List<FileInfo> files) {
116         boolean allImages = true;
117         boolean allVideos = true;
118         for (FileInfo fileInfo : files) {
119             ScrollableImagePreviewView.PreviewType previewType =
120                     getPreviewType(mTypeClassifier, fileInfo.getMimeType());
121             allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image;
122             allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video;
123         }
124         mAllImages = allImages;
125         mAllVideos = allVideos;
126         mFirstFilePreviewUri = files.isEmpty() ? null : files.get(0).getPreviewUri();
127         mIsMetadataUpdated = true;
128         if (mContentPreviewView != null) {
129             updateUiWithMetadata(mContentPreviewView, mHeadliveView);
130         }
131     }
132 
displayInternal( LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent)133     private ViewGroup displayInternal(
134             LayoutInflater layoutInflater,
135             ViewGroup parent,
136             View headlineViewParent) {
137         mContentPreviewView = (ViewGroup) layoutInflater.inflate(
138                 R.layout.chooser_grid_preview_files_text, parent, false);
139         mHeadliveView = headlineViewParent;
140         inflateHeadline(mHeadliveView);
141 
142         final ActionRow actionRow =
143                 mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row);
144         List<ActionRow.Action> actions = mActionFactory.createCustomActions();
145         actionRow.setActions(actions);
146 
147         if (!mIsSingleImage) {
148             mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
149         }
150         prepareTextPreview(mContentPreviewView, mHeadliveView, mActionFactory);
151         if (mIsMetadataUpdated) {
152             updateUiWithMetadata(mContentPreviewView, mHeadliveView);
153         } else {
154             updateHeadline(
155                     mHeadliveView,
156                     mFileCount,
157                     mTypeClassifier.isImageType(mIntentMimeType),
158                     mTypeClassifier.isVideoType(mIntentMimeType));
159         }
160 
161         return mContentPreviewView;
162     }
163 
updateUiWithMetadata(ViewGroup contentPreviewView, View headlineView)164     private void updateUiWithMetadata(ViewGroup contentPreviewView, View headlineView) {
165         prepareTextPreview(contentPreviewView, headlineView, mActionFactory);
166         updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);
167 
168         ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view);
169         if (mIsSingleImage && mFirstFilePreviewUri != null) {
170             mImageLoader.loadImage(
171                     mScope,
172                     mFirstFilePreviewUri,
173                     bitmap -> {
174                         if (bitmap == null) {
175                             imagePreview.setVisibility(View.GONE);
176                         } else {
177                             imagePreview.setImageBitmap(bitmap);
178                         }
179                     });
180         } else {
181             imagePreview.setVisibility(View.GONE);
182         }
183     }
184 
updateHeadline( View headlineView, int fileCount, boolean allImages, boolean allVideos)185     private void updateHeadline(
186             View headlineView, int fileCount, boolean allImages, boolean allVideos) {
187         CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);
188         String headline;
189         if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) {
190             if (allImages) {
191                 headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, fileCount);
192             } else if (allVideos) {
193                 headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, fileCount);
194             } else {
195                 headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, fileCount);
196             }
197         } else {
198             if (allImages) {
199                 headline = mHeadlineGenerator.getImagesHeadline(fileCount);
200             } else if (allVideos) {
201                 headline = mHeadlineGenerator.getVideosHeadline(fileCount);
202             } else {
203                 headline = mHeadlineGenerator.getFilesHeadline(fileCount);
204             }
205         }
206 
207         displayHeadline(headlineView, headline);
208         displayMetadata(headlineView, mMetadata);
209     }
210 
prepareTextPreview( ViewGroup contentPreview, View headlineView, ChooserContentPreviewUi.ActionFactory actionFactory)211     private void prepareTextPreview(
212             ViewGroup contentPreview,
213             View headlineView,
214             ChooserContentPreviewUi.ActionFactory actionFactory) {
215         final TextView textView = contentPreview.requireViewById(R.id.content_preview_text);
216         CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);
217         boolean isLink = HttpUriMatcher.isHttpUri(mText.toString());
218         textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
219         textView.setText(mText);
220 
221         final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction();
222         includeText.setChecked(true);
223         includeText.setText(isLink ? R.string.include_link : R.string.include_text);
224         shareTextAction.accept(false);
225         includeText.setOnCheckedChangeListener((view, isChecked) -> {
226             if (isChecked) {
227                 textView.setText(mText);
228             } else {
229                 textView.setText(getNoTextString(contentPreview.getResources()));
230             }
231             shareTextAction.accept(!isChecked);
232             updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);
233         });
234         if (SHOW_TOGGLE_CHECKMARK) {
235             includeText.setVisibility(View.VISIBLE);
236         }
237     }
238 
getNoTextString(Resources resources)239     private String getNoTextString(Resources resources) {
240         int stringResource;
241 
242         if (mAllImages) {
243             stringResource = R.string.sharing_images_only;
244         } else if (mAllVideos) {
245             stringResource = R.string.sharing_videos_only;
246         } else {
247             stringResource = R.string.sharing_files_only;
248         }
249 
250         HashMap<String, Object> params = new HashMap<>();
251         params.put("count", mFileCount);
252 
253         return PluralsMessageFormatter.format(
254                 resources,
255                 params,
256                 stringResource
257         );
258     }
259 }
260