Initial Commit!

This commit is contained in:
2026-08-08 22:34:33 +02:00
commit a08c09829d
100 changed files with 7031 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -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

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

10
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

0
app/ Normal file
View File

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

87
app/build.gradle Normal file
View File

@@ -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
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -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

Binary file not shown.

Binary file not shown.

View File

@@ -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
}

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.exoplayertv">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk/>
<application
android:label="@string/application_name"
android:icon="@mipmap/ic_launcher"
android:banner="@drawable/ic_banner"
android:largeHeap="true"
android:allowBackup="false"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:name="androidx.multidex.MultiDexApplication"
android:usesCleartextTraffic="true"
tools:targetApi="29">
<activity android:name=".SampleChooserActivity"
android:configChanges="keyboardHidden"
android:label="@string/application_name"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.exolist\\.json"/>
</intent-filter>
</activity>
<activity android:name=".PlayerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:label="@string/application_name"
android:theme="@style/PlayerTheme"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.exoplayer.demo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
<data android:scheme="ssai"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.demo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.demo.action.VIEW_LIST"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<service android:name=".DemoDownloadService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<receiver
android:name=".StartActivityOnBootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -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<Download> 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.
*
* <p>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);
}
}
}

View File

@@ -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).
*
* <p>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() {}
}

View File

@@ -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<Listener> listeners;
@UnstableApi
private final HashMap<Uri, Download> 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<Void, Void, Void> {
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));
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<MediaItem> createMediaItemsFromIntent(Intent intent) {
List<MediaItem> 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<MediaItem> 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<String, String> 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<String, String> 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);
}
}
}

View File

@@ -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<MediaItem> 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<MediaItem> 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<MediaItem> 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<PlaybackException> {
@Override
public Pair<Integer, String> 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<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) {
List<MediaItem> 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();
}
}

View File

@@ -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<PlaylistHolder> 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<PlaylistGroup> 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<String, Void, List<PlaylistGroup>> {
private boolean sawError;
@Override
protected List<PlaylistGroup> doInBackground(String... uris) {
List<PlaylistGroup> 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<PlaylistGroup> result) {
playlists = result.get(0).playlists;
onPlaylistGroups(result, sawError);
}
private void readPlaylistGroups(JsonReader reader, List<PlaylistGroup> groups)
throws IOException {
reader.beginArray();
while (reader.hasNext()) {
readPlaylistGroup(reader, groups);
}
reader.endArray();
}
private void readPlaylistGroup(JsonReader reader, List<PlaylistGroup> groups)
throws IOException {
String groupName = "";
ArrayList<PlaylistHolder> 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<PlaylistHolder> children = null;
Uri subtitleUri = null;
String subtitleMimeType = null;
String subtitleLanguage = null;
UUID drmUuid = null;
String drmLicenseUri = null;
ImmutableMap<String, String> 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<String, String> 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<MediaItem> 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<PlaylistGroup> 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<MediaItem> mediaItems;
private PlaylistHolder(String title, List<MediaItem> 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<PlaylistHolder> 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<Channel> 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<EPGEvent> getEvents(int channelPosition) {
List<Program> programs = channels.get(channelPosition).getPrograms();
List<EPGEvent> 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<Void, Void, List<String[]>> {
@Override
protected List<String[]> doInBackground(Void... params) {
List<String[]> 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<String[]> 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<String, Void, String> {
@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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Integer> SUPPORTED_TRACK_TYPES =
ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT);
private final SparseArray<TrackSelectionViewFragment> tabFragments;
private final ArrayList<Integer> 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<TrackGroup, TrackSelectionOverride> 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<Tracks.Group> 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<TrackGroup, TrackSelectionOverride> 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<Tracks.Group> trackGroups;
private boolean allowAdaptiveSelections;
private boolean allowMultipleOverrides;
/* package */ boolean isDisabled;
/* package */ Map<TrackGroup, TrackSelectionOverride> overrides;
public TrackSelectionViewFragment() {
// Retain instance across activity re-creation to prevent losing access to init data.
setRetainInstance(true);
}
public void init(
List<Tracks.Group> trackGroups,
boolean isDisabled,
Map<TrackGroup, TrackSelectionOverride> 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<TrackGroup, TrackSelectionOverride> overrides) {
this.isDisabled = isDisabled;
this.overrides = overrides;
}
}
}

View File

@@ -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;

View File

@@ -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<String, Bitmap> mChannelImageCache;
private final Map<String, Target> 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<EPGEvent> 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<EPGEvent> 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() && playtime<event.getEnd())
mPaint.setColor(event.isCurrent() ? mEventLayoutBackgroundCurrentSelected : mEventLayoutBackgroundSelected);
else
mPaint.setColor(event.isCurrent() ? mEventLayoutBackgroundCurrent : mEventLayoutBackground);
canvas.drawRect(drawingRect, mPaint);
// Add left and right inner padding
if (drawingRect.left<getScrollX()+mChannelLayoutWidth)
drawingRect.left = getScrollX()+mChannelLayoutWidth+mChannelLayoutPadding;
else
drawingRect.left += mChannelLayoutPadding;
drawingRect.right -= mChannelLayoutPadding;
// Text
mPaint.setColor(mEventLayoutTextColor);
mPaint.setTextSize(mEventLayoutTextSize);
// Move drawing.top so text will be centered (text is drawn bottom>up)
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<EPGEvent> 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;
}
}
}

View File

@@ -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();
}

View File

@@ -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<EPGEvent> getEvents(int channelPosition);
EPGEvent getEvent(int channelPosition, int programPosition);
int getChannelCount();
boolean hasData();
}

View File

@@ -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;
}
}

View File

@@ -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<String> categories;
private final String country;
private final String date;
private final String description;
private final String director;
private final String writer;
private final List<String> actors;
public EPGEvent(String start, String end, String title,
List<String> categories, String country, String date,
String description, String director, String writer, List<String> 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<String> 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<String> getActors() {
return actors;
}
public boolean isCurrent() {
long now = System.currentTimeMillis();
return now >= getStart() && now <= getEnd();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<String[], Void, List<Channel>> {
public interface TvGuide {
public void setTvGuide(List<Channel> channels);
};
private Map<String, List<Channel>> channels = new HashMap<String, List<Channel>>();
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<Channel> channels) {
tvGuide.setTvGuide(channels);
}
@Override
protected List<Channel> doInBackground(String[]... args) {
List<String[]> 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<Channel> 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;
}
}

View File

@@ -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("<link rel='stylesheet' type='text/css' href='file:///android_asset/stylesheet.css' />", getStyleSheet());
} catch (IOException e) {
Log.e("TVActivity.class.getCanonicalName()", "Could not read asset template.html");
}
return "<html>%html%</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("<style type='text/css'>");
stylesheet.append(new String(buffer));
stylesheet.append("</style>");
return stylesheet.toString();
} catch (IOException e) {
Log.e("TVActivity.class.getCanonicalName()", "Could not read asset stylesheet.css");
}
return "<style type='text/css'><style>";
}
}

View File

@@ -0,0 +1,73 @@
package com.example.exoplayertv.tvepg.xmltv.custom;
import java.util.ArrayList;
import java.util.List;
import com.example.exoplayertv.tvepg.xmltv.model.Program;
/**
* The purpose of this class is to calculate and generate content according to:
*
* A program is a first-class citizen.
* Programs are NOT grouped by channel.
* Programs are ordered according to their start time.
* Programs are ordered according to the order of the selected channels.
* Programs do not have an end time. They have a length.
* Programs have attributes such as: length and channel.
*
* An example:
*
* 19:55
* 19:00 The Real Housewvies of Atlanta (60 min) (tv3)
* 19:30 The Bing Bang Theory (30 min) (kanal5)
* 19:30 2 1/2 m<>n (30 min) (tv6)
* 19:30 Rapport (30 min) (svt1)
* 19:30 Miffo-tv (30 min) (svt2)
* 19:30 Halv <20>tta hos mig (30 min) (tv4)
* 20:00 Tunnelbanan (60 min) (kanal5)
* 20:00 Efterlyst (60 min) (tv3)
* 20:00 Chuck (60 min) (tv6)
* 20:00 Uppdrag granskning (60 min) (svt1)
* 20:00 Den njutbara tr<74>dg<64>rden (30 min) (svt2)
* 20:00 Sveriges m<>sterkock (60 min) (tv4)
*
* The selected channels are (in order): kanal5, tv3, tv6, svt1, svt2, tv4 in the example above.
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*
*/
public class CustomTvGuide {
private List<CustomTvGuideRow> rows = new ArrayList<CustomTvGuideRow>();
public void addProgram(Program program, String channel) {
rows.add(new CustomTvGuideRow(program.getName(), program.getStart(), program.getStop(), channel));
}
public final List<CustomTvGuideRow> asRow() {
return rows;
}
public String asHtml() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("<table>");
stringBuilder.append("<tbody>");
CustomTvGuideRow[] array = rows.toArray(new CustomTvGuideRow[0]);
// Arrays.sort(array, new CustomTvGuideRowComparator());
for (CustomTvGuideRow row : array) {
String start = row.getStart();
String name = row.getName();
String length = row.getLength();
String channel = row.getChannelShort();
stringBuilder.append(String.format("<tr><td>%s</td><td>%s</td><td>(%s min)</td><td>(%s)</td></tr>", start, name, length, channel));
}
stringBuilder.append("</tbody>");
stringBuilder.append("</table>");
return stringBuilder.toString();
}
}

View File

@@ -0,0 +1,63 @@
package com.example.exoplayertv.tvepg.xmltv.custom;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CustomTvGuideRow {
private String name;
private Date start;
private Date stop;
private String channel;
public CustomTvGuideRow(String name, Date start, Date stop, String channel) {
super();
this.name = name;
this.start = start;
this.stop = stop;
this.channel = channel;
}
public Date getStartDate() {
return start;
}
public boolean isPlayingNow() {
Date d = new Date();
if (/*start!=null && stop!=null &&*/ start.getTime() <= d.getTime() && d.getTime() <= stop.getTime())
return true;
else
return false;
}
public long getLengthNumber() {
long lengthInMilliseconds = stop.getTime() - start.getTime();
return lengthInMilliseconds / 1000 / 60;
}
public long getProgressNumber() {
Date d = new Date();
long lengthInMilliseconds = d.getTime() - start.getTime();
return lengthInMilliseconds / 1000 / 60;
}
public String getStart() { return getStart("yyyy-MM-dd HH:mm"); }
public String getStart(String format) {
return new SimpleDateFormat(format).format(start);
}
public String getStop() { return getStart("yyyy-MM-dd HH:mm"); }
public String getStop(String format) {
return new SimpleDateFormat(format).format(stop);
}
public String getName() {
return name;
}
public String getLength() {
return String.valueOf(getLengthNumber());
}
public String getChannel() {
return channel;
}
public String getChannelShort() {
int index = channel.indexOf(".");
if (index >= 1) return (channel.substring(0, index));
return channel;
}
}

View File

@@ -0,0 +1,26 @@
package com.example.exoplayertv.tvepg.xmltv.custom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class CustomTvGuideRowComparator implements Comparator<CustomTvGuideRow> {
private static List<String> list = new ArrayList<String>();
public static void setChannels(String[] channels) {
list = Arrays.asList(channels);
}
public int compare(CustomTvGuideRow lhs, CustomTvGuideRow rhs) {
if (lhs.getStartDate().before(rhs.getStartDate())) return -1;
else if (lhs.getStartDate().after(rhs.getStartDate())) return 1;
if (list.indexOf(lhs.getChannel()) < list.indexOf(rhs.getChannel())) return -1;
if (list.indexOf(lhs.getChannel()) > list.indexOf(rhs.getChannel())) return 1;
return 0;
}
}

View File

@@ -0,0 +1,46 @@
package com.example.exoplayertv.tvepg.xmltv.grabber;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import android.util.Log;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
abstract class AbstractXpathFactory {
protected Document document;
protected XPath xpath;
public AbstractXpathFactory(String xml) throws IOException {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
InputStream xmlStream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
// InputStream xmlStream = new ByteArrayInputStream(xml.getBytes("ISO-8859-1"));
document = builder.parse(xmlStream);
XPathFactory factory = XPathFactory.newInstance();
xpath = factory.newXPath();
} catch (ParserConfigurationException ex) {
Log.e(AbstractXpathFactory.class.getCanonicalName(), ex.getMessage());
} catch (SAXException ex) {
Log.e(AbstractXpathFactory.class.getCanonicalName(), ex.getMessage());
}
}
}

View File

@@ -0,0 +1,52 @@
package com.example.exoplayertv.tvepg.xmltv.grabber;
import com.example.exoplayertv.tvepg.xmltv.model.Channel;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.util.Log;
import com.example.exoplayertv.tvepg.xmltv.model.Program;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
class ChannelsFactory extends AbstractXpathFactory {
private List<Channel> channels = new ArrayList<Channel>();
public ChannelsFactory(String xmlString, List<String[]> whitelist) throws IOException {
super(xmlString);
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String today = dateFormat.format(Calendar.getInstance().getTime());
Map<String, Channel> map = new HashMap<String, Channel>();
for (String[] channel: whitelist) {
map.put(channel[0], new Channel(channel[0], channel[0], channel[1], channel[2], channel[3], channel[4]));
}
// Only fetch programs for white listed channels, and fetch in the order of the whitelist
int j = 1;
for (String[] id : whitelist) {
Channel channel = map.get(id[0]);
// Channel channel = map.get(names.item(j-1).getNodeValue());
Log.d("downloading...", String.format("%d/%d", j++, whitelist.size()));
if (channel!=null) {
List<Program> programs = Grabber.grabPrograms(channel.getId(), today);
channel.addPrograms(Calendar.getInstance().getTime(), programs);
channels.add(channel);
}
}
}
public List<Channel> getChannels() {
return channels;
}
}

View File

@@ -0,0 +1,31 @@
package com.example.exoplayertv.tvepg.xmltv.grabber;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.Charset;
//import java.util.zip.GZIPInputStream;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
class Curl {
public static String getFrom(String string) throws IOException {
try {
URL url = new URL(string);
InputStream inputStream = url.openStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String xml = "", inputLine;
while ((inputLine = bufferedReader.readLine()) != null) {
xml += inputLine;
}
bufferedReader.close();
return xml;
} catch (IOException exception) {
}
return null;
}
}

View File

@@ -0,0 +1,42 @@
package com.example.exoplayertv.tvepg.xmltv.grabber;
import com.example.exoplayertv.tvepg.xmltv.model.Channel;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.example.exoplayertv.tvepg.xmltv.model.Program;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class Grabber {
private static final String URL_CHANNELS = "https://www.ask-meyer.ch/IPTV/Channels.xml";
// private static final String URL_CHANNELS = "http://tv.swedb.se/xmltv/channels.xml.gz";
private static final String URL_TEMPLATE = "https://www.ask-meyer.ch/IPTV/EPG/%s.xml";
// private static final String URL_TEMPLATE = "http://xmltv.tvsajten.com/xmltv/%s_%s.xml.gz";
private List<Channel> channels = new ArrayList<Channel>();
public Grabber(List<String[]> whitelist) throws IOException {
String channelsXml = Curl.getFrom(URL_CHANNELS);
ChannelsFactory channelsFactory = new ChannelsFactory(channelsXml, whitelist);
channels = channelsFactory.getChannels();
}
static List<Program> grabPrograms(String channelId, String date) throws IOException {
String url = String.format(URL_TEMPLATE, channelId, date);
String programsXml = Curl.getFrom(url);
programsXml = programsXml.replace("<!DOCTYPE tv SYSTEM \"xmltv.dtd\">", "");
ProgramsFactory programsFactory = new ProgramsFactory(programsXml);
List<Program> programs = programsFactory.getPrograms();
return programs;
}
public List<Channel> getChannels() {
return channels;
}
}

View File

@@ -0,0 +1,112 @@
package com.example.exoplayertv.tvepg.xmltv.grabber;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.NodeList;
import android.util.Log;
import com.example.exoplayertv.tvepg.xmltv.model.Program;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
class ProgramsFactory extends AbstractXpathFactory {
private List<Program> programs = new ArrayList<Program>();
public ProgramsFactory(String xml) throws IOException {
super(xml);
try {
XPathExpression expression = xpath.compile("/tv/programme/@start");
NodeList starts = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
expression = xpath.compile("/tv/programme/@stop");
NodeList stops = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
expression = xpath.compile("tv/programme/title/text()");
NodeList names = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
expression = xpath.compile("tv/programme");
NodeList programmes = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < names.getLength(); i++) {
String name = names.item(i).getNodeValue();
String start = starts.item(i).getNodeValue();
String stop = stops.item(i).getNodeValue();
NodeList children = programmes.item(i).getChildNodes();
List<String> categories = null;
String country = null;
String date = null;
String description = null;
String director = null;
String writer = null;
List<String> actors = null;
for (int j = 0; j < children.getLength(); j++) {
try { // catch Null values
String local = children.item(j).getLocalName();
switch (local) {
case "category":
if (categories == null)
categories = new ArrayList<>();
categories.add(children.item(j).getFirstChild().getNodeValue());
break;
case "country":
country = children.item(j).getFirstChild().getNodeValue();
break;
case "date":
date = children.item(j).getFirstChild().getNodeValue();
break;
case "desc":
description = children.item(j).getFirstChild().getNodeValue();
break;
case "credits":
NodeList credits = children.item(j).getChildNodes();
for (int k = 0; k < credits.getLength(); k++) {
switch (credits.item(k).getLocalName()) {
case "director":
director = credits.item(k).getFirstChild().getNodeValue();
break;
case "writer":
writer = credits.item(k).getFirstChild().getNodeValue();
break;
case "actor":
if (actors == null)
actors = new ArrayList<>();
actors.add(credits.item(k).getFirstChild().getNodeValue());
break;
}
}
break;
}
}catch (Exception e) {
Log.e("EPG", "unkown EPG-TAG:"+children.item(j).getNodeValue());
}
}
Program program;
try {
program = new Program(name, start, stop, categories, country, date, description, director, writer, actors);
programs.add(program);
} catch (ParseException ex) {
Log.e(ProgramsFactory.class.getCanonicalName(), ex.getMessage());
}
}
} catch (XPathExpressionException ex) {
Log.e(ProgramsFactory.class.getCanonicalName(), ex.getMessage());
}
}
public List<Program> getPrograms() {
return programs;
}
}

View File

@@ -0,0 +1,125 @@
package com.example.exoplayertv.tvepg.xmltv.model;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class Channel {
public Channel() {}
private String id;
private String name;
private String stream;
private String drm_scheme;
private String drm_uri;
private String logo;
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
private Map<String, List<Program>> allPrograms = new HashMap<String, List<Program>>();
public Channel(String id, String name, String logo, String stream, String drm_scheme, String drm_uri) {
this.id = id;
this.name = name;
this.logo = logo;
this.stream = stream;
this.drm_scheme = drm_scheme;
this.drm_uri = drm_uri;
}
/* public String getLogo() {
return String.format(logo, id);
}*/
public String getLogo() {
return logo;
}
public String getName() {
return name;
}
public String getStream() {
return stream;
}
public String getDrmScheme() {
return drm_scheme;
}
public String getDrmUri() {
return drm_uri;
}
public String getId() {
return id;
}
public List<Program> getPrograms() {
return getPrograms(Calendar.getInstance().getTime(), DisplayMode.FUTURE);
}
public List<Program> getPrograms(Date date) {
return getPrograms(date, DisplayMode.FUTURE);
}
public List<Program> getPrograms(Date date, DisplayMode programMode) {
String string = dateFormat.format(date);
List<Program> datePrograms = allPrograms.get(string);
if (datePrograms == null) datePrograms = new ArrayList<Program>();
if (DisplayMode.ALL.equals(programMode)) return datePrograms;
List<Program> programModePrograms = new ArrayList<Program>();
for (Program program : datePrograms) {
if (program.getStop().after(date)) {
if (DisplayMode.CURRENT.equals(programMode)) {
if (program.getStart().before(date)) {
programModePrograms.add(program);
}
} else {
programModePrograms.add(program);
}
}
}
return programModePrograms;
}
public void addPrograms(Date date, List<Program> programs) {
String string = dateFormat.format(date);
allPrograms.put(string, programs);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Channel other = (Channel) obj;
if ((this.id == null) ? (other.id != null) : !this.id.equals(other.id)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
return hash;
}
@Override
public String toString() {
return "Channel{" + "id=" + id + ", name=" + name + ", logo=" + getLogo() + '}';
}
}

View File

@@ -0,0 +1,10 @@
package com.example.exoplayertv.tvepg.xmltv.model;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*
*/
public enum DisplayMode {
ALL, FUTURE, CURRENT
}

View File

@@ -0,0 +1,94 @@
package com.example.exoplayertv.tvepg.xmltv.model;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class Program {
private final static DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss Z");
private String name;
private String start;
private String stop;
private List<String> categories;
private String country;
private String date;
private String description;
private String director;
private String writer;
private List<String> actors;
public Program() {}
public Program(String name, String start, String stop,
List<String> categories, String country, String date,
String description, String director, String writer, List<String> actors) throws ParseException {
this.start = start;
this.stop = stop;
this.name = name;
this.categories = categories;
this.country = country;
this.date = date;
this.description = description;
this.director = director;
this.writer = writer;
this.actors = actors;
}
public String getName() {
return name;
}
public String getStartAsString() { return start; }
public Date getStart() {
try {
return dateFormat.parse(start);
}catch(ParseException e){return null;}
}
public String getStopAsString() { return stop; }
public Date getStop() {
try {
return dateFormat.parse(stop);
}catch(ParseException e){return null;}
}
public List<String> 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<String> getActors() {
return actors;
}
@Override
public String toString() {
return "Program{" + "name=" + name + ", start=" + start + ", stop=" + stop + '}';
}
}

View File

@@ -0,0 +1,70 @@
package com.example.exoplayertv.xmltv;
import com.example.exoplayertv.xmltv.custom.CustomTvGuide;
import com.example.exoplayertv.xmltv.custom.CustomTvGuideRow;
import com.example.exoplayertv.xmltv.model.Channel;
import com.example.exoplayertv.xmltv.model.Program;
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.xmltv.grabber.Grabber;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class ContentTask extends AsyncTask<String, Void, List<CustomTvGuideRow>> {
private Map<String, List<Channel>> channels = new HashMap<String, List<Channel>>();
private CustomTvGuide.TvGuide tvGuide;
public ContentTask(CustomTvGuide.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<CustomTvGuideRow> rows) {
tvGuide.setTvGuide(rows);
}
@Override
protected List<CustomTvGuideRow> doInBackground(String... args) {
List<String> 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<Channel> 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;
}
}
// Iterate channels and programs and create custom html representation
CustomTvGuide customTvGuide = new CustomTvGuide();
for (Channel channel : lChannels) {
List<Program> programs = channel.getPrograms();
for (Program program : programs) {
customTvGuide.addProgram(program, channel.getName());
}
}
return customTvGuide.asRow();
}
}

View File

@@ -0,0 +1,61 @@
package com.example.exoplayertv.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("<link rel='stylesheet' type='text/css' href='file:///android_asset/stylesheet.css' />", getStyleSheet());
} catch (IOException e) {
Log.e("TVActivity.class.getCanonicalName()", "Could not read asset template.html");
}
return "<html>%html%</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("<style type='text/css'>");
stylesheet.append(new String(buffer));
stylesheet.append("</style>");
return stylesheet.toString();
} catch (IOException e) {
Log.e("TVActivity.class.getCanonicalName()", "Could not read asset stylesheet.css");
}
return "<style type='text/css'><style>";
}
}

View File

@@ -0,0 +1,78 @@
package com.example.exoplayertv.xmltv.custom;
import java.util.ArrayList;
import java.util.List;
import com.example.exoplayertv.xmltv.model.Program;
/**
* The purpose of this class is to calculate and generate content according to:
*
* A program is a first-class citizen.
* Programs are NOT grouped by channel.
* Programs are ordered according to their start time.
* Programs are ordered according to the order of the selected channels.
* Programs do not have an end time. They have a length.
* Programs have attributes such as: length and channel.
*
* An example:
*
* 19:55
* 19:00 The Real Housewvies of Atlanta (60 min) (tv3)
* 19:30 The Bing Bang Theory (30 min) (kanal5)
* 19:30 2 1/2 m<>n (30 min) (tv6)
* 19:30 Rapport (30 min) (svt1)
* 19:30 Miffo-tv (30 min) (svt2)
* 19:30 Halv <20>tta hos mig (30 min) (tv4)
* 20:00 Tunnelbanan (60 min) (kanal5)
* 20:00 Efterlyst (60 min) (tv3)
* 20:00 Chuck (60 min) (tv6)
* 20:00 Uppdrag granskning (60 min) (svt1)
* 20:00 Den njutbara tr<74>dg<64>rden (30 min) (svt2)
* 20:00 Sveriges m<>sterkock (60 min) (tv4)
*
* The selected channels are (in order): kanal5, tv3, tv6, svt1, svt2, tv4 in the example above.
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*
*/
public class CustomTvGuide {
public interface TvGuide {
public void setTvGuide(List<CustomTvGuideRow> EPG);
};
private List<CustomTvGuideRow> rows = new ArrayList<CustomTvGuideRow>();
public void addProgram(Program program, String channel) {
rows.add(new CustomTvGuideRow(program.getName(), program.getStart(), program.getStop(), channel));
}
public final List<CustomTvGuideRow> asRow() {
return rows;
}
public String asHtml() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("<table>");
stringBuilder.append("<tbody>");
CustomTvGuideRow[] array = rows.toArray(new CustomTvGuideRow[0]);
// Arrays.sort(array, new CustomTvGuideRowComparator());
for (CustomTvGuideRow row : array) {
String start = row.getStart();
String name = row.getName();
String length = row.getLength();
String channel = row.getChannelShort();
stringBuilder.append(String.format("<tr><td>%s</td><td>%s</td><td>(%s min)</td><td>(%s)</td></tr>", start, name, length, channel));
}
stringBuilder.append("</tbody>");
stringBuilder.append("</table>");
return stringBuilder.toString();
}
}

View File

@@ -0,0 +1,63 @@
package com.example.exoplayertv.xmltv.custom;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CustomTvGuideRow {
private String name;
private Date start;
private Date stop;
private String channel;
public CustomTvGuideRow(String name, Date start, Date stop, String channel) {
super();
this.name = name;
this.start = start;
this.stop = stop;
this.channel = channel;
}
public Date getStartDate() {
return start;
}
public boolean isPlayingNow() {
Date d = new Date();
if (/*start!=null && stop!=null &&*/ start.getTime() <= d.getTime() && d.getTime() <= stop.getTime())
return true;
else
return false;
}
public long getLengthNumber() {
long lengthInMilliseconds = stop.getTime() - start.getTime();
return lengthInMilliseconds / 1000 / 60;
}
public long getProgressNumber() {
Date d = new Date();
long lengthInMilliseconds = d.getTime() - start.getTime();
return lengthInMilliseconds / 1000 / 60;
}
public String getStart() { return getStart("yyyy-MM-dd HH:mm"); }
public String getStart(String format) {
return new SimpleDateFormat(format).format(start);
}
public String getStop() { return getStart("yyyy-MM-dd HH:mm"); }
public String getStop(String format) {
return new SimpleDateFormat(format).format(stop);
}
public String getName() {
return name;
}
public String getLength() {
return String.valueOf(getLengthNumber());
}
public String getChannel() {
return channel;
}
public String getChannelShort() {
int index = channel.indexOf(".");
if (index >= 1) return (channel.substring(0, index));
return channel;
}
}

View File

@@ -0,0 +1,26 @@
package com.example.exoplayertv.xmltv.custom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class CustomTvGuideRowComparator implements Comparator<CustomTvGuideRow> {
private static List<String> list = new ArrayList<String>();
public static void setChannels(String[] channels) {
list = Arrays.asList(channels);
}
public int compare(CustomTvGuideRow lhs, CustomTvGuideRow rhs) {
if (lhs.getStartDate().before(rhs.getStartDate())) return -1;
else if (lhs.getStartDate().after(rhs.getStartDate())) return 1;
if (list.indexOf(lhs.getChannel()) < list.indexOf(rhs.getChannel())) return -1;
if (list.indexOf(lhs.getChannel()) > list.indexOf(rhs.getChannel())) return 1;
return 0;
}
}

View File

@@ -0,0 +1,46 @@
package com.example.exoplayertv.xmltv.grabber;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import android.util.Log;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
abstract class AbstractXpathFactory {
protected Document document;
protected XPath xpath;
public AbstractXpathFactory(String xml) throws IOException {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
InputStream xmlStream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
// InputStream xmlStream = new ByteArrayInputStream(xml.getBytes("ISO-8859-1"));
document = builder.parse(xmlStream);
XPathFactory factory = XPathFactory.newInstance();
xpath = factory.newXPath();
} catch (ParserConfigurationException ex) {
Log.e(AbstractXpathFactory.class.getCanonicalName(), ex.getMessage());
} catch (SAXException ex) {
Log.e(AbstractXpathFactory.class.getCanonicalName(), ex.getMessage());
}
}
}

View File

@@ -0,0 +1,70 @@
package com.example.exoplayertv.xmltv.grabber;
import com.example.exoplayertv.xmltv.model.Channel;
import com.example.exoplayertv.xmltv.model.Program;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.NodeList;
import android.util.Log;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
class ChannelsFactory extends AbstractXpathFactory {
private List<Channel> channels = new ArrayList<Channel>();
public ChannelsFactory(String xmlString, List<String> whitelist) throws IOException {
super(xmlString);
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String today = dateFormat.format(Calendar.getInstance().getTime());
try {
XPathExpression expression = xpath.compile("/tv/channel/@id");
NodeList ids = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
expression = xpath.compile("/tv/channel/display-name/text()");
NodeList names = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
Map<String, Channel> map = new HashMap<String, Channel>();
for (int i = 0; i < ids.getLength(); i++) {
String id = ids.item(i).getNodeValue();
String name = names.item(i).getNodeValue();
map.put(name, new Channel(id, name));
// map.put(id, new Channel(id, name));
}
// Only fetch programs for white listed channels, and fetch in the order of the whitelist
int j = 1;
for (String id : whitelist) {
Log.d("downloading...", String.format("%d/%d", j++, whitelist.size()));
Channel channel = map.get(id);
// List<Program> programs = Grabber.grabPrograms(id, today);
if (channel!=null) {
List<Program> programs = Grabber.grabPrograms(channel.getId(), today);
channel.addPrograms(Calendar.getInstance().getTime(), programs);
channels.add(channel);
}
}
} catch (XPathExpressionException ex) {
Log.e(ChannelsFactory.class.getCanonicalName(), ex.getMessage());
}
}
public List<Channel> getChannels() {
return channels;
}
}

View File

@@ -0,0 +1,33 @@
package com.example.exoplayertv.xmltv.grabber;
import android.os.AsyncTask;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.Charset;
//import java.util.zip.GZIPInputStream;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
class Curl {
public static String getFrom(String string) throws IOException {
try {
URL url = new URL(string);
InputStream inputStream = url.openStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String xml = "", inputLine;
while ((inputLine = bufferedReader.readLine()) != null) {
xml += inputLine;
}
bufferedReader.close();
return xml;
} catch (IOException exception) {
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
package com.example.exoplayertv.xmltv.grabber;
import com.example.exoplayertv.xmltv.model.Channel;
import com.example.exoplayertv.xmltv.model.Program;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class Grabber {
private static final String URL_CHANNELS = "https://www.ask-meyer.ch/IPTV/Channels.xml";
// private static final String URL_CHANNELS = "http://tv.swedb.se/xmltv/channels.xml.gz";
private static final String URL_TEMPLATE = "https://www.ask-meyer.ch/IPTV/EPG/%s.xml";
// private static final String URL_TEMPLATE = "http://xmltv.tvsajten.com/xmltv/%s_%s.xml.gz";
private List<Channel> channels = new ArrayList<Channel>();
public Grabber(List<String> whitelist) throws IOException {
String channelsXml = Curl.getFrom(URL_CHANNELS);
ChannelsFactory channelsFactory = new ChannelsFactory(channelsXml, whitelist);
channels = channelsFactory.getChannels();
}
static List<Program> grabPrograms(String channelId, String date) throws IOException {
String url = String.format(URL_TEMPLATE, channelId, date);
String programsXml = Curl.getFrom(url);
programsXml = programsXml.replace("<!DOCTYPE tv SYSTEM \"xmltv.dtd\">", "");
ProgramsFactory programsFactory = new ProgramsFactory(programsXml);
List<Program> programs = programsFactory.getPrograms();
return programs;
}
public List<Channel> getChannels() {
return channels;
}
}

View File

@@ -0,0 +1,58 @@
package com.example.exoplayertv.xmltv.grabber;
import com.example.exoplayertv.xmltv.model.Program;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.NodeList;
import android.util.Log;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
class ProgramsFactory extends AbstractXpathFactory {
private List<Program> programs = new ArrayList<Program>();
public ProgramsFactory(String xml) throws IOException {
super(xml);
try {
XPathExpression expression = xpath.compile("/tv/programme/@start");
NodeList starts = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
expression = xpath.compile("/tv/programme/@stop");
NodeList stops = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
expression = xpath.compile("tv/programme/title/text()");
NodeList names = (NodeList) expression.evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < names.getLength(); i++) {
String name = names.item(i).getNodeValue();
String start = starts.item(i).getNodeValue();
String stop = stops.item(i).getNodeValue();
Program program;
try {
program = new Program(name, start, stop);
programs.add(program);
} catch (ParseException ex) {
Log.e(ProgramsFactory.class.getCanonicalName(), ex.getMessage());
}
}
} catch (XPathExpressionException ex) {
Log.e(ProgramsFactory.class.getCanonicalName(), ex.getMessage());
}
}
public List<Program> getPrograms() {
return programs;
}
}

View File

@@ -0,0 +1,105 @@
package com.example.exoplayertv.xmltv.model;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class Channel {
public Channel() {}
private String id;
private String name;
private String logo = "http://xmltv.tvsajten.com/chanlogos/%s.png";
// private String logo = "https://www.teleboy.ch/assets/stations/303/icon320_dark.png";
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
private Map<String, List<Program>> allPrograms = new HashMap<String, List<Program>>();
public Channel(String id, String name) {
this.id = id;
this.name = name;
}
public String getLogo() {
// return "http://search.ch/tv/images/logo/sf1.png";
return String.format(logo, id);
}
public String getName() {
return name;
}
public String getId() {
return id;
}
public List<Program> getPrograms() {
return getPrograms(Calendar.getInstance().getTime(), DisplayMode.FUTURE);
}
public List<Program> getPrograms(Date date) {
return getPrograms(date, DisplayMode.FUTURE);
}
public List<Program> getPrograms(Date date, DisplayMode programMode) {
String string = dateFormat.format(date);
List<Program> datePrograms = allPrograms.get(string);
if (datePrograms == null) datePrograms = new ArrayList<Program>();
if (DisplayMode.ALL.equals(programMode)) return datePrograms;
List<Program> programModePrograms = new ArrayList<Program>();
for (Program program : datePrograms) {
if (program.getStop().after(date)) {
if (DisplayMode.CURRENT.equals(programMode)) {
if (program.getStart().before(date)) {
programModePrograms.add(program);
}
} else {
programModePrograms.add(program);
}
}
}
return programModePrograms;
}
public void addPrograms(Date date, List<Program> programs) {
String string = dateFormat.format(date);
allPrograms.put(string, programs);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Channel other = (Channel) obj;
if ((this.id == null) ? (other.id != null) : !this.id.equals(other.id)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
return hash;
}
@Override
public String toString() {
return "Channel{" + "id=" + id + ", name=" + name + ", logo=" + getLogo() + '}';
}
}

View File

@@ -0,0 +1,10 @@
package com.example.exoplayertv.xmltv.model;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*
*/
public enum DisplayMode {
ALL, FUTURE, CURRENT
}

View File

@@ -0,0 +1,44 @@
package com.example.exoplayertv.xmltv.model;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
*
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
*/
public class Program {
public Program() {}
private String name;
private Date start;
private Date stop;
public Program(String name, String start, String stop) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss Z");
this.start = dateFormat.parse(start);
this.stop = dateFormat.parse(stop);
this.name = name;
}
public String getName() {
return name;
}
public Date getStart() {
return start;
}
public Date getStop() {
return stop;
}
@Override
public String toString() {
return "Program{" + "name=" + name + ", start=" + start + ", stop=" + stop + '}';
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/dialog_body"
android:layout_width="match_parent"
android:layout_height="250dp"
android:orientation="vertical">
<TextView
android:id="@+id/dialog_costumtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:textColor="#020202"
android:textStyle="bold"
android:textSize="16sp"/>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:padding="10dp"
android:background="#020202" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="20dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:layout_weight="0.40"
android:textColor="#020202"
android:textStyle="bold"
android:text="Jahr"/>
<TextView
android:id="@+id/dialog_date"
android:layout_width="0dp"
android:layout_height="match_parent"
android:textColor="#020202"
android:layout_weight="0.60"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="20dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:layout_weight="0.40"
android:textColor="#020202"
android:textStyle="bold"
android:text="Land"/>
<TextView
android:id="@+id/dialog_country"
android:layout_width="0dp"
android:layout_height="match_parent"
android:textColor="#020202"
android:layout_weight="0.60"/>
</LinearLayout>
<LinearLayout
android:id="@+id/dialog_categories_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:layout_weight="0.40"
android:textColor="#020202"
android:textStyle="bold"
android:text="Kategorie"/>
<TextView
android:id="@+id/dialog_categories"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#020202"
android:layout_weight="0.60"/>
</LinearLayout>
<LinearLayout
android:id="@+id/dialog_director_layout"
android:layout_width="match_parent"
android:layout_height="20dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:layout_weight="0.40"
android:textColor="#020202"
android:textStyle="bold"
android:text="Regisseur"/>
<TextView
android:id="@+id/dialog_director"
android:layout_width="0dp"
android:layout_height="match_parent"
android:textColor="#020202"
android:layout_weight="0.60"/>
</LinearLayout>
<LinearLayout
android:id="@+id/dialog_actors_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:layout_weight="0.40"
android:textColor="#020202"
android:textStyle="bold"
android:text="Schauspieler"/>
<TextView
android:id="@+id/dialog_actors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#020202"
android:layout_weight="0.60"/>
</LinearLayout>
<TextView
android:id="@+id/dialog_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#020202"
android:padding="10dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_below="@id/dialog_body">
<LinearLayout
android:id="@+id/dialog_cancel_select"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.50"
android:background="#cccccc"
android:padding="5dp">
<Button
android:id="@+id/dialog_cancel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#cccccc"
android:focusable="true"
android:text="Cancel" />
</LinearLayout>
<LinearLayout
android:id="@+id/dialog_ok_select"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.50"
android:background="#787878"
android:padding="5dp">
<Button
android:id="@+id/dialog_ok"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#787878"
android:focusable="true"
android:text="Ok"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<androidx.media3.ui.PlayerView android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:id="@+id/controls_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#88000000"
android:orientation="horizontal">
<Button
android:id="@+id/select_tracks_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/track_selection_title" />
<TextView
android:id="@+id/debug_text_view"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="20sp" />
<TextClock
android:id="@+id/textClock"
android:textStyle="bold"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:paddingRight="4dp"
android:gravity="right"
android:textColor="@android:color/white"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.exoplayertv.tvepg.epg.EPG
android:id="@+id/epg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/epg_background"/>
</LinearLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@+id/sample_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
<ImageButton android:id="@+id/download_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/exo_download_description"
android:background="@android:color/transparent"/>
</LinearLayout>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:id="@+id/track_selection_dialog_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_selection_dialog_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed"/>
</androidx.viewpager.widget.ViewPager>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<Button
android:id="@+id/track_selection_dialog_cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
style="?android:attr/borderlessButtonStyle"/>
<Button
android:id="@+id/track_selection_dialog_ok_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
style="?android:attr/borderlessButtonStyle"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".SampleChooserActivity">
<item android:id="@+id/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_update" android:title="@string/action_update"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_destroy" android:title="@string/action_destroy"
android:orderInCategory="100" app:showAsAction="never" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="epg_background">#ff1e1e1e</color>
<color name="epg_channel_layout_background">#ff323232</color>
<color name="epg_event_layout_background">#ff4f4f4f</color>
<color name="epg_event_layout_background_current">#ff3b3b3b</color>
<color name="epg_event_layout_background_selected">#4f0000</color>
<color name="epg_event_layout_background_current_selected">#3b0000</color>
<color name="epg_event_layout_text">#ffd6d6d6</color>
<color name="epg_time_bar">#ffc57120</color>
</resources>

View File

@@ -0,0 +1,18 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<!-- EPG dimensions-->
<dimen name="epg_channel_layout_margin">3dp</dimen>
<dimen name="epg_channel_layout_padding">8dp</dimen>
<dimen name="epg_channel_layout_height">70dp</dimen>
<dimen name="epg_channel_layout_width">70dp</dimen>
<dimen name="epg_event_layout_text">20dp</dimen>
<dimen name="epg_time_bar_height">30dp</dimen>
<dimen name="epg_time_bar_text">14dp</dimen>
<dimen name="epg_time_bar_line_width">2dp</dimen>
<dimen name="epg_reset_button_size">40dp</dimen>
<dimen name="epg_reset_button_margin">10dp</dimen>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="exo_title" type="id"/>
</resources>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="application_name">TV-EPG</string>
<string name="track_selection_title">Select tracks</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_cleartext_not_permitted">Cleartext HTTP traffic not permitted. See https://exoplayer.dev/issues/cleartext-not-permitted</string>
<string name="error_generic">Playback failed</string>
<string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_querying_decoders">Unable to query device decoders</string>
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
<string name="storage_permission_denied">Permission to access storage was denied</string>
<string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="post_notification_not_granted">Notifications suppressed. Grant permission to see download notifications.</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_start_error_offline_license">Failed to obtain offline license</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="app_name">TV EPG</string>
<string name="action_settings">Einstellung</string>
<string name="action_update">EPG aktualisieren</string>
<string name="action_destroy">EPG beenden</string>
</resources>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="windowNoTitle">false</item>
</style>
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/black</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

4
build.gradle Normal file
View File

@@ -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
}

21
gradle.properties Normal file
View File

@@ -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

24
gradle/libs.versions.toml Normal file
View File

@@ -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" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -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

185
gradlew vendored Normal file
View File

@@ -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" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -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

23
settings.gradle Normal file
View File

@@ -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'