commit a08c09829dba91741889200a10ab8ad4d7094acb Author: Elmar Meyer Date: Sat Aug 8 22:34:33 2026 +0200 Initial Commit! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git "a/app/\001" "b/app/\001" new file mode 100644 index 0000000..e69de29 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..6459f5b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,87 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.example.exoplayertv' + compileSdk 36 + + defaultConfig { + applicationId "com.example.exoplayertv" + minSdk 21 + targetSdk 36 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + viewBinding true + } +} + + + +project.ext { + checkerframeworkVersion = '3.13.0' + androidxAnnotationVersion = '1.6.0' + androidxAppCompatVersion = '1.6.1' + androidxMaterialVersion = '1.8.0' + androidxMultidexVersion = '2.0.1' + androidxMedia3Version = '1.8.0' + comPicassoVersion = '2.8' + okhttpVersion = '5.0.0' + jotaTimeVersion = '2.7' +} + + +dependencies { + + implementation libs.appcompat + implementation libs.material + implementation libs.constraintlayout + implementation libs.navigation.fragment + implementation libs.navigation.ui + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core + + + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'com.google.android.material:material:' + androidxMaterialVersion + + + + implementation 'androidx.multidex:multidex:' + androidxMultidexVersion + + implementation "androidx.media3:media3-exoplayer:" + androidxMedia3Version + implementation "androidx.media3:media3-common:" + androidxMedia3Version + implementation "androidx.media3:media3-ui:" + androidxMedia3Version + + implementation "androidx.media3:media3-exoplayer-ima:" + androidxMedia3Version + implementation "androidx.media3:media3-exoplayer-dash:" + androidxMedia3Version +// implementation "androidx.media3:media3-demo:" + androidxMedia3Version + implementation "androidx.media3:media3-datasource:" + androidxMedia3Version + implementation "androidx.media3:media3-datasource-cronet:" + androidxMedia3Version + implementation "androidx.media3:media3-ui-compose:" + androidxMedia3Version + + implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' + + implementation 'com.squareup.picasso:picasso:' + comPicassoVersion + implementation 'com.squareup.okhttp3:okhttp:' + okhttpVersion + implementation 'com.squareup.okhttp3:okhttp-urlconnection:' + okhttpVersion + implementation 'joda-time:joda-time:' + jotaTimeVersion +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..dea33c7 Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..1f009b5 Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..fd284df --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.example.exoplayertv", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 21 +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/exoplayertv/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/exoplayertv/ExampleInstrumentedTest.java new file mode 100644 index 0000000..5d36f9e --- /dev/null +++ b/app/src/androidTest/java/com/example/exoplayertv/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.exoplayertv; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.exoplayertv", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ce799fe --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/exoplayertv/DemoDownloadService.java b/app/src/main/java/com/example/exoplayertv/DemoDownloadService.java new file mode 100644 index 0000000..f0e870f --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/DemoDownloadService.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +//import static androidx.media3.exoplayer.demo.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID; + +import android.app.Notification; +import android.content.Context; +import androidx.annotation.Nullable; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.exoplayer.scheduler.PlatformScheduler; +import androidx.media3.exoplayer.scheduler.Requirements; +import androidx.media3.exoplayer.scheduler.Scheduler; +import androidx.media3.exoplayer.offline.DownloadNotificationHelper; +import androidx.media3.common.util.NotificationUtil; +import androidx.media3.common.util.Util; +import java.util.List; + +/** A service for downloading media. */ +@androidx.media3.common.util.UnstableApi +public class DemoDownloadService extends DownloadService { + + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; + + + public DemoDownloadService() { + super( + FOREGROUND_NOTIFICATION_ID, + DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, + DOWNLOAD_NOTIFICATION_CHANNEL_ID, + androidx.media3.exoplayer.R.string.exo_download_notification_channel_name, + /* channelDescriptionResourceId= */ 0); + } + + @Override + protected DownloadManager getDownloadManager() { + // This will only happen once, because getDownloadManager is guaranteed to be called only once + // in the life cycle of the process. + DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this); + DownloadNotificationHelper downloadNotificationHelper = + DemoUtil.getDownloadNotificationHelper(/* context= */ this); + downloadManager.addListener( + new TerminalStateNotificationHelper( + this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); + return downloadManager; + } + + @Override + protected Scheduler getScheduler() { + return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; + } + + @Override + protected Notification getForegroundNotification( + List downloads, @Requirements.RequirementFlags int notMetRequirements) { + return DemoUtil.getDownloadNotificationHelper(/* context= */ this) + .buildProgressNotification( + /* context= */ this, + R.drawable.ic_download, + /* contentIntent= */ null, + /* message= */ null, + downloads, + notMetRequirements); + } + + /** + * Creates and displays notifications for downloads when they complete or fail. + * + *

This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}. + * It is static to avoid leaking the first {@link DemoDownloadService} instance. + */ + private static final class TerminalStateNotificationHelper implements DownloadManager.Listener { + + private final Context context; + private final DownloadNotificationHelper notificationHelper; + + private int nextNotificationId; + + public TerminalStateNotificationHelper( + Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) { + this.context = context.getApplicationContext(); + this.notificationHelper = notificationHelper; + nextNotificationId = firstNotificationId; + } + + @Override + public void onDownloadChanged( + DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { + notification = + notificationHelper.buildDownloadCompletedNotification( + context, + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = + notificationHelper.buildDownloadFailedNotification( + context, + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else { + return; + } + NotificationUtil.setNotification(context, nextNotificationId++, notification); + } + } +} diff --git a/app/src/main/java/com/example/exoplayertv/DemoUtil.java b/app/src/main/java/com/example/exoplayertv/DemoUtil.java new file mode 100644 index 0000000..9e509c0 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/DemoUtil.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +import android.content.Context; + +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultRenderersFactory; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.database.DatabaseProvider; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.cronet.CronetDataSource; +import androidx.media3.datasource.cronet.CronetUtil; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadNotificationHelper; +import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.datasource.DefaultHttpDataSource; +import androidx.media3.datasource.cache.Cache; +import androidx.media3.datasource.cache.CacheDataSource; +import androidx.media3.datasource.cache.NoOpCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; +import java.io.File; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.concurrent.Executors; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.chromium.net.CronetEngine; + +/** Utility methods for the demo app. */ +public final class DemoUtil { + + public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; + + /** + * Whether the demo application uses Cronet for networking. Note that Cronet does not provide + * automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975). + * + *

If set to false, the platform's default network stack is used with a {@link CookieManager} + * configured in {@link #getHttpDataSourceFactory}. + */ + private static final boolean USE_CRONET_FOR_NETWORKING = true; + + private static final String TAG = "DemoUtil"; + private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + + private static DataSource.@MonotonicNonNull Factory dataSourceFactory; + private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory; + @UnstableApi + private static @MonotonicNonNull DatabaseProvider databaseProvider; + private static @MonotonicNonNull File downloadDirectory; + @UnstableApi + private static @MonotonicNonNull Cache downloadCache; + @UnstableApi + private static @MonotonicNonNull DownloadManager downloadManager; + private static @MonotonicNonNull DownloadTracker downloadTracker; + @UnstableApi + private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper; + + /** Returns whether extension renderers should be used. */ + public static boolean useExtensionRenderers() { + return false; // Elmar: BuildConfig.USE_DECODER_EXTENSIONS; + } + + @UnstableApi + public static RenderersFactory buildRenderersFactory( + Context context, boolean preferExtensionRenderer) { + @DefaultRenderersFactory.ExtensionRendererMode + int extensionRendererMode = + useExtensionRenderers() + ? (preferExtensionRenderer + ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + return new DefaultRenderersFactory(context.getApplicationContext()) + .setExtensionRendererMode(extensionRendererMode); + } + + public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) { + if (httpDataSourceFactory == null) { + if (USE_CRONET_FOR_NETWORKING) { + context = context.getApplicationContext(); + @Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context); + if (cronetEngine != null) { + httpDataSourceFactory = + new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor()); + } + } + if (httpDataSourceFactory == null) { + // We don't want to use Cronet, or we failed to instantiate a CronetEngine. + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + CookieHandler.setDefault(cookieManager); + httpDataSourceFactory = new DefaultHttpDataSource.Factory(); + } + } + return httpDataSourceFactory; + } + + /** Returns a {@link DataSource.Factory}. */ + @UnstableApi + public static synchronized DataSource.Factory getDataSourceFactory(Context context) { + if (dataSourceFactory == null) { + context = context.getApplicationContext(); + DefaultDataSource.Factory upstreamFactory = + new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context)); + dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context)); + } + return dataSourceFactory; + } + + @UnstableApi + public static synchronized DownloadNotificationHelper getDownloadNotificationHelper( + Context context) { + if (downloadNotificationHelper == null) { + downloadNotificationHelper = + new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); + } + return downloadNotificationHelper; + } + + @UnstableApi + public static synchronized DownloadManager getDownloadManager(Context context) { + ensureDownloadManagerInitialized(context); + return downloadManager; + } + + @UnstableApi + public static synchronized DownloadTracker getDownloadTracker(Context context) { + ensureDownloadManagerInitialized(context); + return downloadTracker; + } + + @UnstableApi + private static synchronized Cache getDownloadCache(Context context) { + if (downloadCache == null) { + File downloadContentDirectory = + new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = + new SimpleCache( + downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context)); + } + return downloadCache; + } + + @UnstableApi + private static synchronized void ensureDownloadManagerInitialized(Context context) { + if (downloadManager == null) { + downloadManager = + new DownloadManager( + context, + getDatabaseProvider(context), + getDownloadCache(context), + getHttpDataSourceFactory(context), + Executors.newFixedThreadPool(/* nThreads= */ 6)); + downloadTracker = + new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager); + } + } + + @UnstableApi + private static synchronized DatabaseProvider getDatabaseProvider(Context context) { + if (databaseProvider == null) { + databaseProvider = new StandaloneDatabaseProvider(context); + } + return databaseProvider; + } + + private static synchronized File getDownloadDirectory(Context context) { + if (downloadDirectory == null) { + downloadDirectory = context.getExternalFilesDir(/* type= */ null); + if (downloadDirectory == null) { + downloadDirectory = context.getFilesDir(); + } + } + return downloadDirectory; + } + + @UnstableApi + private static CacheDataSource.Factory buildReadOnlyCacheDataSource( + DataSource.Factory upstreamFactory, Cache cache) { + return new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory) + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR); + } + + private DemoUtil() {} +} diff --git a/app/src/main/java/com/example/exoplayertv/DownloadTracker.java b/app/src/main/java/com/example/exoplayertv/DownloadTracker.java new file mode 100644 index 0000000..f7e66bb --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/DownloadTracker.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.fragment.app.FragmentManager; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.common.Tracks; +import androidx.media3.common.DrmInitData; +import androidx.media3.exoplayer.drm.DrmSession; +import androidx.media3.exoplayer.drm.DrmSessionEventListener; +import androidx.media3.exoplayer.drm.OfflineLicenseHelper; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadCursor; +import androidx.media3.exoplayer.offline.DownloadHelper; +import androidx.media3.exoplayer.offline.DownloadHelper.LiveContentUnsupportedException; +import androidx.media3.exoplayer.offline.DownloadIndex; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadRequest; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.common.TrackGroup; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.datasource.DataSource; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +/** Tracks media that has been downloaded. */ +public class DownloadTracker { + + /** Listens for changes in the tracked downloads. */ + public interface Listener { + + /** Called when the tracked downloads changed. */ + void onDownloadsChanged(); + } + + private static final String TAG = "DownloadTracker"; + + private final Context context; + private final DataSource.Factory dataSourceFactory; + private final CopyOnWriteArraySet listeners; + @UnstableApi + private final HashMap downloads; + @UnstableApi + private final DownloadIndex downloadIndex; + + @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; + + @UnstableApi + public DownloadTracker( + Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { + this.context = context.getApplicationContext(); + this.dataSourceFactory = dataSourceFactory; + listeners = new CopyOnWriteArraySet<>(); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); + } + + public void addListener(Listener listener) { + listeners.add(checkNotNull(listener)); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + @UnstableApi + public boolean isDownloaded(MediaItem mediaItem) { + @Nullable Download download = downloads.get(checkNotNull(mediaItem.localConfiguration).uri); + return download != null && download.state != Download.STATE_FAILED; + } + + @UnstableApi + @Nullable + public DownloadRequest getDownloadRequest(Uri uri) { + @Nullable Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; + } + + @UnstableApi + public void toggleDownload( + FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) { + @Nullable Download download = downloads.get(checkNotNull(mediaItem.localConfiguration).uri); + if (download != null && download.state != Download.STATE_FAILED) { + DownloadService.sendRemoveDownload( + context, DemoDownloadService.class, download.request.id, /* foreground= */ false); + } else { + if (startDownloadDialogHelper != null) { + startDownloadDialogHelper.release(); + } + startDownloadDialogHelper = + new StartDownloadDialogHelper( + fragmentManager, + DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, dataSourceFactory), + mediaItem); + } + } + + @UnstableApi + private void loadDownloads() { + try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { + while (loadedDownloads.moveToNext()) { + Download download = loadedDownloads.getDownload(); + downloads.put(download.request.uri, download); + } + } catch (IOException e) { + Log.w(TAG, "Failed to query downloads", e); + } + } + + @UnstableApi + private class DownloadManagerListener implements DownloadManager.Listener { + + @Override + public void onDownloadChanged( + DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + downloads.put(download.request.uri, download); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + downloads.remove(download.request.uri); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + } + + @UnstableApi + private final class StartDownloadDialogHelper + implements DownloadHelper.Callback, + TrackSelectionDialog.TrackSelectionListener, + DialogInterface.OnDismissListener { + + private final FragmentManager fragmentManager; + private final DownloadHelper downloadHelper; + private final MediaItem mediaItem; + + private TrackSelectionDialog trackSelectionDialog; + private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; + @Nullable private byte[] keySetId; + + public StartDownloadDialogHelper( + FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) { + this.fragmentManager = fragmentManager; + this.downloadHelper = downloadHelper; + this.mediaItem = mediaItem; + downloadHelper.prepare(this); + } + + public void release() { + downloadHelper.release(); + if (trackSelectionDialog != null) { + trackSelectionDialog.dismiss(); + } + if (widevineOfflineLicenseFetchTask != null) { + widevineOfflineLicenseFetchTask.cancel(false); + } + } + + // DownloadHelper.Callback implementation. + + @Override + public void onPrepared(DownloadHelper helper, boolean tracksInfoAvailable) { + @Nullable Format format = getFirstFormatWithDrmInitData(helper); + if (format == null) { + onDownloadPrepared(helper); + return; + } + + // The content is DRM protected. We need to acquire an offline license. + if (Util.SDK_INT < 18) { + Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18"); + return; + } + // TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest. + if (!hasSchemaData(format.drmInitData)) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e( + TAG, + "Downloading content where DRM scheme data is not located in the manifest is not" + + " supported"); + return; + } + widevineOfflineLicenseFetchTask = + new WidevineOfflineLicenseFetchTask( + format, + mediaItem.localConfiguration.drmConfiguration, + dataSourceFactory, + /* dialogHelper= */ this, + helper); + widevineOfflineLicenseFetchTask.execute(); + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + boolean isLiveContent = e instanceof LiveContentUnsupportedException; + int toastStringId = + isLiveContent ? R.string.download_live_unsupported : R.string.download_start_error; + String logMessage = + isLiveContent ? "Downloading live content unsupported" : "Failed to start download"; + Toast.makeText(context, toastStringId, Toast.LENGTH_LONG).show(); + Log.e(TAG, logMessage, e); + } + + // TrackSelectionListener implementation. + + @Override + public void onTracksSelected(TrackSelectionParameters trackSelectionParameters) { + for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { + downloadHelper.clearTrackSelections(periodIndex); + downloadHelper.addTrackSelection(periodIndex, trackSelectionParameters); + } + DownloadRequest downloadRequest = buildDownloadRequest(); + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; + } + startDownload(downloadRequest); + } + + // DialogInterface.OnDismissListener implementation. + + @Override + public void onDismiss(DialogInterface dialogInterface) { + trackSelectionDialog = null; + downloadHelper.release(); + } + + // Internal methods. + + /** + * Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the + * content's tracks, or null if none is found. + */ + @Nullable + private Format getFirstFormatWithDrmInitData(DownloadHelper helper) { + for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) { + MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex); + for (int rendererIndex = 0; + rendererIndex < mappedTrackInfo.getRendererCount(); + rendererIndex++) { + TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) { + TrackGroup trackGroup = trackGroups.get(trackGroupIndex); + for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { + Format format = trackGroup.getFormat(formatIndex); + if (format.drmInitData != null) { + return format; + } + } + } + } + } + return null; + } + + private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) { + this.keySetId = keySetId; + onDownloadPrepared(helper); + } + + private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Failed to fetch offline DRM license", e); + } + + private void onDownloadPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + + Tracks tracks = downloadHelper.getTracks(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(tracks)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + trackSelectionDialog = + TrackSelectionDialog.createForTracksAndParameters( + /* titleId= */ androidx.media3.exoplayer.R.string.exo_download_description, + tracks, + DownloadHelper.getDefaultTrackSelectorParameters(context), + /* allowAdaptiveSelections= */ false, + /* allowMultipleOverrides= */ true, + /* onTracksSelectedListener= */ this, + /* onDismissListener= */ this); + trackSelectionDialog.show(fragmentManager, /* tag= */ null); + } + + /** + * Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has + * non-null {@link DrmInitData.SchemeData#data}. + */ + private boolean hasSchemaData(DrmInitData drmInitData) { + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + if (drmInitData.get(i).hasData()) { + return true; + } + } + return false; + } + + private void startDownload() { + startDownload(buildDownloadRequest()); + } + + private void startDownload(DownloadRequest downloadRequest) { + DownloadService.sendAddDownload( + context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); + } + + private DownloadRequest buildDownloadRequest() { + return downloadHelper + .getDownloadRequest( + Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title.toString()))) + .copyWithKeySetId(keySetId); + } + } + + /** Downloads a Widevine offline license in a background thread. */ + @RequiresApi(18) + private static final class WidevineOfflineLicenseFetchTask extends AsyncTask { + + private final Format format; + private final MediaItem.DrmConfiguration drmConfiguration; + private final DataSource.Factory dataSourceFactory; + private final StartDownloadDialogHelper dialogHelper; + private final DownloadHelper downloadHelper; + + @Nullable private byte[] keySetId; + @Nullable private DrmSession.DrmSessionException drmSessionException; + + public WidevineOfflineLicenseFetchTask( + Format format, + MediaItem.DrmConfiguration drmConfiguration, + DataSource.Factory dataSourceFactory, + StartDownloadDialogHelper dialogHelper, + DownloadHelper downloadHelper) { + this.format = format; + this.drmConfiguration = drmConfiguration; + this.dataSourceFactory = dataSourceFactory; + this.dialogHelper = dialogHelper; + this.downloadHelper = downloadHelper; + } + + @Override + protected Void doInBackground(Void... voids) { + OfflineLicenseHelper offlineLicenseHelper = + OfflineLicenseHelper.newWidevineInstance( + drmConfiguration.licenseUri.toString(), + drmConfiguration.forceDefaultLicenseUri, + dataSourceFactory, + drmConfiguration.licenseRequestHeaders, + new DrmSessionEventListener.EventDispatcher()); + try { + keySetId = offlineLicenseHelper.downloadLicense(format); + } catch (DrmSession.DrmSessionException e) { + drmSessionException = e; + } finally { + offlineLicenseHelper.release(); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + if (drmSessionException != null) { + dialogHelper.onOfflineLicenseFetchedError(drmSessionException); + } else { + dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId)); + } + } + } +} diff --git a/app/src/main/java/com/example/exoplayertv/Gesten.java b/app/src/main/java/com/example/exoplayertv/Gesten.java new file mode 100644 index 0000000..37caab0 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/Gesten.java @@ -0,0 +1,122 @@ +package com.example.exoplayertv; +// https://droid-lernen.de/wischgesten-erkennen/ + +import android.content.Context; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.util.DisplayMetrics; + +public class Gesten implements View.OnTouchListener { + + private GestureDetector gestureDetector = null; + private Context cxt = null; + private float xDown=-1,yDown=-1; + private boolean isDown = false; + + private float ScreenWidth; + private float ScreenHeight; + + public Gesten(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + ScreenWidth = dm.widthPixels; + ScreenHeight = dm.heightPixels; + + gestureDetector = new GestureDetector(context, new GestureListener()); + cxt = context; + } + + public boolean onTouch(View v, MotionEvent event) { + if(event.getAction()==MotionEvent.ACTION_DOWN) { + isDown = true; + xDown = event.getRawX()/ScreenWidth; + yDown = event.getRawY()/ScreenHeight; + onDown(xDown,yDown); + }else if(event.getAction()==MotionEvent.ACTION_UP) { + isDown = false; + xDown = -1; + yDown = -1; + onUp(event.getRawX()/ScreenWidth,event.getRawY()/ScreenHeight); + }else if(event.getAction()==MotionEvent.ACTION_MOVE && isDown) { + onMove(xDown-event.getRawX()/ScreenWidth, yDown-event.getRawY()/ScreenHeight,ScreenWidth,ScreenHeight); + } + boolean res = gestureDetector.onTouchEvent(event); + return res; + } + + private final class GestureListener extends GestureDetector.SimpleOnGestureListener { + + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return onClick(); + //return super.onSingleTapUp(e); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + boolean result = false; + try { + float diffY = e2.getY() - e1.getY(); + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > Math.abs(diffY)) { + if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + result = onSwipeRight(diffX); + } else { + result = onSwipeLeft(diffX); + } + } + } else { + if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY > 0) { + result = onSwipeBottom(diffY); + } else { + result = onSwipeTop(diffY); + } + } + } + } catch (Exception exception) { + exception.printStackTrace(); + } + return result; + } + } + + public boolean onSwipeRight(float diffX) { + return false; + } + + public boolean onSwipeLeft(float diffX) { + return false; + } + + public void onMove(float diffX, float diffY, float ScreenWidth, float ScreenHeight){ + Log.i("Elmar", "xDiff="+String.valueOf(diffX)+", yDiff="+String.valueOf(diffY)); + } + + public void onDown(float diffX, float diffY){ } + + public void onUp(float diffX, float diffY){ } + + public boolean onClick(){ + return false; + } + + public boolean onSwipeTop(float diffY) { + return false; + } + + public boolean onSwipeBottom(float diffY) { + return false; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/exoplayertv/IntentUtil.java b/app/src/main/java/com/example/exoplayertv/IntentUtil.java new file mode 100644 index 0000000..5588e9d --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/IntentUtil.java @@ -0,0 +1,266 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaItem.ClippingConfiguration; +import androidx.media3.common.MediaItem.SubtitleConfiguration; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.Util; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** Util to read from and populate an intent. */ +public class IntentUtil { + + // Actions. + + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; + public static final String ACTION_VIEW_LIST = + "com.google.android.exoplayer.demo.action.VIEW_LIST"; + + // Activity extras. + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + + // Media item configuration extras. + + public static final String URI_EXTRA = "uri"; + public static final String TITLE_EXTRA = "title"; + public static final String MIME_TYPE_EXTRA = "mime_type"; + public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms"; + public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms"; + + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URI_EXTRA = "drm_license_uri"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_SESSION_FOR_CLEAR_CONTENT = "drm_session_for_clear_content"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA = "drm_force_default_license_uri"; + + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; + + /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */ + public static List createMediaItemsFromIntent(Intent intent) { + List mediaItems = new ArrayList<>(); + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index)); + mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + index)); + index++; + } + } else { + Uri uri = intent.getData(); + mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "")); + } + return mediaItems; + } + + /** Populates the intent with the given list of {@link MediaItem media items}. */ + public static void addToIntent(List mediaItems, Intent intent) { + checkArgument(!mediaItems.isEmpty()); + if (mediaItems.size() == 1) { + MediaItem mediaItem = mediaItems.get(0); + MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration); + intent.setAction(ACTION_VIEW).setData(mediaItem.localConfiguration.uri); + if (mediaItem.mediaMetadata.title != null) { + intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title); + } + addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ ""); + addClippingConfigurationToIntent( + mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ ""); + } else { + intent.setAction(ACTION_VIEW_LIST); + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + MediaItem.LocalConfiguration localConfiguration = + checkNotNull(mediaItem.localConfiguration); + intent.putExtra(URI_EXTRA + ("_" + i), localConfiguration.uri.toString()); + addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "_" + i); + addClippingConfigurationToIntent( + mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "_" + i); + if (mediaItem.mediaMetadata.title != null) { + intent.putExtra(TITLE_EXTRA + ("_" + i), mediaItem.mediaMetadata.title); + } + } + } + } + + private static MediaItem createMediaItemFromIntent( + Uri uri, Intent intent, String extrasKeySuffix) { + @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); + @Nullable String title = intent.getStringExtra(TITLE_EXTRA + extrasKeySuffix); + @Nullable String adTagUri = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + @Nullable + SubtitleConfiguration subtitleConfiguration = + createSubtitleConfiguration(intent, extrasKeySuffix); + MediaItem.Builder builder = + new MediaItem.Builder() + .setUri(uri) + .setMimeType(mimeType) + .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) + .setClippingConfiguration( + new ClippingConfiguration.Builder() + .setStartPositionMs( + intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0)) + .setEndPositionMs( + intent.getLongExtra( + CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)) + .build()); + if (adTagUri != null) { + builder.setAdsConfiguration( + new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build()); + } + if (subtitleConfiguration != null) { + builder.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); + } + + return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build(); + } + + @Nullable + private static MediaItem.SubtitleConfiguration createSubtitleConfiguration( + Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return null; + } + return new MediaItem.SubtitleConfiguration.Builder( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix))) + .setMimeType( + checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix))) + .setLanguage(intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)) + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build(); + } + + private static MediaItem.Builder populateDrmPropertiesFromIntent( + MediaItem.Builder builder, Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + @Nullable String drmSchemeExtra = intent.getStringExtra(schemeKey); + if (drmSchemeExtra == null) { + return builder; + } + Map headers = new HashMap<>(); + @Nullable + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length; i += 2) { + headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); + } + } + @Nullable UUID drmUuid = Util.getDrmUuid(drmSchemeExtra); + if (drmUuid != null) { + builder.setDrmConfiguration( + new MediaItem.DrmConfiguration.Builder(drmUuid) + .setLicenseUri(intent.getStringExtra(DRM_LICENSE_URI_EXTRA + extrasKeySuffix)) + .setMultiSession( + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false)) + .setForceDefaultLicenseUri( + intent.getBooleanExtra( + DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false)) + .setLicenseRequestHeaders(headers) + .setForceSessionsForAudioAndVideoTracks( + intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false)) + .build()); + } + return builder; + } + + private static void addPlaybackPropertiesToIntent( + MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) { + intent + .putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, localConfiguration.mimeType) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, + localConfiguration.adsConfiguration != null + ? localConfiguration.adsConfiguration.adTagUri.toString() + : null); + if (localConfiguration.drmConfiguration != null) { + addDrmConfigurationToIntent(localConfiguration.drmConfiguration, intent, extrasKeySuffix); + } + if (!localConfiguration.subtitleConfigurations.isEmpty()) { + checkState(localConfiguration.subtitleConfigurations.size() == 1); + MediaItem.SubtitleConfiguration subtitleConfiguration = + localConfiguration.subtitleConfigurations.get(0); + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, subtitleConfiguration.uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitleConfiguration.mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitleConfiguration.language); + } + } + + private static void addDrmConfigurationToIntent( + MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) { + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.scheme.toString()); + intent.putExtra( + DRM_LICENSE_URI_EXTRA + extrasKeySuffix, + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : null); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession); + intent.putExtra( + DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, + drmConfiguration.forceDefaultLicenseUri); + + String[] drmKeyRequestProperties = + new String[drmConfiguration.licenseRequestHeaders.size() * 2]; + int index = 0; + for (Map.Entry entry : drmConfiguration.licenseRequestHeaders.entrySet()) { + drmKeyRequestProperties[index++] = entry.getKey(); + drmKeyRequestProperties[index++] = entry.getValue(); + } + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + + List<@C.TrackType Integer> forcedDrmSessionTrackTypes = + drmConfiguration.forcedSessionTrackTypes; + if (!forcedDrmSessionTrackTypes.isEmpty()) { + // Only video and audio together are supported. + checkState( + forcedDrmSessionTrackTypes.size() == 2 + && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO) + && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO)); + intent.putExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, true); + } + } + + private static void addClippingConfigurationToIntent( + MediaItem.ClippingConfiguration clippingConfiguration, + Intent intent, + String extrasKeySuffix) { + if (clippingConfiguration.startPositionMs != 0) { + intent.putExtra( + CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, clippingConfiguration.startPositionMs); + } + if (clippingConfiguration.endPositionMs != C.TIME_END_OF_SOURCE) { + intent.putExtra( + CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, clippingConfiguration.endPositionMs); + } + } +} diff --git a/app/src/main/java/com/example/exoplayertv/PlayerActivity.java b/app/src/main/java/com/example/exoplayertv/PlayerActivity.java new file mode 100644 index 0000000..ebead60 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/PlayerActivity.java @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +import static java.lang.Math.abs; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.media3.common.C; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.Player; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.common.Tracks; +import androidx.media3.common.AudioAttributes; +import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider; +import androidx.media3.exoplayer.drm.FrameworkMediaDrm; +import androidx.media3.exoplayer.ima.ImaAdsLoader; +import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource; +import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.DecoderInitializationException; +import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException; +import androidx.media3.exoplayer.offline.DownloadRequest; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.source.ads.AdsLoader; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.ui.PlayerView; +import androidx.media3.datasource.DataSource; +//import androidx.media3.exoplayer.util.DebugTextViewHelper; +import androidx.media3.common.ErrorMessageProvider; +import androidx.media3.exoplayer.util.EventLogger; +import androidx.media3.common.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** An activity that plays media using {@link ExoPlayer}. */ +public class PlayerActivity extends AppCompatActivity + implements OnClickListener, PlayerView.ControllerVisibilityListener { + + private AudioManager audioManager; + private int max_volume; + private int current_volume; + private WindowManager.LayoutParams lp; + private float current_brightness; + public static final int NONE = 0; + public static final int LOUDNESS = 1; + public static final int BRIGHTNESS = 2; + + private int state = NONE; + + // Saved instance state keys. + + private static final String KEY_TRACK_SELECTION_PARAMETERS = "track_selection_parameters"; + private static final String KEY_SERVER_SIDE_ADS_LOADER_STATE = "server_side_ads_loader_state"; + private static final String KEY_ITEM_INDEX = "item_index"; + private static final String KEY_POSITION = "position"; + private static final String KEY_AUTO_PLAY = "auto_play"; + + protected PlayerView playerView; + protected LinearLayout debugRootView; + protected @Nullable ExoPlayer player; + + private boolean isShowingTrackSelectionDialog; + private Button selectTracksButton; + + private TextView epgTitle; + + private Handler h; + Runnable run; + private DataSource.Factory dataSourceFactory; + private List mediaItems; + private TrackSelectionParameters trackSelectionParameters; + // private DebugTextViewHelper debugViewHelper; + private Tracks lastSeenTracks; + private boolean startAutoPlay; + private int startItemIndex; + private long startPosition; + + // For ad playback only. + + @Nullable private AdsLoader clientSideAdsLoader; + + // TODO: Annotate this and serverSideAdsLoaderState below with @OptIn when it can be applied to + // fields (needs http://r.android.com/2004032 to be released into a version of + // androidx.annotation:annotation-experimental). + @Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader; + + private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State + serverSideAdsLoaderState; + + // Activity lifecycle. + + @UnstableApi + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this); + + setContentView(); + debugRootView = findViewById(R.id.controls_root); +// debugTextView = findViewById(R.id.debug_text_view); + selectTracksButton = findViewById(R.id.select_tracks_button); + selectTracksButton.setOnClickListener(this); + epgTitle = findViewById(R.id.debug_text_view); + char[] title = getIntent().getCharArrayExtra("Title"); + epgTitle.setText(title,0, title.length); + h = new Handler(); + run = new Runnable() { + @Override + public void run() { + debugRootView.setVisibility(View.GONE); + } + }; + + playerView = findViewById(R.id.player_view); + playerView.setControllerVisibilityListener(this); + playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); + playerView.requestFocus(); + + if (savedInstanceState != null) { + trackSelectionParameters = + TrackSelectionParameters.fromBundle( + savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS)); + startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); + startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX); + startPosition = savedInstanceState.getLong(KEY_POSITION); + restoreServerSideAdsLoaderState(savedInstanceState); + } else { + trackSelectionParameters = new TrackSelectionParameters.Builder(/* context= */ this).build(); + clearStartPosition(); + } + + + + audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); + max_volume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + lp = getWindow().getAttributes(); + + playerView.setOnTouchListener(new Gesten(this) { + public boolean onSwipeTop(float diffY) { + //es wurde nach oben gewischt, hier den Code einfügen + return false; + } + public boolean onSwipeBottom(float diffY) { + //es wurde nach unten gewischt, hier den Code einfügen + return false; + } + public boolean onSwipeRight(float diffX) { + //es wurde nach rechts gewischt, hier den Code einfügen + Intent ret = PlayerActivity.this.getIntent(); + ret.putExtra(SampleChooserActivity.CHANNEL_OFFSET,-1); + PlayerActivity.this.setResult(RESULT_OK,ret); + PlayerActivity.this.finish(); + return true; + } + public boolean onSwipeLeft(float diffX) { + //es wurde nach links gewischt, hier den Code einfügen + Intent ret = PlayerActivity.this.getIntent(); + ret.putExtra(SampleChooserActivity.CHANNEL_OFFSET,1); + PlayerActivity.this.setResult(RESULT_OK,ret); + PlayerActivity.this.finish(); + return true; + } + public boolean onClick() { + char[] title = getIntent().getCharArrayExtra("Title"); + epgTitle.setText(title,0, title.length); + debugRootView.setVisibility(debugRootView.getVisibility()==View.VISIBLE?View.GONE:View.VISIBLE); + h.removeCallbacks(run); + h.postDelayed(run,5000); + /*playerView.showController();*/ + return true; + } + public void onDown(float diffX, float diffY) { + if(diffX>0.5) { + state = LOUDNESS; + current_volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + } else { + state = BRIGHTNESS; + current_brightness = lp.screenBrightness; + } + } + public void onMove(float diffX, float diffY, float ScreenWidth, float ScreenHeight) { + if (abs(diffY)<0.02 || abs(diffX)>abs(diffY)) + return; + if(state==LOUDNESS) { + audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); + audioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, // Stream type + current_volume+(int)(max_volume*diffY), // Index + AudioManager.FLAG_SHOW_UI // Flags + ); + } else { + lp.screenBrightness = current_brightness+255*diffY; + getWindow().setAttributes(lp); + getWindow().addFlags(WindowManager.LayoutParams.FLAGS_CHANGED); + } + } + public void onUp(float diffX, float diffY) { } + }); + h.postDelayed(run,5000); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + releasePlayer(); + releaseClientSideAdsLoader(); + clearStartPosition(); + setIntent(intent); + } + + @Override + public void onStart() { + super.onStart(); + if (Build.VERSION.SDK_INT > 23) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (Build.VERSION.SDK_INT <= 23 || player == null) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onPause() { + super.onPause(); + if (Build.VERSION.SDK_INT <= 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (Build.VERSION.SDK_INT > 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + releaseClientSideAdsLoader(); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initializePlayer(); + } else { + showToast(R.string.storage_permission_denied); + finish(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + updateTrackSelectorParameters(); + updateStartPosition(); + outState.putBundle(KEY_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle()); + outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); + outState.putInt(KEY_ITEM_INDEX, startItemIndex); + outState.putLong(KEY_POSITION, startPosition); + saveServerSideAdsLoaderState(outState); + } + + // Activity input + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // See whether the player view wants to handle media or DPAD keys events. + if (event.getKeyCode()==KeyEvent.KEYCODE_DPAD_UP) { + Intent ret = PlayerActivity.this.getIntent(); + ret.putExtra(SampleChooserActivity.CHANNEL_OFFSET,-1); + PlayerActivity.this.setResult(RESULT_OK,ret); + PlayerActivity.this.finish(); + return true; + } + if (event.getKeyCode()==KeyEvent.KEYCODE_DPAD_DOWN) { + Intent ret = PlayerActivity.this.getIntent(); + ret.putExtra(SampleChooserActivity.CHANNEL_OFFSET,1); + PlayerActivity.this.setResult(RESULT_OK,ret); + PlayerActivity.this.finish(); + return true; + } + if (event.getKeyCode()==KeyEvent.KEYCODE_ENTER) { + debugRootView.setVisibility(View.VISIBLE); + } + if (debugRootView.getVisibility()==View.VISIBLE && + (event.getKeyCode()==KeyEvent.KEYCODE_DPAD_LEFT || event.getKeyCode()==KeyEvent.KEYCODE_DPAD_UP)) { + selectTracksButton.setFocusableInTouchMode(true); + return true; + } + return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); + } + + // OnClickListener methods + + @Override + public void onClick(View view) { + if (view == selectTracksButton + && !isShowingTrackSelectionDialog + && TrackSelectionDialog.willHaveContent(player)) { + isShowingTrackSelectionDialog = true; + TrackSelectionDialog trackSelectionDialog = + TrackSelectionDialog.createForPlayer( + player, + /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); + trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); + } + } + + // StyledPlayerView.ControllerVisibilityListener implementation + + @Override + public void onVisibilityChanged(int visibility) { + debugRootView.setVisibility(visibility); + } + + // Internal methods + + protected void setContentView() { + setContentView(R.layout.player_activity); + } + + /** + * @return Whether initialization was successful. + */ + protected boolean initializePlayer() { + if (player == null) { + Intent intent = getIntent(); + + mediaItems = createMediaItems(intent); + if (mediaItems.isEmpty()) { + return false; + } + + lastSeenTracks = Tracks.EMPTY; + ExoPlayer.Builder playerBuilder = + new ExoPlayer.Builder(/* context= */ this) + .setMediaSourceFactory(createMediaSourceFactory()); + setRenderersFactory( + playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false)); + player = playerBuilder.build(); + player.setTrackSelectionParameters(trackSelectionParameters); + player.addListener(new PlayerEventListener()); + player.addAnalyticsListener(new EventLogger()); + player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); + player.setPlayWhenReady(startAutoPlay); + playerView.setPlayer(player); + configurePlayerWithServerSideAdsLoader(); +// debugViewHelper = new DebugTextViewHelper(player, debugTextView); +// debugViewHelper.start(); + } + boolean haveStartPosition = startItemIndex != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startItemIndex, startPosition); + } + player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); + player.prepare(); + updateButtonVisibility(); + return true; + } + + private MediaSource.Factory createMediaSourceFactory() { + DefaultDrmSessionManagerProvider drmSessionManagerProvider = + new DefaultDrmSessionManagerProvider(); + drmSessionManagerProvider.setDrmHttpDataSourceFactory( + DemoUtil.getHttpDataSourceFactory(/* context= */ this)); + ImaServerSideAdInsertionMediaSource.AdsLoader.Builder serverSideAdLoaderBuilder = + new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(/* context= */ this, playerView); + if (serverSideAdsLoaderState != null) { + serverSideAdLoaderBuilder.setAdsLoaderState(serverSideAdsLoaderState); + } + serverSideAdsLoader = serverSideAdLoaderBuilder.build(); + ImaServerSideAdInsertionMediaSource.Factory imaServerSideAdInsertionMediaSourceFactory = + new ImaServerSideAdInsertionMediaSource.Factory( + serverSideAdsLoader, + new DefaultMediaSourceFactory(/* context= */ this) + .setDataSourceFactory(dataSourceFactory)); + return new DefaultMediaSourceFactory(/* context= */ this) + .setDataSourceFactory(dataSourceFactory) + .setDrmSessionManagerProvider(drmSessionManagerProvider) + .setLocalAdInsertionComponents( + this::getClientSideAdsLoader, /* adViewProvider= */ playerView) + .setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory); + } + + private void setRenderersFactory( + ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) { + RenderersFactory renderersFactory = + DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); + playerBuilder.setRenderersFactory(renderersFactory); + } + + private void configurePlayerWithServerSideAdsLoader() { + serverSideAdsLoader.setPlayer(player); + } + + private List createMediaItems(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return Collections.emptyList(); + } + + List mediaItems = + createMediaItems(intent, DemoUtil.getDownloadTracker(/* context= */ this)); + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + + if (!Util.checkCleartextTrafficPermitted(mediaItem)) { + showToast(R.string.error_cleartext_not_permitted); + finish(); + return Collections.emptyList(); + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) { + // The player will be reinitialized if the permission is granted. + return Collections.emptyList(); + } + + MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; + if (drmConfiguration != null) { + if (Build.VERSION.SDK_INT < 18) { + showToast(R.string.error_drm_unsupported_before_api_18); + finish(); + return Collections.emptyList(); + } else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) { + showToast(R.string.error_drm_unsupported_scheme); + finish(); + return Collections.emptyList(); + } + } + } + return mediaItems; + } + + private AdsLoader getClientSideAdsLoader(MediaItem.AdsConfiguration adsConfiguration) { + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + if (clientSideAdsLoader == null) { + clientSideAdsLoader = new ImaAdsLoader.Builder(/* context= */ this).build(); + } + clientSideAdsLoader.setPlayer(player); + return clientSideAdsLoader; + } + + protected void releasePlayer() { + if (player != null) { + updateTrackSelectorParameters(); + updateStartPosition(); + releaseServerSideAdsLoader(); +// debugViewHelper.stop(); +// debugViewHelper = null; + player.release(); + player = null; + playerView.setPlayer(/* player= */ null); + mediaItems = Collections.emptyList(); + } + if (clientSideAdsLoader != null) { + clientSideAdsLoader.setPlayer(null); + } else { + playerView.getAdViewGroup().removeAllViews(); + } + } + + private void releaseServerSideAdsLoader() { + serverSideAdsLoaderState = serverSideAdsLoader.release(); + serverSideAdsLoader = null; + } + + private void releaseClientSideAdsLoader() { + if (clientSideAdsLoader != null) { + clientSideAdsLoader.release(); + clientSideAdsLoader = null; + playerView.getAdViewGroup().removeAllViews(); + } + } + + private void saveServerSideAdsLoaderState(Bundle outState) { + if (serverSideAdsLoaderState != null) { + outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle()); + } + } + + private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) { + Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE); + if (adsLoaderStateBundle != null) { + serverSideAdsLoaderState = + ImaServerSideAdInsertionMediaSource.AdsLoader.State.fromBundle( + adsLoaderStateBundle); + } + } + + private void updateTrackSelectorParameters() { + if (player != null) { + trackSelectionParameters = player.getTrackSelectionParameters(); + } + } + + private void updateStartPosition() { + if (player != null) { + startAutoPlay = player.getPlayWhenReady(); + startItemIndex = player.getCurrentMediaItemIndex(); + startPosition = Math.max(0, player.getContentPosition()); + } + } + + protected void clearStartPosition() { + startAutoPlay = true; + startItemIndex = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; + } + + // User controls + + private void updateButtonVisibility() { + selectTracksButton.setEnabled(player != null && TrackSelectionDialog.willHaveContent(player)); + epgTitle.setEnabled(player != null && TrackSelectionDialog.willHaveContent(player)); + } + + private void showControls() { + debugRootView.setVisibility(View.VISIBLE); + } + + private void showToast(int messageId) { + showToast(getString(messageId)); + } + + private void showToast(String message) { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } + + private class PlayerEventListener implements Player.Listener { + + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + showControls(); + } + updateButtonVisibility(); + } + + @Override + public void onPlayerError(PlaybackException error) { + if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { + player.seekToDefaultPosition(); + player.prepare(); + } else { + updateButtonVisibility(); + showControls(); + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged(Tracks tracks) { + updateButtonVisibility(); + if (tracks == lastSeenTracks) { + return; + } + if (tracks.containsType(C.TRACK_TYPE_VIDEO) + && !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { + showToast(R.string.error_unsupported_video); + } + if (tracks.containsType(C.TRACK_TYPE_AUDIO) + && !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { + showToast(R.string.error_unsupported_audio); + } + lastSeenTracks = tracks; + } + } + + private class PlayerErrorMessageProvider implements ErrorMessageProvider { + + @Override + public Pair getErrorMessage(PlaybackException e) { + String errorString = getString(R.string.error_generic); + Throwable cause = e.getCause(); + if (cause instanceof DecoderInitializationException) { + // Special case for decoder initialization failures. + DecoderInitializationException decoderInitializationException = + (DecoderInitializationException) cause; + if (decoderInitializationException.codecInfo == null) { + if (decoderInitializationException.getCause() instanceof DecoderQueryException) { + errorString = getString(R.string.error_querying_decoders); + } else if (decoderInitializationException.secureDecoderRequired) { + errorString = + getString( + R.string.error_no_secure_decoder, decoderInitializationException.mimeType); + } else { + errorString = + getString(R.string.error_no_decoder, decoderInitializationException.mimeType); + } + } else { + errorString = + getString( + R.string.error_instantiating_decoder, + decoderInitializationException.codecInfo.name); + } + } + return Pair.create(0, errorString); + } + } + + private static List createMediaItems(Intent intent, DownloadTracker downloadTracker) { + List mediaItems = new ArrayList<>(); + for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { + mediaItems.add( + maybeSetDownloadProperties( + item, downloadTracker.getDownloadRequest(item.localConfiguration.uri))); + } + return mediaItems; + } + + private static MediaItem maybeSetDownloadProperties( + MediaItem item, @Nullable DownloadRequest downloadRequest) { + if (downloadRequest == null) { + return item; + } + MediaItem.Builder builder = item.buildUpon(); + builder + .setMediaId(downloadRequest.id) + .setUri(downloadRequest.uri) + .setCustomCacheKey(downloadRequest.customCacheKey) + .setMimeType(downloadRequest.mimeType) + .setStreamKeys(downloadRequest.streamKeys); + @Nullable + MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; + if (drmConfiguration != null) { + builder.setDrmConfiguration( + drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); + } + return builder.build(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/exoplayertv/SampleChooserActivity.java b/app/src/main/java/com/example/exoplayertv/SampleChooserActivity.java new file mode 100644 index 0000000..fefa58b --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/SampleChooserActivity.java @@ -0,0 +1,948 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import android.Manifest; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.JsonReader; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.OnChildClickListener; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.DoNotInline; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaItem.ClippingConfiguration; +import androidx.media3.common.MediaMetadata; +import androidx.media3.exoplayer.RenderersFactory; +import com.example.exoplayertv.tvepg.epg.EPG; +import com.example.exoplayertv.tvepg.epg.EPGClickListener; +import com.example.exoplayertv.tvepg.epg.EPGData; +import com.example.exoplayertv.tvepg.epg.domain.EPGChannel; +import com.example.exoplayertv.tvepg.epg.domain.EPGEvent; +import com.example.exoplayertv.tvepg.xmltv.ContentTask; +import com.example.exoplayertv.tvepg.xmltv.model.Channel; +import com.example.exoplayertv.tvepg.xmltv.model.Program; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends AppCompatActivity + implements DownloadTracker.Listener, OnChildClickListener, + ContentTask.TvGuide { + private EPG epg; + private static EPGData epgData; + private int curChannelPosition=-1; + private int curProgramPosition=-1; + private List playlists; + + private static final String TAG = "SampleChooserActivity"; + private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; + private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position"; + private static final int POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 100; + + static final int PLAYER_REQUEST = 12345; // The request code + public static final String CHANNEL_OFFSET = "SampleChooserActivity.CHANNEL_OFFSET"; + static PlaylistGroup group; + static int childPosition = 0; + static boolean isExternalURL; + + private String[] uris; + private boolean useExtensionRenderers; + private DownloadTracker downloadTracker; + private MenuItem preferExtensionDecodersMenuItem; + @Nullable private MediaItem downloadMediaItemWaitingForNotificationPermission; + private boolean notificationPermissionToastShown; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.sample_chooser_activity); + + // ActionBar verstecken + if (getSupportActionBar() != null) { + getSupportActionBar().hide(); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + epg = (EPG) findViewById(R.id.epg); + epg.setEPGClickListener(new EPGClickListener() { + @Override + public void onChannelClicked(int channelPosition, EPGChannel epgChannel) { + SampleChooserActivity.this.onChannelClicked(channelPosition, epgChannel); + } + + @Override + public void onEventClicked(int channelPosition, int programPosition, EPGEvent epgEvent) { + SampleChooserActivity.this.onEventClicked(channelPosition, programPosition, epgEvent); + } + + @Override + public void onResetButtonClicked() { + epg.recalculateAndRedraw(true); + } + }); + + // Do initial load of data. + if (epgData==null) { + new SampleChooserActivity.Channels().execute(); + } else { + epg.setEPGData(epgData); + epg.recalculateAndRedraw(true); + } + + uris = new String[]{"https://www.ask-meyer.ch/IPTV/Channels.json"}; + Arrays.sort(uris); + + useExtensionRenderers = DemoUtil.useExtensionRenderers(); + downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this); + loadSample(); + startDownloadService(); + } + + /** Start the download service if it should be running but it's not currently. */ + private void startDownloadService() { + // Starting the service in the foreground causes notification flicker if there is no scheduled + // action. Starting it in the background throws an exception if the app is in the background too + // (e.g. if device screen is locked). + try { + DownloadService.start(this, DemoDownloadService.class); + } catch (IllegalStateException e) { + DownloadService.startForeground(this, DemoDownloadService.class); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.sample_chooser_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_update) { + // Do initial load of data. + new Channels().execute(); + curChannelPosition = -1; + curProgramPosition = -1; + return true; + } + + if (id == R.id.action_destroy) { + // Do initial load of data. + if (epg != null) { + epg.clearEPGImageCache(); + } + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onStart() { + super.onStart(); + downloadTracker.addListener(this); + } + + @Override + public void onStop() { + downloadTracker.removeListener(this); + super.onStop(); + } + + @Override + public void onDownloadsChanged() { + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == POST_NOTIFICATION_PERMISSION_REQUEST_CODE) { + handlePostNotificationPermissionGrantResults(grantResults); + } else { + handleExternalStoragePermissionGrantResults(grantResults); + } + } + + private void handlePostNotificationPermissionGrantResults(int[] grantResults) { + if (!notificationPermissionToastShown + && (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED)) { + Toast.makeText( + getApplicationContext(), R.string.post_notification_not_granted, Toast.LENGTH_LONG) + .show(); + notificationPermissionToastShown = true; + } + if (downloadMediaItemWaitingForNotificationPermission != null) { + // Download with or without permission to post notifications. + toggleDownload(downloadMediaItemWaitingForNotificationPermission); + downloadMediaItemWaitingForNotificationPermission = null; + } + } + + private void handleExternalStoragePermissionGrantResults(int[] grantResults) { + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadSample(); + } else { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + finish(); + } + } + + private void loadSample() { + checkNotNull(uris); + + for (int i = 0; i < uris.length; i++) { + Uri uri = Uri.parse(uris[i]); + if (Util.maybeRequestReadExternalStoragePermission(this, uri)) { + return; + } + } + + SampleListLoader loaderTask = new SampleListLoader(); + loaderTask.execute(uris); + } + + private void onPlaylistGroups(final List groups, boolean sawError) { + if (sawError) { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + } + group = groups.get(0); + + SharedPreferences preferences = getPreferences(MODE_PRIVATE); + int groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1); + int childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1); + // Clear the group and child position if either are unset or if either are out of bounds. + if (groupPosition != -1 + && childPosition != -1 + && groupPosition < groups.size() + && childPosition < groups.get(groupPosition).playlists.size()) { + } + } + + @Override + public boolean onChildClick( + ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + // Save the selected item first to be able to restore it if the tested code crashes. + SharedPreferences.Editor prefEditor = getPreferences(MODE_PRIVATE).edit(); + prefEditor.putInt(GROUP_POSITION_PREFERENCE_KEY, groupPosition); + prefEditor.putInt(CHILD_POSITION_PREFERENCE_KEY, childPosition); + prefEditor.apply(); + + PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag(); + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra("Title",epg.getEPGData().getEvent(curProgramPosition,0).getTitle().toCharArray()); + intent.putExtra( + IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + IntentUtil.addToIntent(playlistHolder.mediaItems, intent); +// startActivity(intent); + this.childPosition = childPosition; + startActivityForResult(intent,PLAYER_REQUEST); + return true; + } + + private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) { + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder); + if (downloadUnsupportedStringId != 0) { + Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) + .show(); + } else if (!notificationPermissionToastShown + && Util.SDK_INT >= 33 + && checkSelfPermission(Api33.getPostNotificationPermissionString()) + != PackageManager.PERMISSION_GRANTED) { + downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0); + requestPermissions( + new String[] {Api33.getPostNotificationPermissionString()}, + /* requestCode= */ POST_NOTIFICATION_PERMISSION_REQUEST_CODE); + } else { + toggleDownload(playlistHolder.mediaItems.get(0)); + } + } + + private void toggleDownload(MediaItem mediaItem) { + RenderersFactory renderersFactory = + DemoUtil.buildRenderersFactory( + /* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem)); + downloadTracker.toggleDownload(getSupportFragmentManager(), mediaItem, renderersFactory); + } + + private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { + if (playlistHolder.mediaItems.size() > 1) { + return R.string.download_playlist_unsupported; + } + MediaItem.LocalConfiguration localConfiguration = + checkNotNull(playlistHolder.mediaItems.get(0).localConfiguration); + if (localConfiguration.adsConfiguration != null) { + return R.string.download_ads_unsupported; + } + String scheme = localConfiguration.uri.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + return R.string.download_scheme_unsupported; + } + return 0; + } + + private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) { + // Temporary workaround for layouts that do not inflate the options menu. + return menuItem != null && menuItem.isChecked(); + } + + private final class SampleListLoader extends AsyncTask> { + + private boolean sawError; + + @Override + protected List doInBackground(String... uris) { + + List result = new ArrayList<>(); + try { + URL url = new URL("https://www.ask-meyer.ch/IPTV/Channels.json"); + InputStream inputStream = url.openStream(); + readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); + } catch (Exception e) { + Log.e(TAG, "Error loading sample list: ", e); + sawError = true; + } + return result; + } + + @Override + protected void onPostExecute(List result) { + playlists = result.get(0).playlists; + onPlaylistGroups(result, sawError); + } + + private void readPlaylistGroups(JsonReader reader, List groups) + throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + readPlaylistGroup(reader, groups); + } + reader.endArray(); + } + + private void readPlaylistGroup(JsonReader reader, List groups) + throws IOException { + String groupName = ""; + ArrayList playlistHolders = new ArrayList<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "name": + groupName = reader.nextString(); + break; + case "samples": + reader.beginArray(); + while (reader.hasNext()) { + playlistHolders.add(readEntry(reader, false)); + } + reader.endArray(); + break; + case "_comment": + reader.nextString(); // Ignore. + break; + default: + throw new IOException("Unsupported name: " + name, /* cause= */ null); + } + } + reader.endObject(); + + PlaylistGroup group = getGroup(groupName, groups); + group.playlists.addAll(playlistHolders); + } + + private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { + Uri uri = null; + String extension = null; + String title = null; + ArrayList children = null; + Uri subtitleUri = null; + String subtitleMimeType = null; + String subtitleLanguage = null; + UUID drmUuid = null; + String drmLicenseUri = null; + ImmutableMap drmLicenseRequestHeaders = ImmutableMap.of(); + boolean drmSessionForClearContent = false; + boolean drmMultiSession = false; + boolean drmForceDefaultLicenseUri = false; + MediaItem.ClippingConfiguration.Builder clippingConfiguration = + new ClippingConfiguration.Builder(); + + MediaItem.Builder mediaItem = new MediaItem.Builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "name": + title = reader.nextString(); + break; + case "logo": + reader.nextString(); + break; + case "uri": + uri = Uri.parse(reader.nextString()); + break; + case "extension": + extension = reader.nextString(); + break; + case "clip_start_position_ms": + clippingConfiguration.setStartPositionMs(reader.nextLong()); + break; + case "clip_end_position_ms": + clippingConfiguration.setEndPositionMs(reader.nextLong()); + break; + case "ad_tag_uri": + mediaItem.setAdsConfiguration( + new MediaItem.AdsConfiguration.Builder(Uri.parse(reader.nextString())).build()); + break; + case "drm_scheme": + drmUuid = Util.getDrmUuid(reader.nextString()); + break; + case "drm_license_uri": + case "drm_license_url": // For backward compatibility only. + drmLicenseUri = reader.nextString(); + break; + case "drm_key_request_properties": + Map requestHeaders = new HashMap<>(); + reader.beginObject(); + while (reader.hasNext()) { + requestHeaders.put(reader.nextName(), reader.nextString()); + } + reader.endObject(); + drmLicenseRequestHeaders = ImmutableMap.copyOf(requestHeaders); + break; + case "drm_session_for_clear_content": + drmSessionForClearContent = reader.nextBoolean(); + break; + case "drm_multi_session": + drmMultiSession = reader.nextBoolean(); + break; + case "drm_force_default_license_uri": + drmForceDefaultLicenseUri = reader.nextBoolean(); + break; + case "subtitle_uri": + subtitleUri = Uri.parse(reader.nextString()); + break; + case "subtitle_mime_type": + subtitleMimeType = reader.nextString(); + break; + case "subtitle_language": + subtitleLanguage = reader.nextString(); + break; + case "playlist": + checkState(!insidePlaylist, "Invalid nesting of playlists"); + children = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + children.add(readEntry(reader, /* insidePlaylist= */ true)); + } + reader.endArray(); + break; + default: + throw new IOException("Unsupported attribute name: " + name, /* cause= */ null); + } + } + reader.endObject(); + + if (children != null) { + List mediaItems = new ArrayList<>(); + for (int i = 0; i < children.size(); i++) { + mediaItems.addAll(children.get(i).mediaItems); + } + return new PlaylistHolder(title, mediaItems); + } else { + @Nullable + String adaptiveMimeType = + Util.getAdaptiveMimeTypeForContentType( + TextUtils.isEmpty(extension) + ? Util.inferContentType(uri) + : Util.inferContentTypeForExtension(extension)); + mediaItem + .setUri(uri) + .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) + .setMimeType(adaptiveMimeType) + .setClippingConfiguration(clippingConfiguration.build()); + if (drmUuid != null) { + mediaItem.setDrmConfiguration( + new MediaItem.DrmConfiguration.Builder(drmUuid) + .setLicenseUri(drmLicenseUri) + .setLicenseRequestHeaders(drmLicenseRequestHeaders) + .setForceSessionsForAudioAndVideoTracks(drmSessionForClearContent) + .setMultiSession(drmMultiSession) + .setForceDefaultLicenseUri(drmForceDefaultLicenseUri) + .build()); + } else { + checkState(drmLicenseUri == null, "drm_uuid is required if drm_license_uri is set."); + checkState( + drmLicenseRequestHeaders.isEmpty(), + "drm_uuid is required if drm_key_request_properties is set."); + checkState( + !drmSessionForClearContent, + "drm_uuid is required if drm_session_for_clear_content is set."); + checkState(!drmMultiSession, "drm_uuid is required if drm_multi_session is set."); + checkState( + !drmForceDefaultLicenseUri, + "drm_uuid is required if drm_force_default_license_uri is set."); + } + if (subtitleUri != null) { + MediaItem.SubtitleConfiguration subtitleConfiguration = + new MediaItem.SubtitleConfiguration.Builder(subtitleUri) + .setMimeType( + checkNotNull( + subtitleMimeType, + "subtitle_mime_type is required if subtitle_uri is set.")) + .setLanguage(subtitleLanguage) + .build(); + mediaItem.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); + } + return new PlaylistHolder(title, Collections.singletonList(mediaItem.build())); + } + } + + private PlaylistGroup getGroup(String groupName, List groups) { + for (int i = 0; i < groups.size(); i++) { + if (Objects.equal(groupName, groups.get(i).title)) { + return groups.get(i); + } + } + PlaylistGroup group = new PlaylistGroup(groupName); + groups.add(group); + return group; + } + } + + private static final class PlaylistHolder { + + public final String title; + public final List mediaItems; + + private PlaylistHolder(String title, List mediaItems) { + checkArgument(!mediaItems.isEmpty()); + this.title = title; + this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems)); + } + } + + private static final class PlaylistGroup { + + public final String title; + public final List playlists; + + public PlaylistGroup(String title) { + this.title = title; + this.playlists = new ArrayList<>(); + } + } + + @RequiresApi(33) + private static class Api33 { + + @DoNotInline + public static String getPostNotificationPermissionString() { + return Manifest.permission.POST_NOTIFICATIONS; + } + } + + private void onSampleSelected(PlaylistHolder holder) { + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra("Title",epg.getEPGData().getEvent(curChannelPosition,0).getTitle().toCharArray()); + intent.putExtra( + IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + IntentUtil.addToIntent(holder.mediaItems, intent); + startActivityForResult(intent,PLAYER_REQUEST); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + // Check which request we're responding to + if (requestCode == PLAYER_REQUEST) { + // Make sure the request was successful + if (resultCode == RESULT_OK) { + if (isExternalURL) { + Intent ret = getIntent(); + ret.putExtra("OFFSET", data.getIntExtra(CHANNEL_OFFSET, 0)); + setResult(RESULT_OK, ret); + finish(); + } else { + int size = group.playlists.size(); + childPosition = childPosition + data.getIntExtra(CHANNEL_OFFSET, 0); + if (childPosition >= size) + childPosition = 0; + else if (childPosition < 0) + childPosition = size - 1; + curChannelPosition = childPosition; + onSampleSelected(group.playlists.get(childPosition)); + } + } else { + if (isExternalURL) { + setResult(RESULT_CANCELED, null); + finish(); + } + } + } + } + + + + + public void setTvGuide(final List channels) { + epgData = new EPGData() { + public EPGChannel getChannel(int position) { + return new EPGChannel(channels.get(position).getLogo(), + channels.get(position).getName(), + channels.get(position).getId(), + channels.get(position).getStream(), + channels.get(position).getDrmScheme(), + channels.get(position).getDrmUri()); + } + public List getEvents(int channelPosition) { + List programs = channels.get(channelPosition).getPrograms(); + List epgList = Collections.synchronizedList(new ArrayList<>()); + for (Program program : programs) { + epgList.add(new EPGEvent(program.getStartAsString(), program.getStopAsString(), program.getName(), + program.getCategories(), program.getCountry(), program.getDate(), program.getDescription(), program.getDirector(), program.getWriter(), program.getActors())); + } + return epgList; + } + public EPGEvent getEvent(int channelPosition, int programPosition) { + Program program = channels.get(channelPosition).getPrograms().get(programPosition); + return new EPGEvent(program.getStartAsString(), program.getStopAsString(), program.getName(), + program.getCategories(), program.getCountry(), program.getDate(), program.getDescription(), program.getDirector(), program.getWriter(), program.getActors()); + } + public int getChannelCount() { return channels.size(); } + public boolean hasData() { return true; } + }; + epg.setEPGData(epgData); + epg.recalculateAndRedraw(false); + } + + public void onChannelClicked(int channelPosition, EPGChannel epgChannel) { + curChannelPosition=channelPosition; + childPosition=channelPosition; + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra("Title",epg.getEPGData().getEvent(curChannelPosition,0).getTitle().toCharArray()); + intent.putExtra( + IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + IntentUtil.addToIntent(playlists.get(channelPosition).mediaItems, intent); + startActivityForResult(intent,PLAYER_REQUEST); + } + + public void onEventClicked(int channelPosition, int programPosition, EPGEvent epgEvent) { + final boolean isCurrent = epgEvent.isCurrent(); + final Dialog dialog = new Dialog(this); // Context, this, etc. + curChannelPosition=channelPosition; + curProgramPosition=programPosition; + dialog.setContentView(R.layout.dialog_description); + dialog.getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.id.dialog_costumtitle); + Button bt_no = (Button)dialog.findViewById(R.id.dialog_cancel); + final LinearLayout border_no = (LinearLayout)dialog.findViewById(R.id.dialog_cancel_select); + bt_no.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (hasFocus) { + border_no.setBackgroundColor(Color.parseColor("#76d8e2")); + } else { + border_no.setBackgroundColor(Color.parseColor("#cccccc")); + } + } + }); + Button bt_yes = (Button) dialog.findViewById(R.id.dialog_ok); + final LinearLayout border_yes = (LinearLayout)dialog.findViewById(R.id.dialog_ok_select); + bt_yes.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (hasFocus) { + border_yes.setBackgroundColor(Color.parseColor("#76d8e2")); + } else { + border_yes.setBackgroundColor(Color.parseColor("#787878")); + } + } + }); + dialog.setOnKeyListener(new DialogInterface.OnKeyListener(){ + @Override + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) + { + if (event.getAction() != KeyEvent.ACTION_DOWN) + return false; + if(keyCode == KeyEvent.KEYCODE_ENTER) + { + if( ((ColorDrawable)border_yes.getBackground()).getColor() == Color.parseColor("#76d8e2")) { + if (isCurrent) + onChannelClicked(curChannelPosition, epg.getEPGData().getChannel(curChannelPosition)); + else { + EPGEvent epgEvent = epg.getEPGData().getEvents(curChannelPosition).get(curProgramPosition); + new SampleChooserActivity.doRecord().execute("https://www.ask-meyer.ch/cgi-bin/record.cgi?" + + "url=" + Uri.encode(epg.getEPGData().getChannel(curChannelPosition).getStreamURL()) + + "&prg=" + Uri.encode(epg.getEPGData().getChannel(curChannelPosition).getName() + + "."+epgEvent.getStartAsString().substring(0,8) + + "."+epgEvent.getStartAsString().substring(8,12) + + "."+epgEvent.getEndAsString().substring(8,12) + + ".mp4") + + "&start=" + Uri.encode(epgEvent.getStartAsString()) + + "&stop=" + Uri.encode(epgEvent.getEndAsString())); + } + } + dialog.dismiss(); + return true; + } + return false; + } + }); + TextView title = (TextView)dialog.findViewById(R.id.dialog_costumtitle); + title.setText(epgEvent.getTitle()); + TextView date = (TextView)dialog.findViewById(R.id.dialog_date); + date.setText(epgEvent.getDate()); + TextView country = (TextView)dialog.findViewById(R.id.dialog_country); + country.setText(epgEvent.getCountry()); + TextView categories = (TextView)dialog.findViewById(R.id.dialog_categories); + if (epgEvent.getCategories()!=null) { + String res = new String(); + for (String s : epgEvent.getCategories()) { + res += s + ", "; + } + categories.setText(res.substring(0,res.length()-2)); + dialog.findViewById(R.id.dialog_categories_layout).setVisibility(View.VISIBLE); + }else + dialog.findViewById(R.id.dialog_categories_layout).setVisibility(View.GONE); + TextView director = (TextView)dialog.findViewById(R.id.dialog_director); + if (epgEvent.getDirector()!=null) { + director.setText(epgEvent.getDirector()); + dialog.findViewById(R.id.dialog_director_layout).setVisibility(View.VISIBLE); + }else + dialog.findViewById(R.id.dialog_director_layout).setVisibility(View.GONE); + TextView actors = (TextView)dialog.findViewById(R.id.dialog_actors); + if (epgEvent.getActors()!=null) { + String res = new String(); + for (String s : epgEvent.getActors()) { + res += s + ", "; + } + actors.setText(res.substring(0,res.length()-2)); + dialog.findViewById(R.id.dialog_actors_layout).setVisibility(View.VISIBLE); + }else + dialog.findViewById(R.id.dialog_actors_layout).setVisibility(View.GONE); + TextView description = (TextView)dialog.findViewById(R.id.dialog_info); + description.setText(epgEvent.getDescription()); + if (isCurrent) + bt_yes.setText("Play"); + else { + bt_yes.setText("Record"); +// bt_yes.setEnabled(false); + } + + bt_yes.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + if (isCurrent) + onChannelClicked(curChannelPosition, epg.getEPGData().getChannel(curChannelPosition)); + else { + EPGEvent epgEvent = epg.getEPGData().getEvents(curChannelPosition).get(curProgramPosition); + new SampleChooserActivity.doRecord().execute("https://www.ask-meyer.ch/cgi-bin/record.cgi?" + + "url=" + Uri.encode(epg.getEPGData().getChannel(curChannelPosition).getStreamURL()) + + "&prg=" + Uri.encode(epg.getEPGData().getChannel(curChannelPosition).getName() + + "."+epgEvent.getStartAsString().substring(0,8) + + "."+epgEvent.getStartAsString().substring(8,12) + + "."+epgEvent.getEndAsString().substring(8,12) + + ".mp4") + + "&start=" + Uri.encode(epgEvent.getStartAsString()) + + "&stop=" + Uri.encode(epgEvent.getEndAsString())); + } + } + }); + bt_no.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + + dialog.show(); + } + + private class Channels extends AsyncTask> { + @Override + protected List doInBackground(Void... params) { + List list = new ArrayList(); + try { + URL url = new URL("https://www.ask-meyer.ch/IPTV/Channels.json"); + JsonReader reader = new JsonReader(new InputStreamReader(url.openStream(), "UTF-8")); + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "name": + name = reader.nextString(); + break; + case "samples": + + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + String[] bundle = new String[5]; + while (reader.hasNext()) { + name = reader.nextName(); + switch (name) { + case "name": + bundle[0] = reader.nextString(); + break; + case "logo": + bundle[1] = reader.nextString(); + break; + case "uri": + bundle[2] = reader.nextString(); + break; + case "drm_scheme": + bundle[3] = reader.nextString(); + break; + case "drm_license_uri": + bundle[4] = reader.nextString(); + break; + default: + throw new IOException("Unsupported name: " + name, /* cause= */ null); + } + } + list.add(bundle); + reader.endObject(); + } + reader.endArray(); + + break; + default: + throw new IOException("Unsupported name: " + name, /* cause= */ null); + } + } + reader.endObject(); + } + reader.endArray(); + } catch (Exception e) { } + return list; + } + + @Override + protected void onPostExecute(List list) { + String[][] channels = new String[list.size()][5]; + channels = list.toArray(channels); + ContentTask contentTask = new ContentTask(SampleChooserActivity.this); + contentTask.execute(channels); + curChannelPosition = -1; + curProgramPosition = -1; + } + } + + private class doRecord extends AsyncTask { + @Override + protected String doInBackground(String... urls) { + try { + URL url = new URL(urls[0]); + BufferedReader rd = new BufferedReader(new InputStreamReader(url.openStream())); + String content = "", line; + while ((line = rd.readLine()) != null) { + content += line + "\n"; + } + return content; + } catch (IOException exception) {} + return null; + } + @Override + protected void onPostExecute(String content) { + Toast.makeText(SampleChooserActivity.this, content, Toast.LENGTH_SHORT).show(); + } + } + + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + hideNavigationBar(); + } + } + + private void hideNavigationBar() { + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); + } +} diff --git a/app/src/main/java/com/example/exoplayertv/StartActivityOnBootReceiver.java b/app/src/main/java/com/example/exoplayertv/StartActivityOnBootReceiver.java new file mode 100644 index 0000000..e59a653 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/StartActivityOnBootReceiver.java @@ -0,0 +1,17 @@ +package com.example.exoplayertv; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import com.example.exoplayertv.DemoDownloadService; + +public class StartActivityOnBootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if(Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + Intent i = new Intent(context, SampleChooserActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + } + } +} diff --git a/app/src/main/java/com/example/exoplayertv/TrackSelectionDialog.java b/app/src/main/java/com/example/exoplayertv/TrackSelectionDialog.java new file mode 100644 index 0000000..c197f1f --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/TrackSelectionDialog.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.exoplayertv; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import androidx.media3.common.C; +import androidx.media3.common.Player; +import androidx.media3.common.Tracks; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.ui.TrackSelectionView; +import com.google.android.material.tabs.TabLayout; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Dialog to select tracks. */ +public final class TrackSelectionDialog extends DialogFragment { + + /** Called when tracks are selected. */ + public interface TrackSelectionListener { + + /** + * Called when tracks are selected. + * + * @param trackSelectionParameters A {@link TrackSelectionParameters} representing the selected + * tracks. Any manual selections are defined by {@link + * TrackSelectionParameters#disabledTrackTypes} and {@link + * TrackSelectionParameters#overrides}. + */ + void onTracksSelected(TrackSelectionParameters trackSelectionParameters); + } + + public static final ImmutableList SUPPORTED_TRACK_TYPES = + ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); + + private final SparseArray tabFragments; + private final ArrayList tabTrackTypes; + + private int titleId; + private DialogInterface.OnClickListener onClickListener; + private DialogInterface.OnDismissListener onDismissListener; + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link Player}. + */ + public static boolean willHaveContent(Player player) { + return willHaveContent(player.getCurrentTracks()); + } + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link Tracks}. + */ + public static boolean willHaveContent(Tracks tracks) { + for (Tracks.Group trackGroup : tracks.getGroups()) { + if (SUPPORTED_TRACK_TYPES.contains(trackGroup.getType())) { + return true; + } + } + return false; + } + + /** + * Creates a dialog for a given {@link Player}, whose parameters will be automatically updated + * when tracks are selected. + * + * @param player The {@link Player}. + * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForPlayer( + Player player, DialogInterface.OnDismissListener onDismissListener) { + return createForTracksAndParameters( + R.string.track_selection_title, + player.getCurrentTracks(), + player.getTrackSelectionParameters(), + /* allowAdaptiveSelections= */ true, + /* allowMultipleOverrides= */ false, + player::setTrackSelectionParameters, + onDismissListener); + } + + /** + * Creates a dialog for given {@link Tracks} and {@link TrackSelectionParameters}. + * + * @param titleId The resource id of the dialog title. + * @param tracks The {@link Tracks} describing the tracks to display. + * @param trackSelectionParameters The initial {@link TrackSelectionParameters}. + * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) + * can be made. + * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. + * @param trackSelectionListener Called when tracks are selected. + * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForTracksAndParameters( + int titleId, + Tracks tracks, + TrackSelectionParameters trackSelectionParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + TrackSelectionListener trackSelectionListener, + DialogInterface.OnDismissListener onDismissListener) { + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + trackSelectionDialog.init( + tracks, + trackSelectionParameters, + titleId, + allowAdaptiveSelections, + allowMultipleOverrides, + /* onClickListener= */ (dialog, which) -> { + TrackSelectionParameters.Builder builder = trackSelectionParameters.buildUpon(); + for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { + int trackType = SUPPORTED_TRACK_TYPES.get(i); + builder.setTrackTypeDisabled(trackType, trackSelectionDialog.getIsDisabled(trackType)); + builder.clearOverridesOfType(trackType); + Map overrides = + trackSelectionDialog.getOverrides(trackType); + for (TrackSelectionOverride override : overrides.values()) { + builder.addOverride(override); + } + } + trackSelectionListener.onTracksSelected(builder.build()); + }, + onDismissListener); + return trackSelectionDialog; + } + + public TrackSelectionDialog() { + tabFragments = new SparseArray<>(); + tabTrackTypes = new ArrayList<>(); + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + private void init( + Tracks tracks, + TrackSelectionParameters trackSelectionParameters, + int titleId, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + this.titleId = titleId; + this.onClickListener = onClickListener; + this.onDismissListener = onDismissListener; + + for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { + @C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i); + ArrayList trackGroups = new ArrayList<>(); + for (Tracks.Group trackGroup : tracks.getGroups()) { + if (trackGroup.getType() == trackType) { + trackGroups.add(trackGroup); + } + } + if (!trackGroups.isEmpty()) { + TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); + tabFragment.init( + trackGroups, + trackSelectionParameters.disabledTrackTypes.contains(trackType), + trackSelectionParameters.overrides, + allowAdaptiveSelections, + allowMultipleOverrides); + tabFragments.put(trackType, tabFragment); + tabTrackTypes.add(trackType); + } + } + } + + /** + * Returns whether the disabled option is selected for the specified track type. + * + * @param trackType The track type. + * @return Whether the disabled option is selected for the track type. + */ + public boolean getIsDisabled(int trackType) { + TrackSelectionViewFragment trackView = tabFragments.get(trackType); + return trackView != null && trackView.isDisabled; + } + + /** + * Returns the selected track overrides for the specified track type. + * + * @param trackType The track type. + * @return The track overrides for the track type. + */ + public Map getOverrides(int trackType) { + TrackSelectionViewFragment trackView = tabFragments.get(trackType); + return trackView == null ? Collections.emptyMap() : trackView.overrides; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // We need to own the view to let tab layout work correctly on all API levels. We can't use + // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using + // the AlertDialog theme overlay with force-enabled title. + AppCompatDialog dialog = + new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); + dialog.setTitle(titleId); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + onDismissListener.onDismiss(dialog); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); + TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); + ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); + Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); + Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); + viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager())); + tabLayout.setupWithViewPager(viewPager); + tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); + cancelButton.setOnClickListener(view -> dismiss()); + okButton.setOnClickListener( + view -> { + onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); + dismiss(); + }); + return dialogView; + } + + private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return resources.getString(com.google.android.exoplayer2.ui.R.string.exo_track_selection_title_video); + case C.TRACK_TYPE_AUDIO: + return resources.getString(com.google.android.exoplayer2.ui.R.string.exo_track_selection_title_audio); + case C.TRACK_TYPE_TEXT: + return resources.getString(com.google.android.exoplayer2.ui.R.string.exo_track_selection_title_text); + default: + throw new IllegalArgumentException(); + } + } + + private final class FragmentAdapter extends FragmentPagerAdapter { + + public FragmentAdapter(FragmentManager fragmentManager) { + super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @Override + public Fragment getItem(int position) { + return tabFragments.get(tabTrackTypes.get(position)); + } + + @Override + public int getCount() { + return tabTrackTypes.size(); + } + + @Override + public CharSequence getPageTitle(int position) { + return getTrackTypeString(getResources(), tabTrackTypes.get(position)); + } + } + + /** Fragment to show a track selection in tab of the track selection dialog. */ + public static final class TrackSelectionViewFragment extends Fragment + implements TrackSelectionView.TrackSelectionListener { + + private List trackGroups; + private boolean allowAdaptiveSelections; + private boolean allowMultipleOverrides; + + /* package */ boolean isDisabled; + /* package */ Map overrides; + + public TrackSelectionViewFragment() { + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + public void init( + List trackGroups, + boolean isDisabled, + Map overrides, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides) { + this.trackGroups = trackGroups; + this.isDisabled = isDisabled; + this.allowAdaptiveSelections = allowAdaptiveSelections; + this.allowMultipleOverrides = allowMultipleOverrides; + // TrackSelectionView does this filtering internally, but we need to do it here as well to + // handle the case where the TrackSelectionView is never created. + this.overrides = + new HashMap<>( + TrackSelectionView.filterOverrides(overrides, trackGroups, allowMultipleOverrides)); + } + + @Override + public View onCreateView( + LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = + inflater.inflate( + com.google.android.exoplayer2.ui.R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); + TrackSelectionView trackSelectionView = rootView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_track_selection_view); + trackSelectionView.setShowDisableOption(true); + trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); + trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); + trackSelectionView.init( + trackGroups, + isDisabled, + overrides, + /* trackFormatComparator= */ null, + /* listener= */ this); + return rootView; + } + + @Override + public void onTrackSelectionChanged( + boolean isDisabled, Map overrides) { + this.isDisabled = isDisabled; + this.overrides = overrides; + } + } +} diff --git a/app/src/main/java/com/example/exoplayertv/package-info.java b/app/src/main/java/com/example/exoplayertv/package-info.java new file mode 100644 index 0000000..5d21a60 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.example.exoplayertv; + +import androidx.media3.common.util.NonNullApi; diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPG.java b/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPG.java new file mode 100644 index 0000000..54e477a --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPG.java @@ -0,0 +1,816 @@ +package com.example.exoplayertv.tvepg.epg; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.Scroller; + +import com.example.exoplayertv.R; +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Target; + +import org.joda.time.LocalDateTime; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.example.exoplayertv.tvepg.epg.domain.EPGEvent; +import com.example.exoplayertv.tvepg.epg.misc.EPGUtil; + +/** + * Classic EPG, electronic program guide, that scrolls both horizontal, vertical and diagonal. + * It utilize onDraw() to draw the graphic on screen. So there are some private helper methods calculating positions etc. + * Listed on Y-axis are channels and X-axis are programs/events. Data is added to EPG by using setEPGData() + * and pass in an EPGData implementation. A click listener can be added using setEPGClickListener(). + * Created by Kristoffer, http://kmdev.se + */ +public class EPG extends ViewGroup { + + public final String TAG = getClass().getSimpleName(); + public static final int DAYS_BACK_MILLIS = 3 * 24 * 60 * 60 * 1000; // 3 days + public static final int DAYS_FORWARD_MILLIS = 3 * 24 * 60 * 60 * 1000; // 3 days + public static final int HOURS_IN_VIEWPORT_MILLIS = 2 * 60 * 60 * 1000; // 2 hours + public static final int TIME_LABEL_SPACING_MILLIS = 30 * 60 * 1000; // 30 minutes + + private final Rect mClipRect; + private final Rect mDrawingRect; + private final Rect mMeasuringRect; + private final Paint mPaint; + private final Scroller mScroller; + private final GestureDetector mGestureDetector; + + private final int mChannelLayoutMargin; + private final int mChannelLayoutPadding; + private final int mChannelLayoutHeight; + private final int mChannelLayoutWidth; + private final int mChannelLayoutBackground; + private final int mEventLayoutBackground; + private final int mEventLayoutBackgroundCurrent; + private final int mEventLayoutBackgroundSelected; + private final int mEventLayoutBackgroundCurrentSelected; + private final int mEventLayoutTextColor; + private final int mEventLayoutTextSize; + private final int mTimeBarLineWidth; + private final int mTimeBarLineColor; + private final int mTimeBarHeight; + private final int mTimeBarTextSize; + + private final int mResetButtonSize; + private final int mResetButtonMargin; + private final Bitmap mResetButtonIcon; + + private final int mEPGBackground; + private final Map mChannelImageCache; + private final Map mChannelImageTargetCache; + + private EPGClickListener mClickListener; + private int mMaxHorizontalScroll; + private int mMaxVerticalScroll; + private long mMillisPerPixel; + private long mTimeOffset; + private long mTimeLowerBoundary; + private long mTimeUpperBoundary; + + private EPGData epgData = null; + + private int channel = 1; + private long playtime = -1; + private long timer = 0; + private static final int duration = 200; + + public EPG(Context context) { + this(context, null); + } + + public EPG(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public EPG(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + setWillNotDraw(false); + + resetBoundaries(); + + mDrawingRect = new Rect(); + mClipRect = new Rect(); + mMeasuringRect = new Rect(); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mGestureDetector = new GestureDetector(context, new OnGestureListener()); + mChannelImageCache = Collections.synchronizedMap(new HashMap()); + mChannelImageTargetCache = Collections.synchronizedMap(new HashMap()); + + // Adding some friction that makes the epg less flappy. + mScroller = new Scroller(context); + mScroller.setFriction(0.2f); + + mEPGBackground = getResources().getColor(R.color.epg_background); + + mChannelLayoutMargin = getResources().getDimensionPixelSize(R.dimen.epg_channel_layout_margin); + mChannelLayoutPadding = getResources().getDimensionPixelSize(R.dimen.epg_channel_layout_padding); + mChannelLayoutHeight = getResources().getDimensionPixelSize(R.dimen.epg_channel_layout_height); + mChannelLayoutWidth = getResources().getDimensionPixelSize(R.dimen.epg_channel_layout_width); + mChannelLayoutBackground = getResources().getColor(R.color.epg_channel_layout_background); + + mEventLayoutBackground = getResources().getColor(R.color.epg_event_layout_background); + mEventLayoutBackgroundCurrent = getResources().getColor(R.color.epg_event_layout_background_current); + mEventLayoutBackgroundSelected = getResources().getColor(R.color.epg_event_layout_background_selected); + mEventLayoutBackgroundCurrentSelected = getResources().getColor(R.color.epg_event_layout_background_current_selected); + mEventLayoutTextColor = getResources().getColor(R.color.epg_event_layout_text); + mEventLayoutTextSize = getResources().getDimensionPixelSize(R.dimen.epg_event_layout_text); + + mTimeBarHeight = getResources().getDimensionPixelSize(R.dimen.epg_time_bar_height); + mTimeBarTextSize = getResources().getDimensionPixelSize(R.dimen.epg_time_bar_text); + mTimeBarLineWidth = getResources().getDimensionPixelSize(R.dimen.epg_time_bar_line_width); + mTimeBarLineColor = getResources().getColor(R.color.epg_time_bar); + + mResetButtonSize = getResources().getDimensionPixelSize(R.dimen.epg_reset_button_size); + mResetButtonMargin = getResources().getDimensionPixelSize(R.dimen.epg_reset_button_margin); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.outWidth = mResetButtonSize; + options.outHeight = mResetButtonSize; + mResetButtonIcon = BitmapFactory.decodeResource(getResources(), R.drawable.reset, options); + + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + channel=0; + playtime = System.currentTimeMillis(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + long delta = 0; + if ( event.getEventTime() < timer || + event.getAction()==KeyEvent.ACTION_UP) + return false; + timer = event.getEventTime()+duration+200; + List events = epgData.getEvents(channel); + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_ENTER: + for (int program = 0; program < events.size(); program++) { + EPGEvent epgEvnt = events.get(program); + if (playtime >= epgEvnt.getStart() && playtime < epgEvnt.getEnd()) { + mClickListener.onEventClicked(channel, program, epgData.getEvent(channel, program)); + return true; + } + } + return false; + case KeyEvent.KEYCODE_DPAD_DOWN: + channel++; + if (channel >= epgData.getChannelCount()) + channel = epgData.getChannelCount() - 1; + events = epgData.getEvents(channel); + if (playtime < events.get(0).getStart()) + playtime = events.get(0).getStart(); + if (getScrollY() != mMaxVerticalScroll && channel >= getLastVisibleChannelPosition() - 2) + delta = mChannelLayoutHeight + mChannelLayoutMargin; + if (getScrollY() + delta > mMaxVerticalScroll) + delta = mMaxVerticalScroll - getScrollY(); + mScroller.startScroll(getScrollX(), getScrollY(), + getXFrom(playtime - (HOURS_IN_VIEWPORT_MILLIS / 2)) - getScrollX(), + (int) delta, true ? duration : 0); + redraw(); + return true; + case KeyEvent.KEYCODE_DPAD_UP: + channel--; + if (channel < 0) + channel = 0; + events = epgData.getEvents(channel); + if (playtime < events.get(0).getStart()) + playtime = events.get(0).getStart(); + if (getScrollY() != 0 && channel <= getFirstVisibleChannelPosition()) + delta = -mChannelLayoutHeight - mChannelLayoutMargin; + if (getScrollY() + delta < 0) + delta = -getScrollY(); + mScroller.startScroll(getScrollX(), getScrollY(), + getXFrom(playtime - (HOURS_IN_VIEWPORT_MILLIS / 2)) - getScrollX(), + (int) delta, true ? duration : 0); + redraw(); + return true; + case KeyEvent.KEYCODE_DPAD_LEFT: + for (int program = 0; program < events.size(); program++) { + EPGEvent epgEvnt = events.get(program); + if (playtime > epgEvnt.getStart() && playtime <= epgEvnt.getEnd()) { + delta = epgEvnt.getStart() - epgEvnt.getEnd(); + playtime = epgEvnt.getStart(); + delta = delta / calculateMillisPerPixel(); + if (getScrollX() + delta < 0) + delta = 0 - getScrollX(); + mScroller.startScroll(getScrollX(), getScrollY(), + (int) delta, 0, true ? duration : 0); + redraw(); + return true; + } + } + return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: + for (int program = 0; program < events.size(); program++) { + EPGEvent epgEvnt = events.get(program); + if (playtime >= epgEvnt.getStart() && playtime < epgEvnt.getEnd()) { + delta = epgEvnt.getEnd() - epgEvnt.getStart(); + playtime = epgEvnt.getEnd(); + delta = delta / calculateMillisPerPixel(); + if (getScrollX() + delta > mMaxHorizontalScroll) + delta = mMaxHorizontalScroll - getScrollX(); + mScroller.startScroll(getScrollX(), getScrollY(), + (int) delta, 0, true ? duration : 0); + redraw(); + return true; + } + } + } + return false; + } + + + public EPGData getEPGData() { return epgData; } + + + @Override + protected void onDraw(Canvas canvas) { + + if (epgData != null && epgData.hasData()) { + mTimeLowerBoundary = getTimeFrom(getScrollX()); + mTimeUpperBoundary = getTimeFrom(getScrollX() + getWidth()); + + Rect drawingRect = mDrawingRect; + drawingRect.left = getScrollX(); + drawingRect.top = getScrollY(); + drawingRect.right = drawingRect.left + getWidth(); + drawingRect.bottom = drawingRect.top + getHeight(); + + drawChannelListItems(canvas, drawingRect); + drawEvents(canvas, drawingRect); + drawTimebar(canvas, drawingRect); + drawTimeLine(canvas, drawingRect); + drawResetButton(canvas, drawingRect); + + // If scroller is scrolling/animating do scroll. This applies when doing a fling. + if (mScroller.computeScrollOffset()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + recalculateAndRedraw(false); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + + private void drawResetButton(Canvas canvas, Rect drawingRect) { + // Show button when scrolled 1/3 of screen width from current time + final long threshold = getWidth() / 3; + if (Math.abs(getXPositionStart() - getScrollX()) > threshold) { + drawingRect = calculateResetButtonHitArea(); + mPaint.setColor(mTimeBarLineColor); + canvas.drawCircle(drawingRect.right - (mResetButtonSize / 2), + drawingRect.bottom - (mResetButtonSize / 2), + Math.min(drawingRect.width(), drawingRect.height()) / 2, + mPaint); + + drawingRect.left += mResetButtonMargin; + drawingRect.right -= mResetButtonMargin; + drawingRect.top += mResetButtonMargin; + drawingRect.bottom -= mResetButtonMargin; + canvas.drawBitmap(mResetButtonIcon, null, drawingRect, mPaint); + } + } + + private void drawTimebarBottomStroke(Canvas canvas, Rect drawingRect) { + drawingRect.left = getScrollX(); + drawingRect.top = getScrollY() + mTimeBarHeight; + drawingRect.right = drawingRect.left + getWidth(); + drawingRect.bottom = drawingRect.top + mChannelLayoutMargin; + + // Bottom stroke + mPaint.setColor(mEPGBackground); + canvas.drawRect(drawingRect, mPaint); + } + + private void drawTimebar(Canvas canvas, Rect drawingRect) { + drawingRect.left = getScrollX() + mChannelLayoutWidth + mChannelLayoutMargin; + drawingRect.top = getScrollY(); + drawingRect.right = drawingRect.left + getWidth(); + drawingRect.bottom = drawingRect.top + mTimeBarHeight; + + mClipRect.left = getScrollX() + mChannelLayoutWidth + mChannelLayoutMargin; + mClipRect.top = getScrollY(); + mClipRect.right = getScrollX() + getWidth(); + mClipRect.bottom = mClipRect.top + mTimeBarHeight; + + canvas.save(); + canvas.clipRect(mClipRect); + + // Background + mPaint.setColor(mChannelLayoutBackground); + canvas.drawRect(drawingRect, mPaint); + + // Time stamps + mPaint.setColor(mEventLayoutTextColor); + mPaint.setTextSize(mTimeBarTextSize); + + for (int i = 0; i < HOURS_IN_VIEWPORT_MILLIS / TIME_LABEL_SPACING_MILLIS; i++) { + // Get time and round to nearest half hour + final long time = TIME_LABEL_SPACING_MILLIS * + (((mTimeLowerBoundary + (TIME_LABEL_SPACING_MILLIS * i)) + + (TIME_LABEL_SPACING_MILLIS / 2)) / TIME_LABEL_SPACING_MILLIS); + + canvas.drawText(EPGUtil.getShortTime(time), + getXFrom(time), + drawingRect.top + (((drawingRect.bottom - drawingRect.top) / 2) + (mTimeBarTextSize / 2)), mPaint); + } + + canvas.restore(); + + drawTimebarDayIndicator(canvas, drawingRect); + drawTimebarBottomStroke(canvas, drawingRect); + } + + private void drawTimebarDayIndicator(Canvas canvas, Rect drawingRect) { + drawingRect.left = getScrollX(); + drawingRect.top = getScrollY(); + drawingRect.right = drawingRect.left + mChannelLayoutWidth; + drawingRect.bottom = drawingRect.top + mTimeBarHeight; + + // Background + mPaint.setColor(mChannelLayoutBackground); + canvas.drawRect(drawingRect, mPaint); + + // Text + mPaint.setColor(mEventLayoutTextColor); + mPaint.setTextSize(mTimeBarTextSize); + mPaint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(EPGUtil.getWeekdayName(mTimeLowerBoundary), + drawingRect.left + ((drawingRect.right - drawingRect.left) / 2), + drawingRect.top + (((drawingRect.bottom - drawingRect.top) / 2) + (mTimeBarTextSize / 2)), mPaint); + + mPaint.setTextAlign(Paint.Align.LEFT); + } + + private void drawTimeLine(Canvas canvas, Rect drawingRect) { + long now = System.currentTimeMillis(); + + if (shouldDrawTimeLine(now)) { + drawingRect.left = getXFrom(now); + drawingRect.top = getScrollY(); + drawingRect.right = drawingRect.left + mTimeBarLineWidth; + drawingRect.bottom = drawingRect.top + getHeight(); + + mPaint.setColor(mTimeBarLineColor); + canvas.drawRect(drawingRect, mPaint); + } + + } + + private void drawEvents(Canvas canvas, Rect drawingRect) { + final int firstPos = getFirstVisibleChannelPosition(); + final int lastPos = getLastVisibleChannelPosition(); + + for (int pos = firstPos; pos <= lastPos; pos++) { + + // Set clip rectangle + mClipRect.left = getScrollX() + mChannelLayoutWidth + mChannelLayoutMargin; + mClipRect.top = getTopFrom(pos); + mClipRect.right = getScrollX() + getWidth(); + mClipRect.bottom = mClipRect.top + mChannelLayoutHeight; + + canvas.save(); + canvas.clipRect(mClipRect); + + // Draw each event + boolean foundFirst = false; + + List epgEvents = epgData.getEvents(pos); + + for (EPGEvent event : epgEvents) { + if (isEventVisible(event.getStart(), event.getEnd())) { + drawEvent(canvas, pos, event, drawingRect); + foundFirst = true; + } else if (foundFirst) { + break; + } + } + + canvas.restore(); + } + + } + + private void drawEvent(final Canvas canvas, final int channelPosition, final EPGEvent event, final Rect drawingRect) { + + setEventDrawingRectangle(channelPosition, event.getStart(), event.getEnd(), drawingRect); + + // Background + if (channelPosition==channel && playtime>=event.getStart() && playtimeup) + mPaint.getTextBounds(event.getTitle(), 0, event.getTitle().length(), mMeasuringRect); + drawingRect.top += (((drawingRect.bottom - drawingRect.top) / 2) + (mMeasuringRect.height()/2)); + + String title = event.getTitle(); + title = title.substring(0, + mPaint.breakText(title, true, drawingRect.right - drawingRect.left, null)); + canvas.drawText(title, drawingRect.left, drawingRect.top, mPaint); + + } + + private void setEventDrawingRectangle(final int channelPosition, final long start, final long end, final Rect drawingRect) { + drawingRect.left = getXFrom(start); + drawingRect.top = getTopFrom(channelPosition); + drawingRect.right = getXFrom(end) - mChannelLayoutMargin; + drawingRect.bottom = drawingRect.top + mChannelLayoutHeight; + } + + private void drawChannelListItems(Canvas canvas, Rect drawingRect) { + // Background + mMeasuringRect.left = getScrollX(); + mMeasuringRect.top = getScrollY(); + mMeasuringRect.right = drawingRect.left + mChannelLayoutWidth; + mMeasuringRect.bottom = mMeasuringRect.top + getHeight(); + + mPaint.setColor(mChannelLayoutBackground); + canvas.drawRect(mMeasuringRect, mPaint); + + final int firstPos = getFirstVisibleChannelPosition(); + final int lastPos = getLastVisibleChannelPosition(); + + for (int pos = firstPos; pos <= lastPos; pos++) { + drawChannelItem(canvas, pos, drawingRect); + } + } + + private void drawChannelItem(final Canvas canvas, int position, Rect drawingRect) { + drawingRect.left = getScrollX(); + drawingRect.top = getTopFrom(position); + drawingRect.right = drawingRect.left + mChannelLayoutWidth; + drawingRect.bottom = drawingRect.top + mChannelLayoutHeight; + + // Loading channel image into target for + final String imageURL = epgData.getChannel(position).getImageURL(); + + if (mChannelImageCache.containsKey(imageURL)) { + Bitmap image = mChannelImageCache.get(imageURL); + drawingRect = getDrawingRectForChannelImage(drawingRect, image); + canvas.drawBitmap(image, null, drawingRect, null); + } else { + final int smallestSide = Math.min(mChannelLayoutHeight, mChannelLayoutWidth); + + if (!mChannelImageTargetCache.containsKey(imageURL)) { + mChannelImageTargetCache.put(imageURL, new Target() { + @Override + public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { + mChannelImageCache.put(imageURL, bitmap); + redraw(); + mChannelImageTargetCache.remove(imageURL); + } + + @Override + public void onBitmapFailed(Exception e, Drawable errorDrawable) { + + } + + @Override + public void onPrepareLoad(Drawable placeHolderDrawable) { + + } + }); + + EPGUtil.loadImageInto(getContext(), imageURL, smallestSide, smallestSide, mChannelImageTargetCache.get(imageURL)); + } + + } + } + + private Rect getDrawingRectForChannelImage(Rect drawingRect, Bitmap image) { + drawingRect.left += mChannelLayoutPadding; + drawingRect.top += mChannelLayoutPadding; + drawingRect.right -= mChannelLayoutPadding; + drawingRect.bottom -= mChannelLayoutPadding; + + final int imageWidth = image.getWidth(); + final int imageHeight = image.getHeight(); + final float imageRatio = imageHeight / (float) imageWidth; + + final int rectWidth = drawingRect.right - drawingRect.left; + final int rectHeight = drawingRect.bottom - drawingRect.top; + + // Keep aspect ratio. + if (imageWidth > imageHeight) { + final int padding = (int) (rectHeight - (rectWidth * imageRatio)) / 2; + drawingRect.top += padding; + drawingRect.bottom -= padding; + } else if (imageWidth <= imageHeight) { + final int padding = (int) (rectWidth - (rectHeight / imageRatio)) / 2; + drawingRect.left += padding; + drawingRect.right -= padding; + } + + return drawingRect; + } + + private boolean shouldDrawTimeLine(long now) { + return now >= mTimeLowerBoundary && now < mTimeUpperBoundary; + } + + private boolean isEventVisible(final long start, final long end) { + return (start >= mTimeLowerBoundary && start <= mTimeUpperBoundary) + || (end >= mTimeLowerBoundary && end <= mTimeUpperBoundary) + || (start <= mTimeLowerBoundary && end >= mTimeUpperBoundary); + } + + private long calculatedBaseLine() { + return LocalDateTime.now().toDateTime().minusMillis(DAYS_BACK_MILLIS).getMillis(); + } + + private int getFirstVisibleChannelPosition() { + final int y = getScrollY(); + + int position = (y - mChannelLayoutMargin - mTimeBarHeight) + / (mChannelLayoutHeight + mChannelLayoutMargin); + + if (position < 0) { + position = 0; + } + return position; + } + + private int getLastVisibleChannelPosition() { + final int y = getScrollY(); + final int totalChannelCount = epgData.getChannelCount(); + final int screenHeight = getHeight(); + int position = (y + screenHeight + mTimeBarHeight - mChannelLayoutMargin) + / (mChannelLayoutHeight + mChannelLayoutMargin); + + if (position > totalChannelCount - 1) { + position = totalChannelCount - 1; + } + + // Add one extra row if we don't fill screen with current.. + return (y + screenHeight) > (position * mChannelLayoutHeight) && position < totalChannelCount - 1 ? position + 1 : position; + } + + private void calculateMaxHorizontalScroll() { + mMaxHorizontalScroll = (int) ((DAYS_BACK_MILLIS + DAYS_FORWARD_MILLIS - HOURS_IN_VIEWPORT_MILLIS) / mMillisPerPixel); + } + + private void calculateMaxVerticalScroll() { + final int maxVerticalScroll = getTopFrom(epgData.getChannelCount() - 1) + mChannelLayoutHeight; + mMaxVerticalScroll = maxVerticalScroll < getHeight() ? 0 : maxVerticalScroll - getHeight(); + } + + private int getXFrom(long time) { + return (int) ((time - mTimeOffset) / mMillisPerPixel) + mChannelLayoutMargin + + mChannelLayoutWidth + mChannelLayoutMargin; + } + + private int getTopFrom(int position) { + int y = position * (mChannelLayoutHeight + mChannelLayoutMargin) + + mChannelLayoutMargin + mTimeBarHeight; + return y; + } + + private long getTimeFrom(int x) { + return (x * mMillisPerPixel) + mTimeOffset; + } + + private long calculateMillisPerPixel() { + return HOURS_IN_VIEWPORT_MILLIS / (getResources().getDisplayMetrics().widthPixels - mChannelLayoutWidth - mChannelLayoutMargin); + } + + private int getXPositionStart() { + return getXFrom(System.currentTimeMillis() - (HOURS_IN_VIEWPORT_MILLIS / 2)); + } + + private void resetBoundaries() { + mMillisPerPixel = calculateMillisPerPixel(); + mTimeOffset = calculatedBaseLine(); + mTimeLowerBoundary = getTimeFrom(0); + mTimeUpperBoundary = getTimeFrom(getWidth()); + } + + private Rect calculateChannelsHitArea() { + mMeasuringRect.top = mTimeBarHeight; + int visibleChannelsHeight = epgData.getChannelCount() * (mChannelLayoutHeight + mChannelLayoutMargin); + mMeasuringRect.bottom = visibleChannelsHeight < getHeight() ? visibleChannelsHeight : getHeight(); + mMeasuringRect.left = 0; + mMeasuringRect.right = mChannelLayoutWidth; + return mMeasuringRect; + } + + private Rect calculateProgramsHitArea() { + mMeasuringRect.top = mTimeBarHeight; + int visibleChannelsHeight = epgData.getChannelCount() * (mChannelLayoutHeight + mChannelLayoutMargin); + mMeasuringRect.bottom = visibleChannelsHeight < getHeight() ? visibleChannelsHeight : getHeight(); + mMeasuringRect.left = mChannelLayoutWidth; + mMeasuringRect.right = getWidth(); + return mMeasuringRect; + } + + private Rect calculateResetButtonHitArea() { + mMeasuringRect.left = getScrollX() + getWidth() - mResetButtonSize - mResetButtonMargin; + mMeasuringRect.top = getScrollY() + getHeight() - mResetButtonSize - mResetButtonMargin; + mMeasuringRect.right = mMeasuringRect.left + mResetButtonSize; + mMeasuringRect.bottom = mMeasuringRect.top + mResetButtonSize; + return mMeasuringRect; + } + + private int getChannelPosition(int y) { + y -= mTimeBarHeight; + int channelPosition = (y + mChannelLayoutMargin) + / (mChannelLayoutHeight + mChannelLayoutMargin); + + return epgData.getChannelCount() == 0 ? -1 : channelPosition; + } + + private int getProgramPosition(int channelPosition, long time) { + List events = epgData.getEvents(channelPosition); + + if (events != null) { + + for (int eventPos = 0; eventPos < events.size(); eventPos++) { + EPGEvent event = events.get(eventPos); + + if (event.getStart() <= time && event.getEnd() >= time) { + return eventPos; + } + } + } + return -1; + } + + /** + * Add click listener to the EPG. + * @param epgClickListener to add. + */ + public void setEPGClickListener(EPGClickListener epgClickListener) { + mClickListener = epgClickListener; + } + + /** + * Add data to EPG. This must be set for EPG to able to draw something. + * @param epgData pass in any implementation of EPGData. + */ + public void setEPGData(EPGData epgData) { + this.epgData = epgData; + } + + /** + * This will recalculate boundaries, maximal scroll and scroll to start position which is current time. + * To be used on device rotation etc since the device height and width will change. + * @param withAnimation true if scroll to current position should be animated. + */ + public void recalculateAndRedraw(boolean withAnimation) { + if (epgData != null && epgData.hasData()) { + resetBoundaries(); + + calculateMaxVerticalScroll(); + calculateMaxHorizontalScroll(); + + mScroller.startScroll(getScrollX(), getScrollY(), + getXPositionStart() - getScrollX(), + 0, withAnimation ? 600 : 0); + + redraw(); + } + } + + /** + * Does a invalidate() and requestLayout() which causes a redraw of screen. + */ + public void redraw() { + invalidate(); + requestLayout(); + } + + /** + * Clears the local image cache for channel images. Can be used when leaving epg and you want to + * free some memory. Images will be fetched again when loading EPG next time. + */ + public void clearEPGImageCache() { + mChannelImageCache.clear(); + } + + + private class OnGestureListener extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onSingleTapUp(MotionEvent e) { + + // This is absolute coordinate on screen not taking scroll into account. + int x = (int) e.getX(); + int y = (int) e.getY(); + + // Adding scroll to clicked coordinate + int scrollX = getScrollX() + x; + int scrollY = getScrollY() + y; + + int channelPosition = getChannelPosition(scrollY); + if (channelPosition != -1 && mClickListener != null) { + if (calculateResetButtonHitArea().contains(scrollX,scrollY)) { + // Reset button clicked + mClickListener.onResetButtonClicked(); + } else if (calculateChannelsHitArea().contains(x, y)) { + // Channel area is clicked + mClickListener.onChannelClicked(channelPosition, epgData.getChannel(channelPosition)); + } else if (calculateProgramsHitArea().contains(x, y)) { + // Event area is clicked + int programPosition = getProgramPosition(channelPosition, getTimeFrom(getScrollX() + x - calculateProgramsHitArea().left)); + if (programPosition != -1) { + channel = channelPosition; + playtime = epgData.getEvent(channel,programPosition).getStart(); + redraw(); + mClickListener.onEventClicked(channelPosition, programPosition, epgData.getEvent(channelPosition, programPosition)); + } + } + } + + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + int dx = (int) distanceX; + int dy = (int) distanceY; + int x = getScrollX(); + int y = getScrollY(); + + + // Avoid over scrolling + if (x + dx < 0) { + dx = 0 - x; + } + if (y + dy < 0) { + dy = 0 - y; + } + if (x + dx > mMaxHorizontalScroll) { + dx = mMaxHorizontalScroll - x; + } + if (y + dy > mMaxVerticalScroll) { + dy = mMaxVerticalScroll - y; + } + + scrollBy(dx, dy); + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, + float vX, float vY) { + + mScroller.fling(getScrollX(), getScrollY(), -(int) vX, + -(int) vY, 0, mMaxHorizontalScroll, 0, mMaxVerticalScroll); + + redraw(); + return true; + } + + @Override + public boolean onDown(MotionEvent e) { + if (!mScroller.isFinished()) { + mScroller.forceFinished(true); + return true; + } + return true; + } + } +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPGClickListener.java b/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPGClickListener.java new file mode 100644 index 0000000..219f693 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPGClickListener.java @@ -0,0 +1,16 @@ +package com.example.exoplayertv.tvepg.epg; + +import com.example.exoplayertv.tvepg.epg.domain.EPGChannel; +import com.example.exoplayertv.tvepg.epg.domain.EPGEvent; + +/** + * Created by Kristoffer on 15-05-25. + */ +public interface EPGClickListener { + + void onChannelClicked(int channelPosition, EPGChannel epgChannel); + + void onEventClicked(int channelPosition, int programPosition, EPGEvent epgEvent); + + void onResetButtonClicked(); +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPGData.java b/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPGData.java new file mode 100644 index 0000000..507cd84 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/epg/EPGData.java @@ -0,0 +1,24 @@ +package com.example.exoplayertv.tvepg.epg; + +import java.util.List; + +import com.example.exoplayertv.tvepg.epg.domain.EPGChannel; +import com.example.exoplayertv.tvepg.epg.domain.EPGEvent; + +/** + * Interface to implement and pass to EPG containing data to be used. + * Implementation can be a simple as simple as a Map/List or maybe an Adapter. + * Created by Kristoffer on 15-05-23. + */ +public interface EPGData { + + EPGChannel getChannel(int position); + + List getEvents(int channelPosition); + + EPGEvent getEvent(int channelPosition, int programPosition); + + int getChannelCount(); + + boolean hasData(); +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/epg/domain/EPGChannel.java b/app/src/main/java/com/example/exoplayertv/tvepg/epg/domain/EPGChannel.java new file mode 100644 index 0000000..0e6b841 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/epg/domain/EPGChannel.java @@ -0,0 +1,47 @@ +package com.example.exoplayertv.tvepg.epg.domain; + +/** + * Created by Kristoffer. + */ +public class EPGChannel { + + private final String channelID; + private final String name; + private final String imageURL; + private final String streamURL; + private final String drm_scheme; + private final String drm_uri; + + public EPGChannel(String imageURL, String name, String channelID, String streamURL, String drm_scheme, String drm_uri) { + this.imageURL = imageURL; + this.name = name; + this.channelID = channelID; + this.streamURL = streamURL; + this.drm_scheme = drm_scheme; + this.drm_uri = drm_uri; + } + + public String getChannelID() { + return channelID; + } + + public String getName() { + return name; + } + + public String getImageURL() { + return imageURL; + } + + public String getStreamURL() { + return streamURL; + } + + public String getDrmScheme() { + return drm_scheme; + } + + public String getDrmUri() { + return drm_uri; + } +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/epg/domain/EPGEvent.java b/app/src/main/java/com/example/exoplayertv/tvepg/epg/domain/EPGEvent.java new file mode 100644 index 0000000..9690f5a --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/epg/domain/EPGEvent.java @@ -0,0 +1,90 @@ +package com.example.exoplayertv.tvepg.epg.domain; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * Created by Kristoffer. + */ +public class EPGEvent { + + private final static DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss Z"); + private final String start; + private final String end; + private final String title; + private final List categories; + private final String country; + private final String date; + private final String description; + private final String director; + private final String writer; + private final List actors; + + public EPGEvent(String start, String end, String title, + List categories, String country, String date, + String description, String director, String writer, List actors) { + this.start = start; + this.end = end; + this.title = title; + this.categories = categories; + this.country = country; + this.date = date; + this.description = description; + this.director = director; + this.writer = writer; + this.actors = actors; + } + + public String getStartAsString() { return start; } + public long getStart() { + try { + return dateFormat.parse(start).getTime(); + }catch(ParseException e){return 0;} + } + + public String getEndAsString() { return end; } + public long getEnd() { + try { + return dateFormat.parse(end).getTime(); + }catch(ParseException e){return 0;} + } + + public String getTitle() { + return title; + } + + public List getCategories() { + return categories; + } + + public String getCountry() { + return country; + } + + public String getDate() { + return date; + } + + public String getDescription() { + return description; + } + + public String getDirector() { + return director; + } + + public String getWriter() { + return writer; + } + + public List getActors() { + return actors; + } + + public boolean isCurrent() { + long now = System.currentTimeMillis(); + return now >= getStart() && now <= getEnd(); + } +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/epg/misc/EPGUtil.java b/app/src/main/java/com/example/exoplayertv/tvepg/epg/misc/EPGUtil.java new file mode 100644 index 0000000..747c693 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/epg/misc/EPGUtil.java @@ -0,0 +1,56 @@ +package com.example.exoplayertv.tvepg.epg.misc; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import okhttp3.OkHttpClient; +import com.squareup.picasso.OkHttp3Downloader; +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Target; + +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +/** + * Created by Kristoffer. + */ +public class EPGUtil { + private static final String TAG = "EPGUtil"; + private static final DateTimeFormatter dtfShortTime = DateTimeFormat.forPattern("HH:mm"); + private static Picasso picasso = null; + + public static String getShortTime(long timeMillis) { + return dtfShortTime.print(timeMillis); + } + + public static String getWeekdayName(long dateMillis) { + LocalDate date = new LocalDate(dateMillis); + return date.dayOfWeek().getAsText(); + } + + public static void loadImageInto(Context context, String url, int width, int height, Target target) { + initPicasso(context); + + picasso.load(url) + .resize(width, height) + .centerInside() + .into(target); + } + + private static void initPicasso(Context context) { + if (picasso == null) { + picasso = new Picasso.Builder(context) + .downloader(new OkHttp3Downloader(new OkHttpClient())) + .listener(new Picasso.Listener() { + @Override + public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) { + Log.e(TAG, exception.getMessage()); + } + }) + .build(); + } + } + +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/xmltv/ContentTask.java b/app/src/main/java/com/example/exoplayertv/tvepg/xmltv/ContentTask.java new file mode 100644 index 0000000..9199009 --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/xmltv/ContentTask.java @@ -0,0 +1,63 @@ +package com.example.exoplayertv.tvepg.xmltv; + +import com.example.exoplayertv.tvepg.xmltv.model.Channel; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import android.os.AsyncTask; +import android.util.Log; + +import com.example.exoplayertv.tvepg.xmltv.grabber.Grabber; + +/** + * + * @author Daniel Oskarsson (daniel.oskarsson@gmail.com) + */ +public class ContentTask extends AsyncTask> { + + public interface TvGuide { + public void setTvGuide(List channels); + }; + + private Map> channels = new HashMap>(); + + private TvGuide tvGuide; + public ContentTask(TvGuide tvGuide) { + this.tvGuide = tvGuide; // Get reference to UI thread, so we don't have to push this code into TVActivity + } + + @Override + protected void onPostExecute(List channels) { + tvGuide.setTvGuide(channels); + } + + @Override + protected List doInBackground(String[]... args) { + List whitelist = Arrays.asList(args); // The interface to doInBackground want's varargs... + DateFormat timeFormat = new SimpleDateFormat("HH:mm"); + timeFormat.setTimeZone(TimeZone.getTimeZone("GMT+1")); + + // Fetch filtered channels from Map or from Xmltv + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String today = dateFormat.format(Calendar.getInstance().getTime()); + List lChannels = channels.get(today); + if (lChannels == null) { + try { + lChannels = new Grabber(whitelist).getChannels(); + channels.put(today, lChannels); + } catch (IOException e) { + Log.e(ContentTask.class.getCanonicalName(), e.getMessage()); + return null; + } + } + + return lChannels; + } +} diff --git a/app/src/main/java/com/example/exoplayertv/tvepg/xmltv/WebAssets.java b/app/src/main/java/com/example/exoplayertv/tvepg/xmltv/WebAssets.java new file mode 100644 index 0000000..ca20e5c --- /dev/null +++ b/app/src/main/java/com/example/exoplayertv/tvepg/xmltv/WebAssets.java @@ -0,0 +1,61 @@ +package com.example.exoplayertv.tvepg.xmltv; + +import java.io.IOException; +import java.io.InputStream; + +import android.content.res.AssetManager; +import android.util.Log; + +/** + * + * @author Daniel Oskarsson (daniel.oskarsson@gmail.com) + */ +public /*final*/ class WebAssets { + private AssetManager assetManager; + + public WebAssets(AssetManager assetManager) { + this.assetManager = assetManager; + } + + /** + * This method returns a html template that contains the magic string %html%. + * The purpose is to replace %html% with generated html + * while still being able to edit the 'frame' around that data as in an asset. + * @return A string representation of the template asset. + */ + public String getTemplate() { + try { + InputStream inputStream = assetManager.open("template.html"); + byte[] buffer = new byte[inputStream.available()]; + inputStream.read(buffer); + inputStream.close(); + return new String(buffer).replace("", getStyleSheet()); + } catch (IOException e) { + Log.e("TVActivity.class.getCanonicalName()", "Could not read asset template.html"); + } + return "%html%"; + } + + /** + * This method is used by {@link #getTemplate()} to dynamically replace the link to the asset style sheet. + * The reason for doing that is that WebView does not seem to parse asset references. + * This way the same style sheet can be used by both the template and the about asset. + * @return A string representation of the style sheet asset. + */ + public String getStyleSheet() { + try { + InputStream inputStream = assetManager.open("stylesheet.css"); + byte[] buffer = new byte[inputStream.available()]; + inputStream.read(buffer); + inputStream.close(); + StringBuilder stylesheet = new StringBuilder(); + stylesheet.append(""); + return stylesheet.toString(); + } catch (IOException e) { + Log.e("TVActivity.class.getCanonicalName()", "Could not read asset stylesheet.css"); + } + return ""); + return stylesheet.toString(); + } catch (IOException e) { + Log.e("TVActivity.class.getCanonicalName()", "Could not read asset stylesheet.css"); + } + return " + + + + + + + diff --git a/app/src/test/java/com/example/exoplayertv/ExampleUnitTest.java b/app/src/test/java/com/example/exoplayertv/ExampleUnitTest.java new file mode 100644 index 0000000..4f232ef --- /dev/null +++ b/app/src/test/java/com/example/exoplayertv/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.exoplayertv; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..565f8c2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { +alias(libs.plugins.android.application) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4387edc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..8798cc8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +agp = "8.13.2" +junit = "4.13.2" +junitVersion = "1.1.5" +espressoCore = "3.5.1" +appcompat = "1.6.1" +material = "1.10.0" +constraintlayout = "2.1.4" +navigationFragment = "2.6.0" +navigationUi = "2.6.0" + +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" } +navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afe2cfc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Sep 28 18:30:44 CEST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ba72dd7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "ExoPlayerTV" +include ':app'