Initial Commit!
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
87
app/build.gradle
Normal 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
@@ -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
|
||||
BIN
app/release/baselineProfiles/0/app-release.dm
Normal file
BIN
app/release/baselineProfiles/1/app-release.dm
Normal file
37
app/release/output-metadata.json
Normal 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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
120
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
205
app/src/main/java/com/example/exoplayertv/DemoUtil.java
Normal 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() {}
|
||||
}
|
||||
417
app/src/main/java/com/example/exoplayertv/DownloadTracker.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
app/src/main/java/com/example/exoplayertv/Gesten.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
266
app/src/main/java/com/example/exoplayertv/IntentUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
694
app/src/main/java/com/example/exoplayertv/PlayerActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/com/example/exoplayertv/package-info.java
Normal 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;
|
||||
816
app/src/main/java/com/example/exoplayertv/tvepg/epg/EPG.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.exoplayertv.tvepg.xmltv.model;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
|
||||
*
|
||||
*/
|
||||
public enum DisplayMode {
|
||||
ALL, FUTURE, CURRENT
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.exoplayertv.xmltv.model;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Daniel Oskarsson (daniel.oskarsson@gmail.com)
|
||||
*
|
||||
*/
|
||||
public enum DisplayMode {
|
||||
ALL, FUTURE, CURRENT
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 199 B |
BIN
app/src/main/res/drawable-hdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 218 B |
BIN
app/src/main/res/drawable-mdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
app/src/main/res/drawable-mdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
app/src/main/res/drawable-xhdpi/ic_banner.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
app/src/main/res/drawable-xhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 304 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 261 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 450 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 263 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 575 B |
BIN
app/src/main/res/drawable/reset.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
192
app/src/main/res/layout/dialog_description.xml
Normal 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>
|
||||
67
app/src/main/res/layout/player_activity.xml
Normal 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>
|
||||
26
app/src/main/res/layout/sample_chooser_activity.xml
Normal 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>
|
||||
38
app/src/main/res/layout/sample_list_item.xml
Normal 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>
|
||||
59
app/src/main/res/layout/track_selection_dialog.xml
Normal 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>
|
||||
26
app/src/main/res/menu/sample_chooser_menu.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_old.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_old.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_old.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_old.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_old.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
11
app/src/main/res/values/colors.xml
Normal 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>
|
||||
18
app/src/main/res/values/dimens.xml
Normal 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>
|
||||
4
app/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="exo_title" type="id"/>
|
||||
</resources>
|
||||
69
app/src/main/res/values/strings.xml
Normal 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>
|
||||
39
app/src/main/res/values/styles.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||