diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 1f5576bb46..44d471bb27 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -62,6 +62,7 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; @@ -71,9 +72,7 @@ import java.net.CookieManager; import java.net.CookiePolicy; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends AppCompatActivity @@ -410,28 +409,29 @@ public class PlayerActivity extends AppCompatActivity ? ((Sample.PlaylistSample) intentAsSample).children : new UriSample[] {(UriSample) intentAsSample}; - boolean seenAdsTagUri = false; List mediaSources = new ArrayList<>(); + Uri adTagUri = null; for (UriSample sample : samples) { - seenAdsTagUri |= sample.adTagUri != null; - if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + MediaItem mediaItem = sample.toMediaItem(); + Assertions.checkNotNull(mediaItem.playbackProperties); + if (!Util.checkCleartextTrafficPermitted(mediaItem)) { showToast(R.string.error_cleartext_not_permitted); return Collections.emptyList(); } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri) - || (sample.subtitleInfo != null - && Util.maybeRequestReadExternalStoragePermission( - /* activity= */ this, sample.subtitleInfo.uri))) { + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) { // The player will be reinitialized if the permission is granted. return Collections.emptyList(); } - MediaSource mediaSource = createLeafMediaSource(sample); + MediaSource mediaSource = createLeafMediaSource(mediaItem); if (mediaSource != null) { + adTagUri = sample.adTagUri; mediaSources.add(mediaSource); } } - if (seenAdsTagUri && mediaSources.size() == 1) { - Uri adTagUri = samples[0].adTagUri; + + if (adTagUri == null) { + releaseAdsLoader(); + } else if (mediaSources.size() == 1) { if (!adTagUri.equals(loadedAdTagUri)) { releaseAdsLoader(); loadedAdTagUri = adTagUri; @@ -442,72 +442,42 @@ public class PlayerActivity extends AppCompatActivity } else { showToast(R.string.ima_not_loaded); } - } else if (seenAdsTagUri && mediaSources.size() > 1) { + } else if (mediaSources.size() > 1) { showToast(R.string.unsupported_ads_in_concatenation); releaseAdsLoader(); - } else { - releaseAdsLoader(); } return mediaSources; } @Nullable - private MediaSource createLeafMediaSource(UriSample parameters) { - MediaItem.Builder builder = new MediaItem.Builder().setSourceUri(parameters.uri); - builder.setMimeType(Sample.inferAdaptiveStreamMimeType(parameters.uri, parameters.extension)); + private MediaSource createLeafMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); HttpDataSource.Factory drmDataSourceFactory = null; - if (parameters.drmInfo != null) { + if (mediaItem.playbackProperties.drmConfiguration != null) { if (Util.SDK_INT < 18) { showToast(R.string.error_drm_unsupported_before_api_18); finish(); return null; - } else if (!MediaDrm.isCryptoSchemeSupported(parameters.drmInfo.drmScheme)) { + } else if (!MediaDrm.isCryptoSchemeSupported( + mediaItem.playbackProperties.drmConfiguration.uuid)) { showToast(R.string.error_drm_unsupported_scheme); finish(); return null; } - builder - .setDrmLicenseUri(parameters.drmInfo.drmLicenseUrl) - .setDrmLicenseRequestHeaders( - createLicenseHeaders(parameters.drmInfo.drmKeyRequestProperties)) - .setDrmUuid(parameters.drmInfo.drmScheme) - .setDrmMultiSession(parameters.drmInfo.drmMultiSession) - .setDrmSessionForClearTypes(Util.toList(parameters.drmInfo.drmSessionForClearTypes)); drmDataSourceFactory = ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); } - if (parameters.subtitleInfo != null) { - builder.setSubtitles( - Collections.singletonList( - new MediaItem.Subtitle( - parameters.subtitleInfo.uri, - parameters.subtitleInfo.mimeType, - parameters.subtitleInfo.language, - C.SELECTION_FLAG_DEFAULT))); - } DownloadRequest downloadRequest = ((DemoApplication) getApplication()) .getDownloadTracker() - .getDownloadRequest(parameters.uri); + .getDownloadRequest(mediaItem.playbackProperties.sourceUri); if (downloadRequest != null) { return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); } return mediaSourceFactory .setDrmHttpDataSourceFactory(drmDataSourceFactory) - .createMediaSource(builder.build()); - } - - @Nullable - private Map createLicenseHeaders(@Nullable String[] drmKeyRequestProperties) { - if (drmKeyRequestProperties == null || drmKeyRequestProperties.length == 0) { - return null; - } - Map headers = new HashMap<>(); - for (int i = 0; i < drmKeyRequestProperties.length; i += 2) { - headers.put(drmKeyRequestProperties[i], drmKeyRequestProperties[i + 1]); - } - return headers; + .createMediaSource(mediaItem); } private void releasePlayer() { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java index 0d43bec890..1225c8b6c4 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -34,11 +34,15 @@ import android.content.Intent; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.UUID; /* package */ abstract class Sample { @@ -140,6 +144,35 @@ import java.util.UUID; subtitleInfo.addToIntent(intent, extrasKeySuffix); } } + + public MediaItem toMediaItem() { + MediaItem.Builder builder = new MediaItem.Builder().setSourceUri(uri); + builder.setMimeType(inferAdaptiveStreamMimeType(uri, extension)); + if (drmInfo != null) { + Map headers = new HashMap<>(); + if (drmInfo.drmKeyRequestProperties != null) { + for (int i = 0; i < drmInfo.drmKeyRequestProperties.length; i += 2) { + headers.put(drmInfo.drmKeyRequestProperties[i], drmInfo.drmKeyRequestProperties[i + 1]); + } + } + builder + .setDrmLicenseUri(drmInfo.drmLicenseUrl) + .setDrmLicenseRequestHeaders(headers) + .setDrmUuid(drmInfo.drmScheme) + .setDrmMultiSession(drmInfo.drmMultiSession) + .setDrmSessionForClearTypes(Util.toList(drmInfo.drmSessionForClearTypes)); + } + if (subtitleInfo != null) { + builder.setSubtitles( + Collections.singletonList( + new MediaItem.Subtitle( + subtitleInfo.uri, + subtitleInfo.mimeType, + subtitleInfo.language, + C.SELECTION_FLAG_DEFAULT))); + } + return builder.build(); + } } public static final class PlaylistSample extends Sample { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java rename to library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java index 62414636c8..a78a2d06ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -314,7 +314,7 @@ public final class MediaItem { /** * Sets the optional tag for custom attributes. The tag for the media source which will be - * published in the {@link com.google.android.exoplayer2.Timeline} of the source as {@link + * published in the {@code com.google.android.exoplayer2.Timeline} of the source as {@code * com.google.android.exoplayer2.Timeline.Window#tag}. * *

If a {@link PlaybackProperties#sourceUri} is set, the tag is used to create a {@link @@ -466,7 +466,7 @@ public final class MediaItem { /** * Optional tag for custom attributes. The tag for the media source which will be published in - * the {@link com.google.android.exoplayer2.Timeline} of the source as {@link + * the {@code com.google.android.exoplayer2.Timeline} of the source as {@code * com.google.android.exoplayer2.Timeline.Window#tag}. */ @Nullable public final Object tag; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaMetadata.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/MediaMetadata.java rename to library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java b/library/common/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java rename to library/common/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/offline/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/offline/package-info.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 4141b534cc..60fe1a39d4 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -50,6 +50,7 @@ import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; import java.io.ByteArrayOutputStream; @@ -185,36 +186,67 @@ public final class Util { } for (Uri uri : uris) { if (isLocalFileUri(uri)) { - if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); - return true; - } - break; + return requestExternalStoragePermission(activity); } } return false; } /** - * Returns whether it may be possible to load the given URIs based on the network security - * policy's cleartext traffic permissions. + * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE} + * permission for the specified {@link MediaItem media items}, requesting the permission if + * necessary. * - * @param uris A list of URIs that will be loaded. - * @return Whether it may be possible to load the given URIs. + * @param activity The host activity for checking and requesting the permission. + * @param mediaItems {@link MediaItem Media items}s that may require {@link + * permission#READ_EXTERNAL_STORAGE} to read. + * @return Whether a permission request was made. */ - public static boolean checkCleartextTrafficPermitted(Uri... uris) { + public static boolean maybeRequestReadExternalStoragePermission( + Activity activity, MediaItem... mediaItems) { + if (Util.SDK_INT < 23) { + return false; + } + for (MediaItem mediaItem : mediaItems) { + if (mediaItem.playbackProperties == null) { + continue; + } + if (isLocalFileUri(mediaItem.playbackProperties.sourceUri)) { + return requestExternalStoragePermission(activity); + } + for (int i = 0; i < mediaItem.playbackProperties.subtitles.size(); i++) { + if (isLocalFileUri(mediaItem.playbackProperties.subtitles.get(i).uri)) { + return requestExternalStoragePermission(activity); + } + } + } + return false; + } + + /** + * Returns whether it may be possible to load the URIs of the given media items based on the + * network security policy's cleartext traffic permissions. + * + * @param mediaItems A list of {@link MediaItem media items}. + * @return Whether it may be possible to load the URIs of the given media items. + */ + public static boolean checkCleartextTrafficPermitted(MediaItem... mediaItems) { if (Util.SDK_INT < 24) { // We assume cleartext traffic is permitted. return true; } - for (Uri uri : uris) { - if ("http".equals(uri.getScheme()) - && !NetworkSecurityPolicy.getInstance() - .isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost()))) { - // The security policy prevents cleartext traffic. + for (MediaItem mediaItem : mediaItems) { + if (mediaItem.playbackProperties == null) { + continue; + } + if (isTrafficRestricted(mediaItem.playbackProperties.sourceUri)) { return false; } + for (int i = 0; i < mediaItem.playbackProperties.subtitles.size(); i++) { + if (isTrafficRestricted(mediaItem.playbackProperties.subtitles.get(i).uri)) { + return false; + } + } } return true; } @@ -2185,6 +2217,24 @@ public final class Util { return replacedLanguages; } + @RequiresApi(api = Build.VERSION_CODES.M) + private static boolean requestExternalStoragePermission(Activity activity) { + if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions( + new String[] {permission.READ_EXTERNAL_STORAGE}, /* requestCode= */ 0); + return true; + } + return false; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private static boolean isTrafficRestricted(Uri uri) { + return "http".equals(uri.getScheme()) + && !NetworkSecurityPolicy.getInstance() + .isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost())); + } + private static String maybeReplaceGrandfatheredLanguageTags(String languageTag) { for (int i = 0; i < isoGrandfatheredTagReplacements.length; i += 2) { if (languageTag.startsWith(isoGrandfatheredTagReplacements[i])) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java similarity index 100% rename from library/core/src/test/java/com/google/android/exoplayer2/MediaItemTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java similarity index 100% rename from library/core/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java b/library/common/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java similarity index 100% rename from library/core/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java