1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.media;
6 
7 import android.app.PendingIntent;
8 import android.content.ComponentName;
9 import android.content.ContentResolver;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.content.RestrictionsManager;
13 import android.content.pm.PackageManager;
14 import android.graphics.Bitmap;
15 import android.graphics.BitmapFactory;
16 import android.graphics.Color;
17 import android.net.Uri;
18 import android.os.Build;
19 import android.provider.Browser;
20 import android.text.TextUtils;
21 
22 import androidx.browser.customtabs.CustomTabsIntent;
23 
24 import org.chromium.base.ApiCompatibilityUtils;
25 import org.chromium.base.ContextUtils;
26 import org.chromium.base.SysUtils;
27 import org.chromium.base.task.PostTask;
28 import org.chromium.base.task.TaskTraits;
29 import org.chromium.chrome.R;
30 import org.chromium.chrome.browser.IntentHandler;
31 import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider.CustomTabsUiType;
32 import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
33 import org.chromium.chrome.browser.document.ChromeLauncherActivity;
34 import org.chromium.chrome.browser.flags.ChromeFeatureList;
35 
36 import java.util.Locale;
37 
38 /**
39  * A class containing some utility static methods.
40  */
41 public class MediaViewerUtils {
42     private static final String DEFAULT_MIME_TYPE = "*/*";
43     private static final String MIMETYPE_AUDIO = "audio";
44     private static final String MIMETYPE_IMAGE = "image";
45     private static final String MIMETYPE_VIDEO = "video";
46 
47     private static boolean sIsMediaLauncherActivityForceEnabledForTest;
48 
49     /**
50      * Creates an Intent that allows viewing the given file in an internal media viewer.
51      * @param displayUri               URI to display to the user, ideally in file:// form.
52      * @param contentUri               content:// URI pointing at the file.
53      * @param mimeType                 MIME type of the file.
54      * @param allowExternalAppHandlers Whether the viewer should allow the user to open with another
55      *                                 app.
56      * @return Intent that can be fired to open the file.
57      */
getMediaViewerIntent( Uri displayUri, Uri contentUri, String mimeType, boolean allowExternalAppHandlers)58     public static Intent getMediaViewerIntent(
59             Uri displayUri, Uri contentUri, String mimeType, boolean allowExternalAppHandlers) {
60         Context context = ContextUtils.getApplicationContext();
61 
62         Bitmap closeIcon = BitmapFactory.decodeResource(
63                 context.getResources(), R.drawable.ic_arrow_back_white_24dp);
64         Bitmap shareIcon = BitmapFactory.decodeResource(
65                 context.getResources(), R.drawable.ic_share_white_24dp);
66 
67         CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
68         builder.setToolbarColor(Color.BLACK);
69         builder.setCloseButtonIcon(closeIcon);
70         builder.setShowTitle(true);
71 
72         if (allowExternalAppHandlers && !willExposeFileUri(contentUri)) {
73             // Create a PendingIntent that can be used to view the file externally.
74             // TODO(https://crbug.com/795968): Check if this is problematic in multi-window mode,
75             //                                 where two different viewers could be visible at the
76             //                                 same time.
77             Intent viewIntent = createViewIntentForUri(contentUri, mimeType, null, null);
78             Intent chooserIntent = Intent.createChooser(viewIntent, null);
79             chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
80             String openWithStr = context.getString(R.string.download_manager_open_with);
81             PendingIntent pendingViewIntent = PendingIntent.getActivity(
82                     context, 0, chooserIntent, PendingIntent.FLAG_CANCEL_CURRENT);
83             builder.addMenuItem(openWithStr, pendingViewIntent);
84         }
85 
86         // Create a PendingIntent that shares the file with external apps.
87         // If the URI is a file URI and the Android version is N or later, this will throw a
88         // FileUriExposedException. In this case, we just don't add the share button.
89         if (!willExposeFileUri(contentUri)) {
90             PendingIntent pendingShareIntent = PendingIntent.getActivity(context, 0,
91                     createShareIntent(contentUri, mimeType), PendingIntent.FLAG_CANCEL_CURRENT);
92             builder.setActionButton(
93                     shareIcon, context.getString(R.string.share), pendingShareIntent, true);
94         }
95 
96         // The color of the media viewer is dependent on the file type.
97         int backgroundRes;
98         if (isImageType(mimeType)) {
99             backgroundRes = R.color.image_viewer_bg;
100         } else {
101             backgroundRes = R.color.media_viewer_bg;
102         }
103         int mediaColor = ApiCompatibilityUtils.getColor(context.getResources(), backgroundRes);
104 
105         // Build up the Intent further.
106         Intent intent = builder.build().intent;
107         intent.setPackage(context.getPackageName());
108         intent.setData(contentUri);
109         intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, CustomTabsUiType.MEDIA_VIEWER);
110         intent.putExtra(CustomTabIntentDataProvider.EXTRA_MEDIA_VIEWER_URL, displayUri.toString());
111         intent.putExtra(CustomTabIntentDataProvider.EXTRA_ENABLE_EMBEDDED_MEDIA_EXPERIENCE, true);
112         intent.putExtra(CustomTabIntentDataProvider.EXTRA_INITIAL_BACKGROUND_COLOR, mediaColor);
113         intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, mediaColor);
114         intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
115         IntentHandler.addTrustedIntentExtras(intent);
116 
117         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
118         intent.setClass(context, ChromeLauncherActivity.class);
119         return intent;
120     }
121 
122     /**
123      * Creates an Intent to open the file in another app by firing an Intent to Android.
124      * @param fileUri  Uri pointing to the file.
125      * @param mimeType MIME type for the file.
126      * @param originalUrl The original url of the downloaded file.
127      * @param referrer Referrer of the downloaded file.
128      * @return Intent that can be used to start an Activity for the file.
129      */
createViewIntentForUri( Uri fileUri, String mimeType, String originalUrl, String referrer)130     public static Intent createViewIntentForUri(
131             Uri fileUri, String mimeType, String originalUrl, String referrer) {
132         Intent fileIntent = new Intent(Intent.ACTION_VIEW);
133         String normalizedMimeType = Intent.normalizeMimeType(mimeType);
134         if (TextUtils.isEmpty(normalizedMimeType)) {
135             fileIntent.setData(fileUri);
136         } else {
137             fileIntent.setDataAndType(fileUri, normalizedMimeType);
138         }
139         fileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
140         fileIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
141         fileIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
142         setOriginalUrlAndReferralExtraToIntent(fileIntent, originalUrl, referrer);
143         return fileIntent;
144     }
145 
146     /**
147      * Adds the originating Uri and referrer extras to an intent if they are not null.
148      * @param intent      Intent for adding extras.
149      * @param originalUrl The original url of the downloaded file.
150      * @param referrer    Referrer of the downloaded file.
151      */
setOriginalUrlAndReferralExtraToIntent( Intent intent, String originalUrl, String referrer)152     public static void setOriginalUrlAndReferralExtraToIntent(
153             Intent intent, String originalUrl, String referrer) {
154         if (originalUrl != null) {
155             intent.putExtra(Intent.EXTRA_ORIGINATING_URI, Uri.parse(originalUrl));
156         }
157         if (referrer != null) intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(originalUrl));
158     }
159 
160     /**
161      * Determines the media type from the given MIME type.
162      * @param mimeType The MIME type to check.
163      * @return MediaLauncherActivity.MediaType enum value for determined media type.
164      */
getMediaTypeFromMIMEType(String mimeType)165     static int getMediaTypeFromMIMEType(String mimeType) {
166         if (TextUtils.isEmpty(mimeType)) return MediaLauncherActivity.MediaType.UNKNOWN;
167 
168         String[] pieces = mimeType.toLowerCase(Locale.getDefault()).split("/");
169         if (pieces.length != 2) return MediaLauncherActivity.MediaType.UNKNOWN;
170 
171         switch (pieces[0]) {
172             case MIMETYPE_AUDIO:
173                 return MediaLauncherActivity.MediaType.AUDIO;
174             case MIMETYPE_IMAGE:
175                 return MediaLauncherActivity.MediaType.IMAGE;
176             case MIMETYPE_VIDEO:
177                 return MediaLauncherActivity.MediaType.VIDEO;
178             default:
179                 return MediaLauncherActivity.MediaType.UNKNOWN;
180         }
181     }
182 
183     /**
184      * Selectively enables or disables the MediaLauncherActivity.
185      */
updateMediaLauncherActivityEnabled()186     public static void updateMediaLauncherActivityEnabled() {
187         PostTask.postTask(TaskTraits.BEST_EFFORT_MAY_BLOCK,
188                 () -> { synchronousUpdateMediaLauncherActivityEnabled(); });
189     }
190 
synchronousUpdateMediaLauncherActivityEnabled()191     static void synchronousUpdateMediaLauncherActivityEnabled() {
192         Context context = ContextUtils.getApplicationContext();
193         PackageManager packageManager = context.getPackageManager();
194         ComponentName mediaComponentName = new ComponentName(context, MediaLauncherActivity.class);
195         ComponentName audioComponentName = new ComponentName(
196                 context, "org.chromium.chrome.browser.media.AudioLauncherActivity");
197 
198         int newMediaState = shouldEnableMediaLauncherActivity()
199                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
200                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
201         int newAudioState = shouldEnableAudioLauncherActivity()
202                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
203                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
204         // This indicates that we don't want to kill Chrome when changing component enabled
205         // state.
206         int flags = PackageManager.DONT_KILL_APP;
207 
208         if (packageManager.getComponentEnabledSetting(mediaComponentName) != newMediaState) {
209             packageManager.setComponentEnabledSetting(mediaComponentName, newMediaState, flags);
210         }
211         if (packageManager.getComponentEnabledSetting(audioComponentName) != newAudioState) {
212             packageManager.setComponentEnabledSetting(audioComponentName, newAudioState, flags);
213         }
214     }
215 
216     /**
217      * Force MediaLauncherActivity to be enabled for testing.
218      */
forceEnableMediaLauncherActivityForTest()219     public static void forceEnableMediaLauncherActivityForTest() {
220         sIsMediaLauncherActivityForceEnabledForTest = true;
221         // Synchronously update to avoid race conditions in tests.
222         synchronousUpdateMediaLauncherActivityEnabled();
223     }
224 
225     /**
226      * Stops forcing MediaLauncherActivity to be enabled for testing.
227      */
stopForcingEnableMediaLauncherActivityForTest()228     public static void stopForcingEnableMediaLauncherActivityForTest() {
229         sIsMediaLauncherActivityForceEnabledForTest = false;
230         // Synchronously update to avoid race conditions in tests.
231         synchronousUpdateMediaLauncherActivityEnabled();
232     }
233 
shouldEnableMediaLauncherActivity()234     private static boolean shouldEnableMediaLauncherActivity() {
235         return sIsMediaLauncherActivityForceEnabledForTest
236                 || ((SysUtils.isAndroidGo() || isEnterpriseManaged())
237                         && ChromeFeatureList.isEnabled(ChromeFeatureList.HANDLE_MEDIA_INTENTS));
238     }
239 
shouldEnableAudioLauncherActivity()240     private static boolean shouldEnableAudioLauncherActivity() {
241         return shouldEnableMediaLauncherActivity() && !SysUtils.isAndroidGo();
242     }
243 
isEnterpriseManaged()244     private static boolean isEnterpriseManaged() {
245 
246         RestrictionsManager restrictionsManager =
247                 (RestrictionsManager) ContextUtils.getApplicationContext().getSystemService(
248                         Context.RESTRICTIONS_SERVICE);
249         return restrictionsManager.hasRestrictionsProvider()
250                 || !restrictionsManager.getApplicationRestrictions().isEmpty();
251     }
252 
createShareIntent(Uri fileUri, String mimeType)253     private static Intent createShareIntent(Uri fileUri, String mimeType) {
254         if (TextUtils.isEmpty(mimeType)) mimeType = DEFAULT_MIME_TYPE;
255 
256         Intent intent = new Intent(Intent.ACTION_SEND);
257         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
258         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
259         intent.putExtra(Intent.EXTRA_STREAM, fileUri);
260         intent.setType(mimeType);
261         return intent;
262     }
263 
isImageType(String mimeType)264     private static boolean isImageType(String mimeType) {
265         if (TextUtils.isEmpty(mimeType)) return false;
266 
267         String[] pieces = mimeType.toLowerCase(Locale.getDefault()).split("/");
268         if (pieces.length != 2) return false;
269 
270         return MIMETYPE_IMAGE.equals(pieces[0]);
271     }
272 
willExposeFileUri(Uri uri)273     private static boolean willExposeFileUri(Uri uri) {
274         // On Android N and later, an Exception is thrown if we try to expose a file:// URI.
275         return uri.getScheme().equals(ContentResolver.SCHEME_FILE)
276                 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
277     }
278 }
279