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