├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── at │ │ └── pansy │ │ └── iptv │ │ ├── activity │ │ └── TvInputSetupActivity.java │ │ ├── domain │ │ ├── Channel.java │ │ ├── PlaybackInfo.java │ │ └── Program.java │ │ ├── fragment │ │ └── TvInputSetupFragment.java │ │ ├── player │ │ └── TvInputPlayer.java │ │ ├── service │ │ ├── AccountService.java │ │ ├── SyncService.java │ │ └── TvInputService.java │ │ ├── sync │ │ └── SyncAdapter.java │ │ ├── util │ │ ├── IptvUtil.java │ │ ├── RendererUtil.java │ │ ├── SyncUtil.java │ │ └── TvContractUtil.java │ │ └── xmltv │ │ └── XmlTvParser.java │ └── res │ ├── drawable-mdpi │ ├── default_background.xml │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── layout │ ├── overlay_view.xml │ └── setup_activity.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values │ ├── colors.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml │ └── xml │ ├── authenticator.xml │ ├── syncadapter.xml │ └── tvinputservice.xml ├── build.gradle ├── gradle.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iptv-live-channels 2 | IPTV Live Channels for Android TV 3 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | 4 | android { 5 | compileSdkVersion 21 6 | buildToolsVersion "23.0.1" 7 | 8 | defaultConfig { 9 | applicationId "at.pansy.iptv" 10 | minSdkVersion 21 11 | targetSdkVersion 21 12 | versionCode 1 13 | versionName "0.1" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile 'com.google.android.exoplayer:exoplayer:r1.5.0' 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | compile 'com.android.support:recyclerview-v7:21.0.3' 30 | compile 'com.android.support:leanback-v17:21.0.3' 31 | compile 'com.android.support:appcompat-v7:21.0.3' 32 | compile 'com.github.bumptech.glide:glide:3.4.+' 33 | } 34 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/notz/programs/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/activity/TvInputSetupActivity.java: -------------------------------------------------------------------------------- 1 | package at.pansy.iptv.activity; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import at.pansy.iptv.R; 7 | 8 | /** 9 | * Created by notz. 10 | */ 11 | public class TvInputSetupActivity extends Activity { 12 | @Override 13 | public void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | 16 | setContentView(R.layout.setup_activity); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/domain/Channel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.domain; 18 | 19 | import android.content.ContentValues; 20 | import android.database.Cursor; 21 | import android.media.tv.TvContract; 22 | import android.text.TextUtils; 23 | 24 | import java.util.Objects; 25 | 26 | /** 27 | * A convenience class to create and insert program information into the database. 28 | */ 29 | public final class Channel implements Comparable { 30 | private static final long INVALID_LONG_VALUE = -1; 31 | 32 | private long channelId; 33 | private String displayName; 34 | private String displayNumber; 35 | private String internalProviderData; 36 | 37 | private Channel() { 38 | channelId = INVALID_LONG_VALUE; 39 | } 40 | 41 | public long getChannelId() { 42 | return channelId; 43 | } 44 | 45 | public String getDisplayName() { 46 | return displayName; 47 | } 48 | 49 | public String getDisplayNumber() { 50 | return displayNumber; 51 | } 52 | 53 | public String getInternalProviderData() { 54 | return internalProviderData; 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(channelId, displayName, displayNumber, internalProviderData); 60 | } 61 | 62 | @Override 63 | public boolean equals(Object other) { 64 | if (!(other instanceof Channel)) { 65 | return false; 66 | } 67 | Channel channel = (Channel) other; 68 | return channelId == channel.channelId 69 | && Objects.equals(displayName, channel.displayName) 70 | && Objects.equals(displayNumber, channel.displayNumber) 71 | && Objects.equals(internalProviderData, channel.internalProviderData); 72 | } 73 | 74 | @Override 75 | public int compareTo(Channel other) { 76 | return Long.compare(channelId, other.channelId); 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "Channel{" 82 | + "channelId=" + channelId 83 | + ", displayName=" + displayName 84 | + ", displayNumber=" + displayNumber 85 | + ", internalProviderData=" + internalProviderData 86 | + "}"; 87 | } 88 | 89 | public void copyFrom(Channel other) { 90 | if (this == other) { 91 | return; 92 | } 93 | 94 | channelId = other.channelId; 95 | displayName = other.displayName; 96 | displayNumber = other.displayNumber; 97 | internalProviderData = other.internalProviderData; 98 | } 99 | 100 | public ContentValues toContentValues() { 101 | ContentValues values = new ContentValues(); 102 | if (!TextUtils.isEmpty(displayName)) { 103 | values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName); 104 | } else { 105 | values.putNull(TvContract.Channels.COLUMN_DISPLAY_NAME); 106 | } 107 | if (!TextUtils.isEmpty(displayNumber)) { 108 | values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, displayNumber); 109 | } else { 110 | values.putNull(TvContract.Channels.COLUMN_DISPLAY_NUMBER); 111 | } 112 | if (!TextUtils.isEmpty(internalProviderData)) { 113 | values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, internalProviderData); 114 | } else { 115 | values.putNull(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA); 116 | } 117 | return values; 118 | } 119 | 120 | public static Channel fromCursor(Cursor cursor) { 121 | Builder builder = new Builder(); 122 | int index = cursor.getColumnIndex(TvContract.Channels._ID); 123 | if (index >= 0 && !cursor.isNull(index)) { 124 | builder.setChannelId(cursor.getLong(index)); 125 | } 126 | index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NAME); 127 | if (index >= 0 && !cursor.isNull(index)) { 128 | builder.setDisplayName(cursor.getString(index)); 129 | } 130 | index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NUMBER); 131 | if (index >= 0 && !cursor.isNull(index)) { 132 | builder.setDisplayNumber(cursor.getString(index)); 133 | } 134 | index = cursor.getColumnIndex(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA); 135 | if (index >= 0 && !cursor.isNull(index)) { 136 | builder.setInternalProviderData(cursor.getString(index)); 137 | } 138 | return builder.build(); 139 | } 140 | 141 | public static final class Builder { 142 | private final Channel channel; 143 | 144 | public Builder() { 145 | channel = new Channel(); 146 | } 147 | 148 | public Builder(Channel other) { 149 | channel = new Channel(); 150 | channel.copyFrom(other); 151 | } 152 | 153 | public Builder setChannelId(long channelId) { 154 | channel.channelId = channelId; 155 | return this; 156 | } 157 | 158 | public Builder setDisplayName(String displayName) { 159 | channel.displayName = displayName; 160 | return this; 161 | } 162 | 163 | public Builder setDisplayNumber(String displayNumber) { 164 | channel.displayNumber = displayNumber; 165 | return this; 166 | } 167 | 168 | public Builder setInternalProviderData(String data) { 169 | channel.internalProviderData = data; 170 | return this; 171 | } 172 | 173 | public Channel build() { 174 | return channel; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/domain/PlaybackInfo.java: -------------------------------------------------------------------------------- 1 | package at.pansy.iptv.domain; 2 | 3 | import android.media.tv.TvContentRating; 4 | 5 | /** 6 | * Created by notz. 7 | */ 8 | public class PlaybackInfo { 9 | 10 | public static final int VIDEO_TYPE_HTTP_PROGRESSIVE = 0; 11 | public static final int VIDEO_TYPE_HLS = 1; 12 | public static final int VIDEO_TYPE_MPEG_DASH = 2; 13 | public static final int VIDEO_TYPE_OTHER = 3; 14 | 15 | public final long startTimeMs; 16 | public final long endTimeMs; 17 | public final String videoUrl; 18 | public final int videoType; 19 | public final TvContentRating[] contentRatings; 20 | 21 | public PlaybackInfo(long startTimeMs, long endTimeMs, String videoUrl, int videoType, 22 | TvContentRating[] contentRatings) { 23 | this.startTimeMs = startTimeMs; 24 | this.endTimeMs = endTimeMs; 25 | this.contentRatings = contentRatings; 26 | this.videoUrl = videoUrl; 27 | this.videoType = videoType; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/domain/Program.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.domain; 18 | 19 | import android.content.ContentValues; 20 | import android.database.Cursor; 21 | import android.media.tv.TvContentRating; 22 | import android.media.tv.TvContract; 23 | import android.text.TextUtils; 24 | 25 | import java.util.Arrays; 26 | import java.util.Objects; 27 | 28 | import at.pansy.iptv.util.TvContractUtil; 29 | 30 | /** 31 | * A convenience class to create and insert program information into the database. 32 | */ 33 | public final class Program implements Comparable { 34 | 35 | private static final long INVALID_LONG_VALUE = -1; 36 | private static final int INVALID_INT_VALUE = -1; 37 | 38 | private long programId; 39 | private long channelId; 40 | private String title; 41 | private String episodeTitle; 42 | private int seasonNumber; 43 | private int episodeNumber; 44 | private long startTimeUtcMillis; 45 | private long endTimeUtcMillis; 46 | private String description; 47 | private String longDescription; 48 | private int videoWidth; 49 | private int videoHeight; 50 | private String posterArtUri; 51 | private String thumbnailUri; 52 | private String[] canonicalGenres; 53 | private TvContentRating[] contentRatings; 54 | private String internalProviderData; 55 | 56 | private Program() { 57 | channelId = INVALID_LONG_VALUE; 58 | programId = INVALID_LONG_VALUE; 59 | seasonNumber = INVALID_INT_VALUE; 60 | episodeNumber = INVALID_INT_VALUE; 61 | startTimeUtcMillis = INVALID_LONG_VALUE; 62 | endTimeUtcMillis = INVALID_LONG_VALUE; 63 | videoWidth = INVALID_INT_VALUE; 64 | videoHeight = INVALID_INT_VALUE; 65 | } 66 | 67 | public long getProgramId() { 68 | return programId; 69 | } 70 | 71 | public long getChannelId() { 72 | return channelId; 73 | } 74 | 75 | public String getTitle() { 76 | return title; 77 | } 78 | 79 | public String getEpisodeTitle() { 80 | return episodeTitle; 81 | } 82 | 83 | public int getSeasonNumber() { 84 | return seasonNumber; 85 | } 86 | 87 | public int getEpisodeNumber() { 88 | return episodeNumber; 89 | } 90 | 91 | public long getStartTimeUtcMillis() { 92 | return startTimeUtcMillis; 93 | } 94 | 95 | public long getEndTimeUtcMillis() { 96 | return endTimeUtcMillis; 97 | } 98 | 99 | public String getDescription() { 100 | return description; 101 | } 102 | 103 | public String getLongDescription() { 104 | return longDescription; 105 | } 106 | 107 | public int getVideoWidth() { 108 | return videoWidth; 109 | } 110 | 111 | public int getVideoHeight() { 112 | return videoHeight; 113 | } 114 | 115 | public String[] getCanonicalGenres() { 116 | return canonicalGenres; 117 | } 118 | 119 | public TvContentRating[] getContentRatings() { 120 | return contentRatings; 121 | } 122 | 123 | public String getPosterArtUri() { 124 | return posterArtUri; 125 | } 126 | 127 | public String getThumbnailUri() { 128 | return thumbnailUri; 129 | } 130 | 131 | public String getInternalProviderData() { 132 | return internalProviderData; 133 | } 134 | 135 | @Override 136 | public int hashCode() { 137 | return Objects.hash(channelId, startTimeUtcMillis, endTimeUtcMillis, 138 | title, episodeTitle, description, longDescription, videoWidth, videoHeight, 139 | posterArtUri, thumbnailUri, contentRatings, canonicalGenres, seasonNumber, 140 | episodeNumber); 141 | } 142 | 143 | @Override 144 | public boolean equals(Object other) { 145 | if (!(other instanceof Program)) { 146 | return false; 147 | } 148 | Program program = (Program) other; 149 | return channelId == program.channelId 150 | && startTimeUtcMillis == program.startTimeUtcMillis 151 | && endTimeUtcMillis == program.endTimeUtcMillis 152 | && Objects.equals(title, program.title) 153 | && Objects.equals(episodeTitle, program.episodeTitle) 154 | && Objects.equals(description, program.description) 155 | && Objects.equals(longDescription, program.longDescription) 156 | && videoWidth == program.videoWidth 157 | && videoHeight == program.videoHeight 158 | && Objects.equals(posterArtUri, program.posterArtUri) 159 | && Objects.equals(thumbnailUri, program.thumbnailUri) 160 | && Arrays.equals(contentRatings, program.contentRatings) 161 | && Arrays.equals(canonicalGenres, program.canonicalGenres) 162 | && seasonNumber == program.seasonNumber 163 | && episodeNumber == program.episodeNumber; 164 | } 165 | 166 | @Override 167 | public int compareTo(Program other) { 168 | return Long.compare(startTimeUtcMillis, other.startTimeUtcMillis); 169 | } 170 | 171 | @Override 172 | public String toString() { 173 | return "Program{" 174 | + "programId=" + programId 175 | + ", channelId=" + channelId 176 | + ", title=" + title 177 | + ", episodeTitle=" + episodeTitle 178 | + ", seasonNumber=" + seasonNumber 179 | + ", episodeNumber=" + episodeNumber 180 | + ", startTimeUtcSec=" + startTimeUtcMillis 181 | + ", endTimeUtcSec=" + endTimeUtcMillis 182 | + ", videoWidth=" + videoWidth 183 | + ", videoHeight=" + videoHeight 184 | + ", contentRatings=" + contentRatings 185 | + ", posterArtUri=" + posterArtUri 186 | + ", thumbnailUri=" + thumbnailUri 187 | + ", contentRatings=" + contentRatings 188 | + ", genres=" + canonicalGenres 189 | + "}"; 190 | } 191 | 192 | public void copyFrom(Program other) { 193 | if (this == other) { 194 | return; 195 | } 196 | 197 | programId = other.programId; 198 | channelId = other.channelId; 199 | title = other.title; 200 | episodeTitle = other.episodeTitle; 201 | seasonNumber = other.seasonNumber; 202 | episodeNumber = other.episodeNumber; 203 | startTimeUtcMillis = other.startTimeUtcMillis; 204 | endTimeUtcMillis = other.endTimeUtcMillis; 205 | description = other.description; 206 | longDescription = other.longDescription; 207 | videoWidth = other.videoWidth; 208 | videoHeight = other.videoHeight; 209 | posterArtUri = other.posterArtUri; 210 | thumbnailUri = other.thumbnailUri; 211 | canonicalGenres = other.canonicalGenres; 212 | contentRatings = other.contentRatings; 213 | } 214 | 215 | public ContentValues toContentValues() { 216 | ContentValues values = new ContentValues(); 217 | if (channelId != INVALID_LONG_VALUE) { 218 | values.put(TvContract.Programs.COLUMN_CHANNEL_ID, channelId); 219 | } else { 220 | values.putNull(TvContract.Programs.COLUMN_CHANNEL_ID); 221 | } 222 | if (!TextUtils.isEmpty(title)) { 223 | values.put(TvContract.Programs.COLUMN_TITLE, title); 224 | } else { 225 | values.putNull(TvContract.Programs.COLUMN_TITLE); 226 | } 227 | if (!TextUtils.isEmpty(episodeTitle)) { 228 | values.put(TvContract.Programs.COLUMN_EPISODE_TITLE, episodeTitle); 229 | } else { 230 | values.putNull(TvContract.Programs.COLUMN_EPISODE_TITLE); 231 | } 232 | if (seasonNumber != INVALID_INT_VALUE) { 233 | values.put(TvContract.Programs.COLUMN_SEASON_NUMBER, seasonNumber); 234 | } else { 235 | values.putNull(TvContract.Programs.COLUMN_SEASON_NUMBER); 236 | } 237 | if (episodeNumber != INVALID_INT_VALUE) { 238 | values.put(TvContract.Programs.COLUMN_EPISODE_NUMBER, episodeNumber); 239 | } else { 240 | values.putNull(TvContract.Programs.COLUMN_EPISODE_NUMBER); 241 | } 242 | if (!TextUtils.isEmpty(description)) { 243 | values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, description); 244 | } else { 245 | values.putNull(TvContract.Programs.COLUMN_SHORT_DESCRIPTION); 246 | } 247 | if (!TextUtils.isEmpty(posterArtUri)) { 248 | values.put(TvContract.Programs.COLUMN_POSTER_ART_URI, posterArtUri); 249 | } else { 250 | values.putNull(TvContract.Programs.COLUMN_POSTER_ART_URI); 251 | } 252 | if (!TextUtils.isEmpty(thumbnailUri)) { 253 | values.put(TvContract.Programs.COLUMN_THUMBNAIL_URI, thumbnailUri); 254 | } else { 255 | values.putNull(TvContract.Programs.COLUMN_THUMBNAIL_URI); 256 | } 257 | if (canonicalGenres != null && canonicalGenres.length > 0) { 258 | values.put(TvContract.Programs.COLUMN_CANONICAL_GENRE, 259 | TvContract.Programs.Genres.encode(canonicalGenres)); 260 | } else { 261 | values.putNull(TvContract.Programs.COLUMN_CANONICAL_GENRE); 262 | } 263 | if (contentRatings != null && contentRatings.length > 0) { 264 | values.put(TvContract.Programs.COLUMN_CONTENT_RATING, 265 | TvContractUtil.contentRatingsToString(contentRatings)); 266 | } else { 267 | values.putNull(TvContract.Programs.COLUMN_CONTENT_RATING); 268 | } 269 | if (startTimeUtcMillis != INVALID_LONG_VALUE) { 270 | values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, startTimeUtcMillis); 271 | } else { 272 | values.putNull(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS); 273 | } 274 | if (endTimeUtcMillis != INVALID_LONG_VALUE) { 275 | values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, endTimeUtcMillis); 276 | } else { 277 | values.putNull(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS); 278 | } 279 | if (videoWidth != INVALID_INT_VALUE) { 280 | values.put(TvContract.Programs.COLUMN_VIDEO_WIDTH, videoWidth); 281 | } else { 282 | values.putNull(TvContract.Programs.COLUMN_VIDEO_WIDTH); 283 | } 284 | if (videoHeight != INVALID_INT_VALUE) { 285 | values.put(TvContract.Programs.COLUMN_VIDEO_HEIGHT, videoHeight); 286 | } else { 287 | values.putNull(TvContract.Programs.COLUMN_VIDEO_HEIGHT); 288 | } 289 | if (!TextUtils.isEmpty(internalProviderData)) { 290 | values.put(TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, internalProviderData); 291 | } else { 292 | values.putNull(TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA); 293 | } 294 | return values; 295 | } 296 | 297 | public static Program fromCursor(Cursor cursor) { 298 | Builder builder = new Builder(); 299 | int index = cursor.getColumnIndex(TvContract.Programs._ID); 300 | if (index >= 0 && !cursor.isNull(index)) { 301 | builder.setProgramId(cursor.getLong(index)); 302 | } 303 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_CHANNEL_ID); 304 | if (index >= 0 && !cursor.isNull(index)) { 305 | builder.setChannelId(cursor.getLong(index)); 306 | } 307 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_TITLE); 308 | if (index >= 0 && !cursor.isNull(index)) { 309 | builder.setTitle(cursor.getString(index)); 310 | } 311 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_EPISODE_TITLE); 312 | if (index >= 0 && !cursor.isNull(index)) { 313 | builder.setEpisodeTitle(cursor.getString(index)); 314 | } 315 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_SEASON_NUMBER); 316 | if(index >= 0 && !cursor.isNull(index)) { 317 | builder.setSeasonNumber(cursor.getInt(index)); 318 | } 319 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_EPISODE_NUMBER); 320 | if(index >= 0 && !cursor.isNull(index)) { 321 | builder.setEpisodeNumber(cursor.getInt(index)); 322 | } 323 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_SHORT_DESCRIPTION); 324 | if (index >= 0 && !cursor.isNull(index)) { 325 | builder.setDescription(cursor.getString(index)); 326 | } 327 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_LONG_DESCRIPTION); 328 | if (index >= 0 && !cursor.isNull(index)) { 329 | builder.setLongDescription(cursor.getString(index)); 330 | } 331 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_POSTER_ART_URI); 332 | if (index >= 0 && !cursor.isNull(index)) { 333 | builder.setPosterArtUri(cursor.getString(index)); 334 | } 335 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_THUMBNAIL_URI); 336 | if (index >= 0 && !cursor.isNull(index)) { 337 | builder.setThumbnailUri(cursor.getString(index)); 338 | } 339 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_CANONICAL_GENRE); 340 | if (index >= 0 && !cursor.isNull(index)) { 341 | builder.setCanonicalGenres(TvContract.Programs.Genres.decode(cursor.getString(index))); 342 | } 343 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_CONTENT_RATING); 344 | if (index >= 0 && !cursor.isNull(index)) { 345 | builder.setContentRatings(TvContractUtil.stringToContentRatings(cursor.getString( 346 | index))); 347 | } 348 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS); 349 | if (index >= 0 && !cursor.isNull(index)) { 350 | builder.setStartTimeUtcMillis(cursor.getLong(index)); 351 | } 352 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS); 353 | if (index >= 0 && !cursor.isNull(index)) { 354 | builder.setEndTimeUtcMillis(cursor.getLong(index)); 355 | } 356 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_VIDEO_WIDTH); 357 | if (index >= 0 && !cursor.isNull(index)) { 358 | builder.setVideoWidth((int) cursor.getLong(index)); 359 | } 360 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_VIDEO_HEIGHT); 361 | if (index >= 0 && !cursor.isNull(index)) { 362 | builder.setVideoHeight((int) cursor.getLong(index)); 363 | } 364 | index = cursor.getColumnIndex(TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA); 365 | if (index >= 0 && !cursor.isNull(index)) { 366 | builder.setInternalProviderData(cursor.getString(index)); 367 | } 368 | return builder.build(); 369 | } 370 | 371 | public static final class Builder { 372 | private final Program mProgram; 373 | 374 | public Builder() { 375 | mProgram = new Program(); 376 | } 377 | 378 | public Builder(Program other) { 379 | mProgram = new Program(); 380 | mProgram.copyFrom(other); 381 | } 382 | 383 | public Builder setProgramId(long programId) { 384 | mProgram.programId = programId; 385 | return this; 386 | } 387 | 388 | public Builder setChannelId(long channelId) { 389 | mProgram.channelId = channelId; 390 | return this; 391 | } 392 | 393 | public Builder setTitle(String title) { 394 | mProgram.title = title; 395 | return this; 396 | } 397 | 398 | public Builder setEpisodeTitle(String episodeTitle) { 399 | mProgram.episodeTitle = episodeTitle; 400 | return this; 401 | } 402 | 403 | public Builder setSeasonNumber(int seasonNumber) { 404 | mProgram.seasonNumber = seasonNumber; 405 | return this; 406 | } 407 | 408 | public Builder setEpisodeNumber(int episodeNumber) { 409 | mProgram.episodeNumber = episodeNumber; 410 | return this; 411 | } 412 | 413 | public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { 414 | mProgram.startTimeUtcMillis = startTimeUtcMillis; 415 | return this; 416 | } 417 | 418 | public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { 419 | mProgram.endTimeUtcMillis = endTimeUtcMillis; 420 | return this; 421 | } 422 | 423 | public Builder setDescription(String description) { 424 | mProgram.description = description; 425 | return this; 426 | } 427 | 428 | public Builder setLongDescription(String longDescription) { 429 | mProgram.longDescription = longDescription; 430 | return this; 431 | } 432 | 433 | public Builder setVideoWidth(int width) { 434 | mProgram.videoWidth = width; 435 | return this; 436 | } 437 | 438 | public Builder setVideoHeight(int height) { 439 | mProgram.videoHeight = height; 440 | return this; 441 | } 442 | 443 | public Builder setContentRatings(TvContentRating[] contentRatings) { 444 | mProgram.contentRatings = contentRatings; 445 | return this; 446 | } 447 | 448 | public Builder setPosterArtUri(String posterArtUri) { 449 | mProgram.posterArtUri = posterArtUri; 450 | return this; 451 | } 452 | 453 | public Builder setThumbnailUri(String thumbnailUri) { 454 | mProgram.thumbnailUri = thumbnailUri; 455 | return this; 456 | } 457 | 458 | public Builder setCanonicalGenres(String[] genres) { 459 | mProgram.canonicalGenres = genres; 460 | return this; 461 | } 462 | 463 | public Builder setInternalProviderData(String data) { 464 | mProgram.internalProviderData = data; 465 | return this; 466 | } 467 | 468 | public Program build() { 469 | return mProgram; 470 | } 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/fragment/TvInputSetupFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.fragment; 18 | 19 | import android.accounts.Account; 20 | import android.app.Activity; 21 | import android.content.ContentResolver; 22 | import android.content.SyncStatusObserver; 23 | import android.media.tv.TvContract; 24 | import android.media.tv.TvInputInfo; 25 | import android.net.Uri; 26 | import android.os.AsyncTask; 27 | import android.os.Bundle; 28 | import android.support.v17.leanback.app.BackgroundManager; 29 | import android.support.v17.leanback.app.DetailsFragment; 30 | import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; 31 | import android.support.v17.leanback.widget.Action; 32 | import android.support.v17.leanback.widget.ArrayObjectAdapter; 33 | import android.support.v17.leanback.widget.ClassPresenterSelector; 34 | import android.support.v17.leanback.widget.DetailsOverviewRow; 35 | import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; 36 | import android.support.v17.leanback.widget.ListRow; 37 | import android.support.v17.leanback.widget.ListRowPresenter; 38 | import android.support.v17.leanback.widget.OnActionClickedListener; 39 | import android.widget.Toast; 40 | 41 | import at.pansy.iptv.R; 42 | import at.pansy.iptv.service.AccountService; 43 | import at.pansy.iptv.util.IptvUtil; 44 | import at.pansy.iptv.util.SyncUtil; 45 | import at.pansy.iptv.util.TvContractUtil; 46 | import at.pansy.iptv.xmltv.XmlTvParser; 47 | 48 | /** 49 | * Fragment which shows a sample UI for registering channels and setting up SyncAdapter to 50 | * provide program information in the background. 51 | */ 52 | public class TvInputSetupFragment extends DetailsFragment { 53 | 54 | private static final int ACTION_ADD_CHANNELS = 1; 55 | private static final int ACTION_CANCEL = 2; 56 | private static final int ACTION_IN_PROGRESS = 3; 57 | 58 | private XmlTvParser.TvListing tvListing = null; 59 | private String inputId = null; 60 | 61 | private Action addChannelAction; 62 | private Action inProgressAction; 63 | private ArrayObjectAdapter adapter; 64 | private Object syncObserverHandle; 65 | private boolean syncRequested; 66 | 67 | @Override 68 | public void onCreate(Bundle savedInstanceState) { 69 | super.onCreate(savedInstanceState); 70 | 71 | inputId = getActivity().getIntent().getStringExtra(TvInputInfo.EXTRA_INPUT_ID); 72 | new SetupRowTask().execute(); 73 | } 74 | 75 | @Override 76 | public void onDestroy() { 77 | super.onDestroy(); 78 | if (syncObserverHandle != null) { 79 | ContentResolver.removeStatusChangeListener(syncObserverHandle); 80 | syncObserverHandle = null; 81 | } 82 | } 83 | 84 | private class SetupRowTask extends AsyncTask { 85 | 86 | @Override 87 | protected Boolean doInBackground(Uri... params) { 88 | tvListing = IptvUtil.getTvListings(getActivity(), 89 | getString(R.string.iptv_ink_channel_url), IptvUtil.FORMAT_M3U); 90 | return tvListing != null; 91 | } 92 | 93 | @Override 94 | protected void onPostExecute(Boolean success) { 95 | if (success) { 96 | initUIs(); 97 | } else { 98 | onError(R.string.feed_error_message); 99 | } 100 | } 101 | 102 | private void initUIs() { 103 | DetailsOverviewRowPresenter dorPresenter = 104 | new DetailsOverviewRowPresenter(new DetailsDescriptionPresenter()); 105 | dorPresenter.setSharedElementEnterTransition(getActivity(), "SetUpFragment"); 106 | 107 | addChannelAction = new Action(ACTION_ADD_CHANNELS, getResources().getString( 108 | R.string.tv_input_setup_add_channel)); 109 | Action cancelAction = new Action(ACTION_CANCEL, 110 | getResources().getString(R.string.tv_input_setup_cancel)); 111 | inProgressAction = new Action(ACTION_IN_PROGRESS, getResources().getString( 112 | R.string.tv_input_setup_in_progress)); 113 | 114 | DetailsOverviewRow row = new DetailsOverviewRow(tvListing); 115 | row.addAction(addChannelAction); 116 | row.addAction(cancelAction); 117 | 118 | ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); 119 | // set detail background and style 120 | dorPresenter.setBackgroundColor(getResources().getColor(R.color.detail_background)); 121 | dorPresenter.setStyleLarge(true); 122 | 123 | dorPresenter.setOnActionClickedListener(new OnActionClickedListener() { 124 | @Override 125 | public void onActionClicked(Action action) { 126 | if (action.getId() == ACTION_ADD_CHANNELS) { 127 | setupChannels(inputId); 128 | } else if (action.getId() == ACTION_CANCEL) { 129 | getActivity().finish(); 130 | } 131 | } 132 | }); 133 | 134 | presenterSelector.addClassPresenter(DetailsOverviewRow.class, dorPresenter); 135 | presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); 136 | adapter = new ArrayObjectAdapter(presenterSelector); 137 | adapter.add(row); 138 | 139 | setAdapter(adapter); 140 | 141 | BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity()); 142 | backgroundManager.attach(getActivity().getWindow()); 143 | backgroundManager.setDrawable( 144 | getActivity().getDrawable(R.drawable.default_background)); 145 | } 146 | } 147 | 148 | private void onError(int errorResId) { 149 | Toast.makeText(getActivity(), errorResId, Toast.LENGTH_SHORT).show(); 150 | getActivity().finish(); 151 | } 152 | 153 | private void setupChannels(String inputId) { 154 | if (tvListing == null) { 155 | onError(R.string.feed_error_message); 156 | return; 157 | } 158 | 159 | TvContractUtil.updateChannels(getActivity(), inputId, tvListing.channels); 160 | SyncUtil.setUpPeriodicSync(getActivity(), inputId); 161 | SyncUtil.requestSync(inputId, true); 162 | 163 | syncRequested = true; 164 | // Watch for sync state changes 165 | if (syncObserverHandle == null) { 166 | final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING | 167 | ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; 168 | syncObserverHandle = ContentResolver.addStatusChangeListener(mask, 169 | mSyncStatusObserver); 170 | } 171 | } 172 | 173 | private class DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter { 174 | @Override 175 | protected void onBindDescription(ViewHolder viewHolder, Object item) { 176 | viewHolder.getTitle().setText(R.string.tv_input_label); 177 | } 178 | } 179 | 180 | private final SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() { 181 | private boolean syncServiceStarted; 182 | private boolean finished; 183 | 184 | @Override 185 | public void onStatusChanged(int which) { 186 | getActivity().runOnUiThread(new Runnable() { 187 | @Override 188 | public void run() { 189 | if (finished) { 190 | return; 191 | } 192 | 193 | Account account = AccountService.getAccount(SyncUtil.ACCOUNT_TYPE); 194 | boolean syncActive = ContentResolver.isSyncActive(account, 195 | TvContract.AUTHORITY); 196 | boolean syncPending = ContentResolver.isSyncPending(account, 197 | TvContract.AUTHORITY); 198 | boolean syncServiceInProgress = syncActive || syncPending; 199 | if (syncRequested && syncServiceStarted && !syncServiceInProgress) { 200 | // Only current programs are registered at this point. Request a full sync. 201 | SyncUtil.requestSync(inputId, false); 202 | 203 | getActivity().setResult(Activity.RESULT_OK); 204 | getActivity().finish(); 205 | finished = true; 206 | } 207 | if (!syncServiceStarted && syncServiceInProgress) { 208 | syncServiceStarted = syncServiceInProgress; 209 | DetailsOverviewRow detailRow = (DetailsOverviewRow) adapter.get(0); 210 | detailRow.removeAction(addChannelAction); 211 | detailRow.addAction(0, inProgressAction); 212 | adapter.notifyArrayItemRangeChanged(0, 1); 213 | } 214 | } 215 | }); 216 | } 217 | }; 218 | 219 | } 220 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/player/TvInputPlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package at.pansy.iptv.player; 17 | 18 | import android.content.Context; 19 | import android.content.pm.PackageInfo; 20 | import android.content.pm.PackageManager; 21 | import android.media.MediaCodec; 22 | import android.media.tv.TvTrackInfo; 23 | import android.net.Uri; 24 | import android.os.Build; 25 | import android.os.Handler; 26 | import android.util.Log; 27 | import android.view.Surface; 28 | 29 | import com.google.android.exoplayer.DefaultLoadControl; 30 | import com.google.android.exoplayer.DummyTrackRenderer; 31 | import com.google.android.exoplayer.ExoPlaybackException; 32 | import com.google.android.exoplayer.ExoPlayer; 33 | import com.google.android.exoplayer.ExoPlayerLibraryInfo; 34 | import com.google.android.exoplayer.LoadControl; 35 | import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; 36 | import com.google.android.exoplayer.MediaCodecTrackRenderer; 37 | import com.google.android.exoplayer.MediaCodecUtil; 38 | import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; 39 | import com.google.android.exoplayer.TrackRenderer; 40 | import com.google.android.exoplayer.chunk.ChunkSampleSource; 41 | import com.google.android.exoplayer.chunk.ChunkSource; 42 | import com.google.android.exoplayer.chunk.Format; 43 | import com.google.android.exoplayer.chunk.FormatEvaluator; 44 | import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil; 45 | import com.google.android.exoplayer.dash.DashChunkSource; 46 | import com.google.android.exoplayer.dash.DefaultDashTrackSelector; 47 | import com.google.android.exoplayer.dash.mpd.AdaptationSet; 48 | import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; 49 | import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; 50 | import com.google.android.exoplayer.dash.mpd.Period; 51 | import com.google.android.exoplayer.dash.mpd.Representation; 52 | import com.google.android.exoplayer.extractor.ExtractorSampleSource; 53 | import com.google.android.exoplayer.hls.HlsChunkSource; 54 | import com.google.android.exoplayer.hls.HlsMasterPlaylist; 55 | import com.google.android.exoplayer.hls.HlsPlaylist; 56 | import com.google.android.exoplayer.hls.HlsPlaylistParser; 57 | import com.google.android.exoplayer.hls.HlsSampleSource; 58 | import com.google.android.exoplayer.hls.Variant; 59 | import com.google.android.exoplayer.text.Cue; 60 | import com.google.android.exoplayer.text.TextRenderer; 61 | import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; 62 | import com.google.android.exoplayer.upstream.DataSource; 63 | import com.google.android.exoplayer.upstream.DefaultAllocator; 64 | import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; 65 | import com.google.android.exoplayer.upstream.DefaultHttpDataSource; 66 | import com.google.android.exoplayer.upstream.DefaultUriDataSource; 67 | import com.google.android.exoplayer.util.ManifestFetcher; 68 | import com.google.android.exoplayer.util.MimeTypes; 69 | 70 | import java.io.IOException; 71 | import java.util.ArrayList; 72 | import java.util.Collections; 73 | import java.util.Comparator; 74 | import java.util.List; 75 | import java.util.concurrent.CopyOnWriteArrayList; 76 | 77 | /** 78 | * A wrapper around {@link ExoPlayer} that provides a higher level interface. Designed for 79 | * integration with {@link android.media.tv.TvInputService}. 80 | */ 81 | public class TvInputPlayer implements TextRenderer { 82 | 83 | public static final int SOURCE_TYPE_HTTP_PROGRESSIVE = 0; 84 | public static final int SOURCE_TYPE_HLS = 1; 85 | public static final int SOURCE_TYPE_MPEG_DASH = 2; 86 | 87 | private static final int RENDERER_COUNT = 3; 88 | private static final int MIN_BUFFER_MS = 1000; 89 | private static final int MIN_REBUFFER_MS = 5000; 90 | 91 | private static final int BUFFER_SEGMENTS = 300; 92 | private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; 93 | private static final int VIDEO_BUFFER_SEGMENTS = 200; 94 | private static final int AUDIO_BUFFER_SEGMENTS = 60; 95 | private static final int LIVE_EDGE_LATENCY_MS = 30000; 96 | 97 | private static final int NO_TRACK_SELECTED = -1; 98 | 99 | private final Handler handler; 100 | private final ExoPlayer player; 101 | private TrackRenderer videoRenderer; 102 | private TrackRenderer audioRenderer; 103 | private TrackRenderer textRenderer; 104 | private final CopyOnWriteArrayList callbacks; 105 | private float volume; 106 | private Surface surface; 107 | private Long pendingSeekPosition; 108 | private final TvTrackInfo[][] tvTracks = new TvTrackInfo[RENDERER_COUNT][]; 109 | private final int[] selectedTvTracks = new int[RENDERER_COUNT]; 110 | 111 | private final MediaCodecVideoTrackRenderer.EventListener videoRendererEventListener = 112 | new MediaCodecVideoTrackRenderer.EventListener() { 113 | @Override 114 | public void onDroppedFrames(int count, long elapsed) { 115 | // Do nothing. 116 | } 117 | 118 | @Override 119 | public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { 120 | 121 | } 122 | 123 | @Override 124 | public void onDrawnToSurface(Surface surface) { 125 | for(Callback callback : callbacks) { 126 | callback.onDrawnToSurface(surface); 127 | } 128 | } 129 | 130 | @Override 131 | public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { 132 | 133 | } 134 | 135 | @Override 136 | public void onDecoderInitializationError( 137 | MediaCodecTrackRenderer.DecoderInitializationException e) { 138 | for(Callback callback : callbacks) { 139 | callback.onPlayerError(new ExoPlaybackException(e)); 140 | } 141 | } 142 | 143 | @Override 144 | public void onCryptoError(MediaCodec.CryptoException e) { 145 | for(Callback callback : callbacks) { 146 | callback.onPlayerError(new ExoPlaybackException(e)); 147 | } 148 | } 149 | }; 150 | 151 | public TvInputPlayer() { 152 | handler = new Handler(); 153 | for (int i = 0; i < RENDERER_COUNT; ++i) { 154 | tvTracks[i] = new TvTrackInfo[0]; 155 | selectedTvTracks[i] = NO_TRACK_SELECTED; 156 | } 157 | callbacks = new CopyOnWriteArrayList<>(); 158 | player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); 159 | player.addListener(new ExoPlayer.Listener() { 160 | @Override 161 | public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { 162 | for (Callback callback : callbacks) { 163 | callback.onPlayerStateChanged(playWhenReady, playbackState); 164 | } 165 | if (pendingSeekPosition != null && playbackState != ExoPlayer.STATE_IDLE 166 | && playbackState != ExoPlayer.STATE_PREPARING) { 167 | seekTo(pendingSeekPosition); 168 | pendingSeekPosition = null; 169 | } 170 | } 171 | 172 | @Override 173 | public void onPlayWhenReadyCommitted() { 174 | for (Callback callback : callbacks) { 175 | callback.onPlayWhenReadyCommitted(); 176 | } 177 | } 178 | 179 | @Override 180 | public void onPlayerError(ExoPlaybackException e) { 181 | for (Callback callback : callbacks) { 182 | callback.onPlayerError(e); 183 | } 184 | } 185 | }); 186 | } 187 | 188 | @Override 189 | public void onCues(List cues) { 190 | for (Callback callback : callbacks) { 191 | callback.onCues(cues); 192 | } 193 | } 194 | 195 | public void prepare(final Context context, final Uri originalUri, int sourceType) { 196 | 197 | final String userAgent = getUserAgent(context); 198 | final DefaultHttpDataSource dataSource = new DefaultHttpDataSource(userAgent, null); 199 | final Uri uri = processUriParameters(originalUri, dataSource); 200 | 201 | if (sourceType == SOURCE_TYPE_HTTP_PROGRESSIVE) { 202 | ExtractorSampleSource sampleSource = 203 | new ExtractorSampleSource(uri, dataSource, new DefaultAllocator(BUFFER_SEGMENT_SIZE), 204 | BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); 205 | audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); 206 | videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, 207 | MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, handler, 208 | videoRendererEventListener, 50); 209 | textRenderer = new DummyTrackRenderer(); 210 | prepareInternal(); 211 | } else if (sourceType == SOURCE_TYPE_HLS) { 212 | HlsPlaylistParser parser = new HlsPlaylistParser(); 213 | ManifestFetcher playlistFetcher = 214 | new ManifestFetcher<>(uri.toString(), dataSource, parser); 215 | playlistFetcher.singleLoad(handler.getLooper(), 216 | new ManifestFetcher.ManifestCallback() { 217 | @Override 218 | public void onSingleManifest(HlsPlaylist manifest) { 219 | DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); 220 | 221 | int[] variantIndices = null; 222 | if (manifest instanceof HlsMasterPlaylist) { 223 | HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) manifest; 224 | 225 | // Sort playlist from highest bitrate to lowest 226 | ArrayList variants = new ArrayList<>(masterPlaylist.variants); 227 | Collections.sort(variants, new Comparator() { 228 | @Override 229 | public int compare(Variant v1, Variant v2) { 230 | return Integer.compare(v2.format.bitrate, v1.format.bitrate); 231 | } 232 | }); 233 | manifest = masterPlaylist = new HlsMasterPlaylist(masterPlaylist.baseUri, 234 | variants, masterPlaylist.subtitles); 235 | 236 | try { 237 | variantIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( 238 | context, masterPlaylist.variants, null, false); 239 | } catch (MediaCodecUtil.DecoderQueryException e) { 240 | for (Callback callback : callbacks) { 241 | callback.onPlayerError(new ExoPlaybackException(e)); 242 | } 243 | return; 244 | } 245 | if (variantIndices.length == 0) { 246 | for (Callback callback : callbacks) { 247 | callback.onPlayerError(new ExoPlaybackException( 248 | new IllegalStateException("No variants selected."))); 249 | } 250 | return; 251 | } 252 | } 253 | 254 | HlsChunkSource chunkSource = new HlsChunkSource(dataSource, uri.toString(), 255 | manifest, bandwidthMeter, 256 | variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE); 257 | 258 | LoadControl lhc = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); 259 | HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, lhc, BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); 260 | audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); 261 | videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, 262 | MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, handler, 263 | videoRendererEventListener, 50); 264 | textRenderer = new Eia608TrackRenderer(sampleSource, 265 | TvInputPlayer.this, handler.getLooper()); 266 | // TODO: Implement custom HLS source to get the internal track metadata. 267 | tvTracks[TvTrackInfo.TYPE_SUBTITLE] = new TvTrackInfo[1]; 268 | tvTracks[TvTrackInfo.TYPE_SUBTITLE][0] = 269 | new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "1") 270 | .build(); 271 | prepareInternal(); 272 | } 273 | 274 | @Override 275 | public void onSingleManifestError(IOException e) { 276 | for (Callback callback : callbacks) { 277 | callback.onPlayerError(new ExoPlaybackException(e)); 278 | } 279 | } 280 | }); 281 | } else if (sourceType == SOURCE_TYPE_MPEG_DASH) { 282 | MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); 283 | final ManifestFetcher manifestFetcher = 284 | new ManifestFetcher<>(uri.toString(), dataSource, parser); 285 | manifestFetcher.singleLoad(handler.getLooper(), 286 | new ManifestFetcher.ManifestCallback() { 287 | @Override 288 | public void onSingleManifest(MediaPresentationDescription manifest) { 289 | Period period = manifest.getPeriod(0); 290 | LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator( 291 | BUFFER_SEGMENT_SIZE)); 292 | 293 | // Determine which video representations we should use for playback. 294 | int maxDecodableFrameSize; 295 | try { 296 | maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); 297 | } catch (MediaCodecUtil.DecoderQueryException e) { 298 | for (Callback callback : callbacks) { 299 | callback.onPlayerError(new ExoPlaybackException(e)); 300 | } 301 | return; 302 | } 303 | 304 | int videoAdaptationSetIndex = period.getAdaptationSetIndex( 305 | AdaptationSet.TYPE_VIDEO); 306 | List videoRepresentations = 307 | period.adaptationSets.get(videoAdaptationSetIndex).representations; 308 | ArrayList videoRepresentationIndexList = new ArrayList<>(); 309 | for (int i = 0; i < videoRepresentations.size(); i++) { 310 | Format format = videoRepresentations.get(i).format; 311 | if (format.width * format.height > maxDecodableFrameSize) { 312 | // Filtering stream that device cannot play 313 | } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) 314 | && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { 315 | // Filtering unsupported mime type 316 | } else { 317 | videoRepresentationIndexList.add(i); 318 | } 319 | } 320 | 321 | 322 | // Build the video renderer. 323 | if (videoRepresentationIndexList.isEmpty()) { 324 | videoRenderer = new DummyTrackRenderer(); 325 | } else { 326 | DataSource videoDataSource = new DefaultUriDataSource(context, userAgent); 327 | DefaultBandwidthMeter videoBandwidthMeter = new DefaultBandwidthMeter(); 328 | ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, 329 | DefaultDashTrackSelector.newVideoInstance(context, true, false), 330 | videoDataSource, 331 | new FormatEvaluator.AdaptiveEvaluator(videoBandwidthMeter), LIVE_EDGE_LATENCY_MS, 332 | 0, true, null, null); 333 | ChunkSampleSource videoSampleSource = new ChunkSampleSource( 334 | videoChunkSource, loadControl, 335 | VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); 336 | videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, 337 | MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, handler, 338 | videoRendererEventListener, 50); 339 | } 340 | 341 | // Build the audio chunk sources. 342 | int audioAdaptationSetIndex = period.getAdaptationSetIndex( 343 | AdaptationSet.TYPE_AUDIO); 344 | AdaptationSet audioAdaptationSet = period.adaptationSets.get( 345 | audioAdaptationSetIndex); 346 | List audioChunkSourceList = new ArrayList<>(); 347 | List audioTrackList = new ArrayList<>(); 348 | if (audioAdaptationSet != null) { 349 | DataSource audioDataSource = new DefaultUriDataSource(context, userAgent); 350 | FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); 351 | List audioRepresentations = 352 | audioAdaptationSet.representations; 353 | for (int i = 0; i < audioRepresentations.size(); i++) { 354 | Format format = audioRepresentations.get(i).format; 355 | audioTrackList.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, 356 | Integer.toString(i)) 357 | .setAudioChannelCount(format.audioChannels) 358 | .setAudioSampleRate(format.audioSamplingRate) 359 | .setLanguage(format.language) 360 | .build()); 361 | audioChunkSourceList.add(new DashChunkSource(manifestFetcher, 362 | DefaultDashTrackSelector.newAudioInstance(), 363 | audioDataSource, 364 | audioEvaluator, LIVE_EDGE_LATENCY_MS, 0, null, null)); 365 | } 366 | } 367 | 368 | // Build the audio renderer. 369 | //final MultiTrackChunkSource audioChunkSource; 370 | if (audioChunkSourceList.isEmpty()) { 371 | audioRenderer = new DummyTrackRenderer(); 372 | } else { 373 | //audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList); 374 | //SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, 375 | // loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); 376 | //audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource); 377 | TvTrackInfo[] tracks = new TvTrackInfo[audioTrackList.size()]; 378 | audioTrackList.toArray(tracks); 379 | tvTracks[TvTrackInfo.TYPE_AUDIO] = tracks; 380 | selectedTvTracks[TvTrackInfo.TYPE_AUDIO] = 0; 381 | //multiTrackChunkSources[TvTrackInfo.TYPE_AUDIO] = audioChunkSource; 382 | } 383 | 384 | // Build the text renderer. 385 | textRenderer = new DummyTrackRenderer(); 386 | 387 | prepareInternal(); 388 | } 389 | 390 | @Override 391 | public void onSingleManifestError(IOException e) { 392 | for (Callback callback : callbacks) { 393 | callback.onPlayerError(new ExoPlaybackException(e)); 394 | } 395 | } 396 | }); 397 | } else { 398 | throw new IllegalArgumentException("Unknown source type: " + sourceType); 399 | } 400 | } 401 | 402 | public TvTrackInfo[] getTracks(int trackType) { 403 | if (trackType < 0 || trackType >= tvTracks.length) { 404 | throw new IllegalArgumentException("Illegal track type: " + trackType); 405 | } 406 | return tvTracks[trackType]; 407 | } 408 | 409 | public String getSelectedTrack(int trackType) { 410 | if (trackType < 0 || trackType >= tvTracks.length) { 411 | throw new IllegalArgumentException("Illegal track type: " + trackType); 412 | } 413 | if (selectedTvTracks[trackType] == NO_TRACK_SELECTED) { 414 | return null; 415 | } 416 | return tvTracks[trackType][selectedTvTracks[trackType]].getId(); 417 | } 418 | 419 | public boolean selectTrack(int trackType, String trackId) { 420 | if (trackType < 0 || trackType >= tvTracks.length) { 421 | return false; 422 | } 423 | if (trackId == null) { 424 | player.setRendererEnabled(trackType, false); 425 | } else { 426 | int trackIndex = Integer.parseInt(trackId); 427 | /* 428 | if (multiTrackChunkSources[trackType] == null) { 429 | player.setRendererEnabled(trackType, true); 430 | } else { 431 | boolean playWhenReady = player.getPlayWhenReady(); 432 | player.setPlayWhenReady(false); 433 | player.setRendererEnabled(trackType, false); 434 | player.sendMessage(multiTrackChunkSources[trackType], 435 | MultiTrackChunkSource.MSG_SELECT_TRACK, trackIndex); 436 | player.setRendererEnabled(trackType, true); 437 | player.setPlayWhenReady(playWhenReady); 438 | } 439 | */ 440 | } 441 | return true; 442 | } 443 | 444 | public void setPlayWhenReady(boolean playWhenReady) { 445 | player.setPlayWhenReady(playWhenReady); 446 | } 447 | 448 | public void setVolume(float volume) { 449 | this.volume = volume; 450 | if (player != null && audioRenderer != null) { 451 | player.sendMessage(audioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, 452 | volume); 453 | } 454 | } 455 | 456 | public void setSurface(Surface surface) { 457 | this.surface = surface; 458 | if (player != null && videoRenderer != null) { 459 | player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, 460 | surface); 461 | } 462 | } 463 | 464 | public void seekTo(long position) { 465 | if (isPlayerPrepared(player)) { // The player doesn't know the duration until prepared. 466 | if (player.getDuration() != ExoPlayer.UNKNOWN_TIME) { 467 | player.seekTo(position); 468 | } 469 | } else { 470 | pendingSeekPosition = position; 471 | } 472 | } 473 | 474 | public void stop() { 475 | player.stop(); 476 | } 477 | 478 | public void release() { 479 | player.release(); 480 | } 481 | 482 | public void addCallback(Callback callback) { 483 | callbacks.add(callback); 484 | } 485 | 486 | public void removeCallback(Callback callback) { 487 | callbacks.remove(callback); 488 | } 489 | 490 | private void prepareInternal() { 491 | player.prepare(audioRenderer, videoRenderer, textRenderer); 492 | player.sendMessage(audioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, 493 | volume); 494 | player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, 495 | surface); 496 | // Disable text track by default. 497 | player.setRendererEnabled(TvTrackInfo.TYPE_SUBTITLE, false); 498 | for (Callback callback : callbacks) { 499 | callback.onPrepared(); 500 | } 501 | } 502 | 503 | private static String getUserAgent(Context context) { 504 | String versionName; 505 | try { 506 | String packageName = context.getPackageName(); 507 | PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); 508 | versionName = info.versionName; 509 | } catch (PackageManager.NameNotFoundException e) { 510 | versionName = "?"; 511 | } 512 | return "IptvLiveChannels/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE + 513 | ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; 514 | } 515 | 516 | private static boolean isPlayerPrepared(ExoPlayer player) { 517 | int state = player.getPlaybackState(); 518 | return state != ExoPlayer.STATE_PREPARING && state != ExoPlayer.STATE_IDLE; 519 | } 520 | 521 | private static Uri processUriParameters(Uri uri, DefaultHttpDataSource dataSource) { 522 | String[] parameters = uri.getPath().split("\\|"); 523 | for (int i = 1; i < parameters.length; i++) { 524 | String[] pair = parameters[i].split("=", 2); 525 | if (pair.length == 2) { 526 | dataSource.setRequestProperty(pair[0], pair[1]); 527 | } 528 | } 529 | 530 | return uri.buildUpon().path(parameters[0]).build(); 531 | } 532 | 533 | public interface Callback { 534 | void onPrepared(); 535 | void onPlayerStateChanged(boolean playWhenReady, int state); 536 | void onPlayWhenReadyCommitted(); 537 | void onPlayerError(ExoPlaybackException e); 538 | void onDrawnToSurface(Surface surface); 539 | void onCues(List cues); 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/service/AccountService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.service; 18 | 19 | import android.accounts.AbstractAccountAuthenticator; 20 | import android.accounts.Account; 21 | import android.accounts.AccountAuthenticatorResponse; 22 | import android.accounts.NetworkErrorException; 23 | import android.app.Service; 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.os.Bundle; 27 | import android.os.IBinder; 28 | 29 | 30 | /** 31 | * Dummy account service for SyncAdapter. Note that this does nothing because this input uses a feed 32 | * which does not require any authentication. 33 | */ 34 | public class AccountService extends Service { 35 | 36 | public static final String ACCOUNT_NAME = "IPTV Live Channels"; 37 | 38 | private Authenticator authenticator; 39 | 40 | public static Account getAccount(String accountType) { 41 | return new Account(ACCOUNT_NAME, accountType); 42 | } 43 | 44 | @Override 45 | public void onCreate() { 46 | authenticator = new Authenticator(this); 47 | } 48 | 49 | @Override 50 | public IBinder onBind(Intent intent) { 51 | return authenticator.getIBinder(); 52 | } 53 | 54 | /** 55 | * Dummy Authenticator used in {@link SyncAdapter}. This does nothing for all the operations 56 | * since channel/program feed does not require any authentication. 57 | */ 58 | public class Authenticator extends AbstractAccountAuthenticator { 59 | public Authenticator(Context context) { 60 | super(context); 61 | } 62 | 63 | @Override 64 | public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, 65 | String s) { 66 | throw new UnsupportedOperationException(); 67 | } 68 | 69 | @Override 70 | public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, 71 | String s, String s2, String[] strings, Bundle bundle) throws NetworkErrorException { 72 | return null; 73 | } 74 | 75 | @Override 76 | public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, 77 | Account account, Bundle bundle) throws NetworkErrorException { 78 | return null; 79 | } 80 | 81 | @Override 82 | public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, 83 | Account account, String s, Bundle bundle) throws NetworkErrorException { 84 | throw new UnsupportedOperationException(); 85 | } 86 | 87 | @Override 88 | public String getAuthTokenLabel(String s) { 89 | throw new UnsupportedOperationException(); 90 | } 91 | 92 | @Override 93 | public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, 94 | Account account, String s, Bundle bundle) throws NetworkErrorException { 95 | throw new UnsupportedOperationException(); 96 | } 97 | 98 | @Override 99 | public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, 100 | Account account, String[] strings) throws NetworkErrorException { 101 | throw new UnsupportedOperationException(); 102 | } 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/service/SyncService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.service; 18 | 19 | import android.app.Service; 20 | import android.content.Intent; 21 | import android.os.IBinder; 22 | 23 | import at.pansy.iptv.sync.SyncAdapter; 24 | 25 | /** 26 | * Service which provides the SyncAdapter implementation to the framework on request. 27 | */ 28 | public class SyncService extends Service { 29 | private static final Object syncAdapterLock = new Object(); 30 | private static SyncAdapter syncAdapter = null; 31 | 32 | @Override 33 | public void onCreate() { 34 | super.onCreate(); 35 | synchronized (syncAdapterLock) { 36 | if (syncAdapter == null) { 37 | syncAdapter = new SyncAdapter(getApplicationContext(), true); 38 | } 39 | } 40 | } 41 | 42 | @Override 43 | public IBinder onBind(Intent intent) { 44 | return syncAdapter.getSyncAdapterBinder(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/service/TvInputService.java: -------------------------------------------------------------------------------- 1 | package at.pansy.iptv.service; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.media.tv.TvContentRating; 8 | import android.media.tv.TvInputManager; 9 | import android.media.tv.TvTrackInfo; 10 | import android.net.Uri; 11 | import android.os.Handler; 12 | import android.os.HandlerThread; 13 | import android.os.Message; 14 | import android.util.Log; 15 | import android.view.LayoutInflater; 16 | import android.view.Surface; 17 | import android.view.View; 18 | import android.view.accessibility.CaptioningManager; 19 | 20 | import com.google.android.exoplayer.ExoPlaybackException; 21 | import com.google.android.exoplayer.ExoPlayer; 22 | import com.google.android.exoplayer.text.CaptionStyleCompat; 23 | import com.google.android.exoplayer.text.Cue; 24 | import com.google.android.exoplayer.text.SubtitleLayout; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.HashSet; 29 | import java.util.List; 30 | import java.util.Set; 31 | 32 | import at.pansy.iptv.R; 33 | import at.pansy.iptv.domain.Channel; 34 | import at.pansy.iptv.domain.PlaybackInfo; 35 | import at.pansy.iptv.player.TvInputPlayer; 36 | import at.pansy.iptv.util.SyncUtil; 37 | import at.pansy.iptv.util.TvContractUtil; 38 | 39 | /** 40 | * Created by notz. 41 | */ 42 | public class TvInputService extends android.media.tv.TvInputService { 43 | 44 | private static final String TAG = "TvInputService"; 45 | 46 | private HandlerThread handlerThread; 47 | private Handler dbHandler; 48 | 49 | private List sessions; 50 | private CaptioningManager captioningManager; 51 | 52 | private final BroadcastReceiver parentalControlsBroadcastReceiver = new BroadcastReceiver() { 53 | @Override 54 | public void onReceive(Context context, Intent intent) { 55 | if (sessions != null) { 56 | for (TvInputSession session : sessions) { 57 | session.checkContentBlockNeeded(); 58 | } 59 | } 60 | } 61 | }; 62 | 63 | @Override 64 | public void onCreate() { 65 | super.onCreate(); 66 | handlerThread = new HandlerThread(getClass().getSimpleName()); 67 | handlerThread.start(); 68 | dbHandler = new Handler(handlerThread.getLooper()); 69 | captioningManager = (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); 70 | 71 | setTheme(android.R.style.Theme_Holo_Light_NoActionBar); 72 | 73 | sessions = new ArrayList<>(); 74 | IntentFilter intentFilter = new IntentFilter(); 75 | intentFilter.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED); 76 | intentFilter.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED); 77 | registerReceiver(parentalControlsBroadcastReceiver, intentFilter); 78 | } 79 | 80 | @Override 81 | public void onDestroy() { 82 | super.onDestroy(); 83 | unregisterReceiver(parentalControlsBroadcastReceiver); 84 | handlerThread.quit(); 85 | handlerThread = null; 86 | dbHandler = null; 87 | } 88 | 89 | @Override 90 | public final Session onCreateSession(String inputId) { 91 | TvInputSession session = new TvInputSession(this, inputId); 92 | session.setOverlayViewEnabled(true); 93 | sessions.add(session); 94 | return session; 95 | } 96 | 97 | class TvInputSession extends android.media.tv.TvInputService.Session implements Handler.Callback { 98 | 99 | private static final int MSG_PLAY_PROGRAM = 1000; 100 | 101 | private final Context context; 102 | private final TvInputManager tvInputManager; 103 | protected TvInputPlayer player; 104 | private Surface surface; 105 | private float volume; 106 | private boolean captionEnabled; 107 | private PlaybackInfo currentPlaybackInfo; 108 | private TvContentRating lastBlockedRating; 109 | private TvContentRating currentContentRating; 110 | private String celectedSubtitleTrackId; 111 | private SubtitleLayout subtitleLayout; 112 | private boolean epgSyncRequested; 113 | private final Set unblockedRatingSet = new HashSet<>(); 114 | private final Handler handler; 115 | 116 | private final TvInputPlayer.Callback playerCallback = new TvInputPlayer.Callback() { 117 | 118 | private boolean firstFrameDrawn; 119 | 120 | @Override 121 | public void onPrepared() { 122 | firstFrameDrawn = false; 123 | List tracks = new ArrayList<>(); 124 | Collections.addAll(tracks, player.getTracks(TvTrackInfo.TYPE_AUDIO)); 125 | Collections.addAll(tracks, player.getTracks(TvTrackInfo.TYPE_VIDEO)); 126 | Collections.addAll(tracks, player.getTracks(TvTrackInfo.TYPE_SUBTITLE)); 127 | 128 | notifyTracksChanged(tracks); 129 | notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, player.getSelectedTrack( 130 | TvTrackInfo.TYPE_AUDIO)); 131 | notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, player.getSelectedTrack( 132 | TvTrackInfo.TYPE_VIDEO)); 133 | notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, player.getSelectedTrack( 134 | TvTrackInfo.TYPE_SUBTITLE)); 135 | } 136 | 137 | @Override 138 | public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { 139 | if (playWhenReady && playbackState == ExoPlayer.STATE_BUFFERING) { 140 | if (firstFrameDrawn) { 141 | notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); 142 | } 143 | } else if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { 144 | notifyVideoAvailable(); 145 | } 146 | } 147 | 148 | @Override 149 | public void onPlayWhenReadyCommitted() { 150 | // Do nothing. 151 | } 152 | 153 | @Override 154 | public void onPlayerError(ExoPlaybackException e) { 155 | // Do nothing. 156 | } 157 | 158 | @Override 159 | public void onDrawnToSurface(Surface surface) { 160 | firstFrameDrawn = true; 161 | notifyVideoAvailable(); 162 | } 163 | 164 | @Override 165 | public void onCues(List cues) { 166 | if (subtitleLayout != null) { 167 | if (cues.isEmpty()) { 168 | subtitleLayout.setVisibility(View.INVISIBLE); 169 | } else { 170 | subtitleLayout.setVisibility(View.VISIBLE); 171 | subtitleLayout.setCues(cues); 172 | } 173 | } 174 | } 175 | }; 176 | 177 | private PlayCurrentProgramRunnable playCurrentProgramRunnable; 178 | private String inputId; 179 | 180 | protected TvInputSession(Context context, String inputId) { 181 | super(context); 182 | this.context = context; 183 | this.inputId = inputId; 184 | 185 | tvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); 186 | lastBlockedRating = null; 187 | captionEnabled = captioningManager.isEnabled(); 188 | handler = new Handler(this); 189 | } 190 | 191 | @Override 192 | public boolean handleMessage(Message msg) { 193 | if (msg.what == MSG_PLAY_PROGRAM) { 194 | playProgram((PlaybackInfo) msg.obj); 195 | return true; 196 | } 197 | return false; 198 | } 199 | 200 | @Override 201 | public void onRelease() { 202 | if (dbHandler != null) { 203 | dbHandler.removeCallbacks(playCurrentProgramRunnable); 204 | } 205 | releasePlayer(); 206 | sessions.remove(this); 207 | } 208 | 209 | @Override 210 | public View onCreateOverlayView() { 211 | LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); 212 | View view = inflater.inflate(R.layout.overlay_view, null); 213 | subtitleLayout = (SubtitleLayout) view.findViewById(R.id.subtitles); 214 | 215 | // Configure the subtitle view. 216 | CaptionStyleCompat captionStyle; 217 | captionStyle = CaptionStyleCompat.createFromCaptionStyle( 218 | captioningManager.getUserStyle()); 219 | subtitleLayout.setStyle(captionStyle); 220 | subtitleLayout.setFractionalTextSize(captioningManager.getFontScale()); 221 | return view; 222 | } 223 | 224 | @Override 225 | public boolean onSetSurface(Surface surface) { 226 | if (player != null) { 227 | player.setSurface(surface); 228 | } 229 | this.surface = surface; 230 | return true; 231 | } 232 | 233 | @Override 234 | public void onSetStreamVolume(float volume) { 235 | if (player != null) { 236 | player.setVolume(volume); 237 | } 238 | this.volume = volume; 239 | } 240 | 241 | private boolean playProgram(PlaybackInfo info) { 242 | releasePlayer(); 243 | 244 | currentPlaybackInfo = info; 245 | currentContentRating = (info.contentRatings == null || info.contentRatings.length == 0) 246 | ? null : info.contentRatings[0]; 247 | player = new TvInputPlayer(); 248 | player.addCallback(playerCallback); 249 | player.prepare(TvInputService.this, Uri.parse(info.videoUrl), info.videoType); 250 | player.setSurface(surface); 251 | player.setVolume(volume); 252 | 253 | long nowMs = System.currentTimeMillis(); 254 | int seekPosMs = (int) (nowMs - info.startTimeMs); 255 | if (seekPosMs > 0) { 256 | player.seekTo(seekPosMs); 257 | } 258 | player.setPlayWhenReady(true); 259 | 260 | checkContentBlockNeeded(); 261 | dbHandler.postDelayed(playCurrentProgramRunnable, info.endTimeMs - nowMs + 1000); 262 | return true; 263 | } 264 | 265 | @Override 266 | public boolean onTune(Uri channelUri) { 267 | if (subtitleLayout != null) { 268 | subtitleLayout.setVisibility(View.INVISIBLE); 269 | } 270 | notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); 271 | unblockedRatingSet.clear(); 272 | 273 | dbHandler.removeCallbacks(playCurrentProgramRunnable); 274 | playCurrentProgramRunnable = new PlayCurrentProgramRunnable(channelUri); 275 | dbHandler.post(playCurrentProgramRunnable); 276 | return true; 277 | } 278 | 279 | @Override 280 | public void onSetCaptionEnabled(boolean enabled) { 281 | captionEnabled = enabled; 282 | if (player != null) { 283 | if (enabled) { 284 | if (celectedSubtitleTrackId != null) { 285 | player.selectTrack(TvTrackInfo.TYPE_SUBTITLE, celectedSubtitleTrackId); 286 | } 287 | } else { 288 | player.selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); 289 | } 290 | } 291 | } 292 | 293 | @Override 294 | public boolean onSelectTrack(int type, String trackId) { 295 | if (player != null) { 296 | if (type == TvTrackInfo.TYPE_SUBTITLE) { 297 | if (!captionEnabled && trackId != null) { 298 | return false; 299 | } 300 | celectedSubtitleTrackId = trackId; 301 | if (trackId == null) { 302 | subtitleLayout.setVisibility(View.INVISIBLE); 303 | } 304 | } 305 | if (player.selectTrack(type, trackId)) { 306 | notifyTrackSelected(type, trackId); 307 | return true; 308 | } 309 | } 310 | return false; 311 | } 312 | 313 | @Override 314 | public void onUnblockContent(TvContentRating rating) { 315 | if (rating != null) { 316 | unblockContent(rating); 317 | } 318 | } 319 | 320 | private void releasePlayer() { 321 | if (player != null) { 322 | player.removeCallback(playerCallback); 323 | player.setSurface(null); 324 | player.stop(); 325 | player.release(); 326 | player = null; 327 | } 328 | } 329 | 330 | private void checkContentBlockNeeded() { 331 | if (currentContentRating == null || !tvInputManager.isParentalControlsEnabled() 332 | || !tvInputManager.isRatingBlocked(currentContentRating) 333 | || unblockedRatingSet.contains(currentContentRating)) { 334 | // Content rating is changed so we don't need to block anymore. 335 | // Unblock content here explicitly to resume playback. 336 | unblockContent(null); 337 | return; 338 | } 339 | 340 | lastBlockedRating = currentContentRating; 341 | if (player != null) { 342 | // Children restricted content might be blocked by TV app as well, 343 | // but TIS should do its best not to show any single frame of blocked content. 344 | releasePlayer(); 345 | } 346 | 347 | notifyContentBlocked(currentContentRating); 348 | } 349 | 350 | private void unblockContent(TvContentRating rating) { 351 | // TIS should unblock content only if unblock request is legitimate. 352 | if (rating == null || lastBlockedRating == null 353 | || rating.equals(lastBlockedRating)) { 354 | lastBlockedRating = null; 355 | if (rating != null) { 356 | unblockedRatingSet.add(rating); 357 | } 358 | if (player == null && currentPlaybackInfo != null) { 359 | playProgram(currentPlaybackInfo); 360 | } 361 | notifyContentAllowed(); 362 | } 363 | } 364 | 365 | private class PlayCurrentProgramRunnable implements Runnable { 366 | 367 | private static final int RETRY_DELAY_MS = 2000; 368 | private final Uri mChannelUri; 369 | 370 | public PlayCurrentProgramRunnable(Uri channelUri) { 371 | mChannelUri = channelUri; 372 | } 373 | 374 | @Override 375 | public void run() { 376 | long nowMs = System.currentTimeMillis(); 377 | List programs = TvContractUtil.getProgramPlaybackInfo( 378 | context.getContentResolver(), mChannelUri, nowMs, nowMs + 1, 1); 379 | if (programs.isEmpty()) { 380 | Log.w(TAG, "Failed to get program info for " + mChannelUri + ". Retry in " + 381 | RETRY_DELAY_MS + "ms."); 382 | if (!epgSyncRequested) { 383 | SyncUtil.requestSync(inputId, true); 384 | epgSyncRequested = true; 385 | } 386 | 387 | String url = null; 388 | 389 | Channel channel = TvContractUtil.getChannel(context.getContentResolver(), mChannelUri); 390 | if (channel != null) { 391 | url = channel.getInternalProviderData(); 392 | } 393 | PlaybackInfo playbackInfo = new PlaybackInfo(nowMs, nowMs + 3600 * 1000l, 394 | url, 1, new TvContentRating[] {}); 395 | programs.add(playbackInfo); 396 | } 397 | 398 | handler.removeMessages(MSG_PLAY_PROGRAM); 399 | handler.obtainMessage(MSG_PLAY_PROGRAM, programs.get(0)).sendToTarget(); 400 | } 401 | } 402 | } 403 | } 404 | 405 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/sync/SyncAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.sync; 18 | 19 | import android.accounts.Account; 20 | import android.content.AbstractThreadedSyncAdapter; 21 | import android.content.ContentProviderClient; 22 | import android.content.ContentProviderOperation; 23 | import android.content.ContentUris; 24 | import android.content.Context; 25 | import android.content.OperationApplicationException; 26 | import android.content.SyncResult; 27 | import android.media.tv.TvContract; 28 | import android.net.Uri; 29 | import android.os.Bundle; 30 | import android.os.RemoteException; 31 | import android.util.Log; 32 | import android.util.LongSparseArray; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | import at.pansy.iptv.R; 38 | import at.pansy.iptv.domain.Program; 39 | import at.pansy.iptv.util.IptvUtil; 40 | import at.pansy.iptv.util.TvContractUtil; 41 | import at.pansy.iptv.xmltv.XmlTvParser; 42 | 43 | /** 44 | * A SyncAdapter implementation which updates program info periodically. 45 | */ 46 | public class SyncAdapter extends AbstractThreadedSyncAdapter { 47 | public static final String TAG = "SyncAdapter"; 48 | 49 | public static final String BUNDLE_KEY_INPUT_ID = "bundle_key_input_id"; 50 | public static final String BUNDLE_KEY_CURRENT_PROGRAM_ONLY = "bundle_key_current_program_only"; 51 | public static final long FULL_SYNC_FREQUENCY_SEC = 60 * 60 * 24; // daily 52 | 53 | private static final int FULL_SYNC_WINDOW_SEC = 60 * 60 * 24 * 14; // 2 weeks 54 | private static final int SHORT_SYNC_WINDOW_SEC = 60 * 60; // 1 hour 55 | private static final int BATCH_OPERATION_COUNT = 100; 56 | 57 | private final Context context; 58 | 59 | public SyncAdapter(Context context, boolean autoInitialize) { 60 | super(context, autoInitialize); 61 | this.context = context; 62 | } 63 | 64 | public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { 65 | super(context, autoInitialize, allowParallelSyncs); 66 | this.context = context; 67 | } 68 | 69 | /** 70 | * Called periodically by the system in every {@code FULL_SYNC_FREQUENCY_SEC}. 71 | */ 72 | @Override 73 | public void onPerformSync(Account account, Bundle extras, String authority, 74 | ContentProviderClient provider, SyncResult syncResult) { 75 | 76 | Log.d(TAG, "onPerformSync(" + account + ", " + authority + ", " + extras + ")"); 77 | String inputId = extras.getString(SyncAdapter.BUNDLE_KEY_INPUT_ID); 78 | if (inputId == null) { 79 | return; 80 | } 81 | 82 | XmlTvParser.TvListing listings = IptvUtil.getTvListings(context, 83 | context.getString(R.string.iptv_ink_epg_url), IptvUtil.FORMAT_XMLTV); 84 | 85 | XmlTvParser.TvListing channelListings = IptvUtil.getTvListings(context, 86 | context.getString(R.string.iptv_ink_channel_url), IptvUtil.FORMAT_M3U); 87 | listings.setChannels(channelListings.channels); 88 | 89 | 90 | LongSparseArray channelMap = TvContractUtil.buildChannelMap( 91 | context.getContentResolver(), inputId, listings.channels); 92 | boolean currentProgramOnly = extras.getBoolean( 93 | SyncAdapter.BUNDLE_KEY_CURRENT_PROGRAM_ONLY, false); 94 | long startMs = System.currentTimeMillis(); 95 | long endMs = startMs + FULL_SYNC_WINDOW_SEC * 1000; 96 | if (currentProgramOnly) { 97 | // This is requested from the setup activity, in this case, users don't need to wait for 98 | // the full sync. Sync the current programs first and do the full sync later in the 99 | // background. 100 | endMs = startMs + SHORT_SYNC_WINDOW_SEC * 1000; 101 | } 102 | for (int i = 0; i < channelMap.size(); ++i) { 103 | Uri channelUri = TvContract.buildChannelUri(channelMap.keyAt(i)); 104 | List programs = getPrograms(channelUri, channelMap.valueAt(i), 105 | listings.programs, startMs, endMs); 106 | updatePrograms(channelUri, programs); 107 | } 108 | } 109 | 110 | /** 111 | * Returns a list of programs for the given time range. 112 | * 113 | * @param channelUri The channel where the program info will be added. 114 | * @param channel The {@link XmlTvParser.XmlTvChannel} for the programs to return. 115 | * @param programs The feed fetched from cloud. 116 | * @param startTimeMs The start time of the range requested. 117 | * @param endTimeMs The end time of the range requested. 118 | */ 119 | private List getPrograms(Uri channelUri, XmlTvParser.XmlTvChannel channel, 120 | List programs, long startTimeMs, long endTimeMs) { 121 | if (startTimeMs > endTimeMs) { 122 | throw new IllegalArgumentException(); 123 | } 124 | List channelPrograms = new ArrayList<>(); 125 | for (XmlTvParser.XmlTvProgram program : programs) { 126 | if (program.channelId.equals(channel.id)) { 127 | channelPrograms.add(program); 128 | } 129 | } 130 | 131 | List programForGivenTime = new ArrayList<>(); 132 | if (!channel.repeatPrograms) { 133 | for (XmlTvParser.XmlTvProgram program : channelPrograms) { 134 | if (program.startTimeUtcMillis <= endTimeMs 135 | && program.endTimeUtcMillis >= startTimeMs) { 136 | programForGivenTime.add(new Program.Builder() 137 | .setChannelId(ContentUris.parseId(channelUri)) 138 | .setTitle(program.title) 139 | .setDescription(program.description) 140 | .setContentRatings(XmlTvParser.xmlTvRatingToTvContentRating( 141 | program.rating)) 142 | .setCanonicalGenres(program.category) 143 | .setPosterArtUri(program.icon != null ? program.icon.src : null) 144 | .setInternalProviderData(TvContractUtil. 145 | convertVideoInfoToInternalProviderData( 146 | program.videoType, 147 | program.videoSrc != null ? program.videoSrc : channel.url)) 148 | .setStartTimeUtcMillis(program.startTimeUtcMillis) 149 | .setEndTimeUtcMillis(program.endTimeUtcMillis) 150 | .build() 151 | ); 152 | } 153 | } 154 | return programForGivenTime; 155 | } 156 | 157 | // If repeat-programs is on, schedule the programs sequentially in a loop. To make every 158 | // device play the same program in a given channel and time, we assumes the loop started 159 | // from the epoch time. 160 | long totalDurationMs = 0; 161 | for (XmlTvParser.XmlTvProgram program : channelPrograms) { 162 | totalDurationMs += program.getDurationMillis(); 163 | } 164 | 165 | long programStartTimeMs = startTimeMs - startTimeMs % totalDurationMs; 166 | int i = 0; 167 | final int programCount = channelPrograms.size(); 168 | while (programStartTimeMs < endTimeMs) { 169 | XmlTvParser.XmlTvProgram programInfo = channelPrograms.get(i++ % programCount); 170 | long programEndTimeMs = programStartTimeMs + programInfo.getDurationMillis(); 171 | if (programEndTimeMs < startTimeMs) { 172 | programStartTimeMs = programEndTimeMs; 173 | continue; 174 | } 175 | programForGivenTime.add(new Program.Builder() 176 | .setChannelId(ContentUris.parseId(channelUri)) 177 | .setTitle(programInfo.title) 178 | .setDescription(programInfo.description) 179 | .setContentRatings(XmlTvParser.xmlTvRatingToTvContentRating( 180 | programInfo.rating)) 181 | .setCanonicalGenres(programInfo.category) 182 | .setPosterArtUri(programInfo.icon.src) 183 | // NOTE: {@code COLUMN_INTERNAL_PROVIDER_DATA} is a private field where 184 | // TvInputService can store anything it wants. Here, we store video type and 185 | // video URL so that TvInputService can play the video later with this field. 186 | .setInternalProviderData(TvContractUtil.convertVideoInfoToInternalProviderData( 187 | programInfo.videoType, 188 | programInfo.videoSrc != null ? programInfo.videoSrc : channel.url)) 189 | .setStartTimeUtcMillis(programStartTimeMs) 190 | .setEndTimeUtcMillis(programEndTimeMs) 191 | .build() 192 | ); 193 | programStartTimeMs = programEndTimeMs; 194 | } 195 | return programForGivenTime; 196 | } 197 | 198 | /** 199 | * Updates the system database, TvProvider, with the given programs. 200 | * 201 | *

If there is any overlap between the given and existing programs, the existing ones 202 | * will be updated with the given ones if they have the same title or replaced. 203 | * 204 | * @param channelUri The channel where the program info will be added. 205 | * @param newPrograms A list of {@link Program} instances which includes program 206 | * information. 207 | */ 208 | private void updatePrograms(Uri channelUri, List newPrograms) { 209 | final int fetchedProgramsCount = newPrograms.size(); 210 | if (fetchedProgramsCount == 0) { 211 | return; 212 | } 213 | List oldPrograms = TvContractUtil.getPrograms(context.getContentResolver(), 214 | channelUri); 215 | Program firstNewProgram = newPrograms.get(0); 216 | int oldProgramsIndex = 0; 217 | int newProgramsIndex = 0; 218 | // Skip the past programs. They will be automatically removed by the system. 219 | for (Program program : oldPrograms) { 220 | oldProgramsIndex++; 221 | if(program.getEndTimeUtcMillis() > firstNewProgram.getStartTimeUtcMillis()) { 222 | break; 223 | } 224 | } 225 | // Compare the new programs with old programs one by one and update/delete the old one or 226 | // insert new program if there is no matching program in the database. 227 | ArrayList ops = new ArrayList<>(); 228 | while (newProgramsIndex < fetchedProgramsCount) { 229 | Program oldProgram = oldProgramsIndex < oldPrograms.size() 230 | ? oldPrograms.get(oldProgramsIndex) : null; 231 | Program newProgram = newPrograms.get(newProgramsIndex); 232 | boolean addNewProgram = false; 233 | if (oldProgram != null) { 234 | if (oldProgram.equals(newProgram)) { 235 | // Exact match. No need to update. Move on to the next programs. 236 | oldProgramsIndex++; 237 | newProgramsIndex++; 238 | } else if (needsUpdate(oldProgram, newProgram)) { 239 | // Partial match. Update the old program with the new one. 240 | // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There could 241 | // be application specific settings which belong to the old program. 242 | ops.add(ContentProviderOperation.newUpdate( 243 | TvContract.buildProgramUri(oldProgram.getProgramId())) 244 | .withValues(newProgram.toContentValues()) 245 | .build()); 246 | oldProgramsIndex++; 247 | newProgramsIndex++; 248 | } else if (oldProgram.getEndTimeUtcMillis() < newProgram.getEndTimeUtcMillis()) { 249 | // No match. Remove the old program first to see if the next program in 250 | // {@code oldPrograms} partially matches the new program. 251 | ops.add(ContentProviderOperation.newDelete( 252 | TvContract.buildProgramUri(oldProgram.getProgramId())) 253 | .build()); 254 | oldProgramsIndex++; 255 | } else { 256 | // No match. The new program does not match any of the old programs. Insert it 257 | // as a new program. 258 | addNewProgram = true; 259 | newProgramsIndex++; 260 | } 261 | } else { 262 | // No old programs. Just insert new programs. 263 | addNewProgram = true; 264 | newProgramsIndex++; 265 | } 266 | if (addNewProgram) { 267 | ops.add(ContentProviderOperation 268 | .newInsert(TvContract.Programs.CONTENT_URI) 269 | .withValues(newProgram.toContentValues()) 270 | .build()); 271 | } 272 | // Throttle the batch operation not to cause TransactionTooLargeException. 273 | if (ops.size() > BATCH_OPERATION_COUNT 274 | || newProgramsIndex >= fetchedProgramsCount) { 275 | try { 276 | context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); 277 | } catch (RemoteException | OperationApplicationException e) { 278 | Log.e(TAG, "Failed to insert programs.", e); 279 | return; 280 | } 281 | ops.clear(); 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * Returns {@code true} if the {@code oldProgram} program needs to be updated with the 288 | * {@code newProgram} program. 289 | */ 290 | private boolean needsUpdate(Program oldProgram, Program newProgram) { 291 | // NOTE: Here, we update the old program if it has the same title and overlaps with the new 292 | // program. The test logic is just an example and you can modify this. E.g. check whether 293 | // the both programs have the same program ID if your EPG supports any ID for the programs. 294 | return oldProgram.getTitle().equals(newProgram.getTitle()) 295 | && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis() 296 | && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis(); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/util/IptvUtil.java: -------------------------------------------------------------------------------- 1 | package at.pansy.iptv.util; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.Context; 5 | import android.net.Uri; 6 | import android.util.Log; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.BufferedInputStream; 11 | import java.io.InputStream; 12 | import java.io.InputStreamReader; 13 | import java.net.URL; 14 | import java.net.URLConnection; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.zip.GZIPInputStream; 20 | 21 | import at.pansy.iptv.xmltv.XmlTvParser; 22 | 23 | /** 24 | * Static helper methods for fetching the channel feed. 25 | */ 26 | public class IptvUtil { 27 | 28 | public static final int FORMAT_XMLTV = 0; 29 | public static final int FORMAT_M3U = 1; 30 | 31 | private static final String TAG = "IptvUtil"; 32 | private static HashMap sampleTvListings = new HashMap<>(); 33 | 34 | private static final int URLCONNECTION_CONNECTION_TIMEOUT_MS = 3000; // 3 sec 35 | private static final int URLCONNECTION_READ_TIMEOUT_MS = 10000; // 10 sec 36 | 37 | private IptvUtil() { 38 | } 39 | 40 | public static XmlTvParser.TvListing getTvListings(Context context, String url, int format) { 41 | 42 | if (sampleTvListings.containsKey(url)) { 43 | return sampleTvListings.get(url); 44 | } 45 | 46 | Uri catalogUri = 47 | Uri.parse(url).normalizeScheme(); 48 | 49 | XmlTvParser.TvListing sampleTvListing = null; 50 | try { 51 | InputStream inputStream = getInputStream(context, catalogUri); 52 | if (url.endsWith(".gz")) { 53 | inputStream = new GZIPInputStream(inputStream); 54 | } 55 | if (format == FORMAT_M3U) { 56 | sampleTvListing = parse(inputStream); 57 | } else { 58 | sampleTvListing = XmlTvParser.parse(inputStream); 59 | } 60 | } catch (IOException e) { 61 | Log.e(TAG, "Error in fetching " + catalogUri, e); 62 | } 63 | if (sampleTvListing != null) { 64 | sampleTvListings.put(url, sampleTvListing); 65 | } 66 | return sampleTvListing; 67 | } 68 | 69 | public static InputStream getInputStream(Context context, Uri uri) throws IOException { 70 | InputStream inputStream; 71 | if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme()) 72 | || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme()) 73 | || ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 74 | inputStream = context.getContentResolver().openInputStream(uri); 75 | } else { 76 | URLConnection urlConnection = new URL(uri.toString()).openConnection(); 77 | urlConnection.setConnectTimeout(URLCONNECTION_CONNECTION_TIMEOUT_MS); 78 | urlConnection.setReadTimeout(URLCONNECTION_READ_TIMEOUT_MS); 79 | inputStream = urlConnection.getInputStream(); 80 | } 81 | return new BufferedInputStream(inputStream); 82 | } 83 | 84 | private static XmlTvParser.TvListing parse(InputStream inputStream) throws IOException { 85 | BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); 86 | String line; 87 | List channels = new ArrayList<>(); 88 | List programs = new ArrayList<>(); 89 | Map channelMap = new HashMap<>(); 90 | 91 | while ((line = in.readLine()) != null) { 92 | if (line.startsWith("#EXTINF:")) { 93 | // #EXTINF:0051 tvg-id="blizz.de" group-title="DE Spartensender" tvg-logo="897815.png", [COLOR orangered]blizz TV HD[/COLOR] 94 | 95 | String id = null; 96 | String displayName = null; 97 | String displayNumber = null; 98 | int originalNetworkId = 0; 99 | XmlTvParser.XmlTvIcon icon = null; 100 | 101 | String[] parts = line.split(", ", 2); 102 | if (parts.length == 2) { 103 | for (String part : parts[0].split(" ")) { 104 | if (part.startsWith("#EXTINF:")) { 105 | displayNumber = part.substring(8).replaceAll("^0+", ""); 106 | originalNetworkId = Integer.parseInt(displayNumber); 107 | } else if (part.startsWith("tvg-id=")) { 108 | int end = part.indexOf("\"", 8); 109 | if (end > 8) { 110 | id = part.substring(8, end); 111 | } 112 | } else if (part.startsWith("tvg-logo=")) { 113 | int end = part.indexOf("\"", 10); 114 | if (end > 10) { 115 | icon = new XmlTvParser.XmlTvIcon("http://logo.iptv.ink/" 116 | + part.substring(10, end)); 117 | } 118 | } 119 | } 120 | displayName = parts[1].replaceAll("\\[\\/?COLOR[^\\]]*\\]", ""); 121 | } 122 | 123 | if (originalNetworkId != 0 && displayName != null) { 124 | XmlTvParser.XmlTvChannel channel = 125 | new XmlTvParser.XmlTvChannel(id, displayName, displayNumber, icon, 126 | originalNetworkId, 0, 0, false); 127 | if (channelMap.containsKey(originalNetworkId)) { 128 | channels.set(channelMap.get(originalNetworkId), channel); 129 | } else { 130 | channelMap.put(originalNetworkId, channels.size()); 131 | channels.add(channel); 132 | } 133 | } 134 | } else if (line.startsWith("http") && channels.size() > 0) { 135 | channels.get(channels.size()-1).url = line; 136 | } 137 | } 138 | return new XmlTvParser.TvListing(channels, programs); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/util/RendererUtil.java: -------------------------------------------------------------------------------- 1 | package at.pansy.iptv.util; 2 | 3 | import android.content.Context; 4 | ; 5 | import com.google.android.exoplayer.upstream.DefaultHttpDataSource; 6 | import com.google.android.exoplayer.upstream.DefaultUriDataSource; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by notz. 13 | */ 14 | public class RendererUtil { 15 | 16 | public static String processUrlParameters(String url, HashMap httpHeaders) { 17 | String[] parameters = url.split("\\|"); 18 | for (int i = 1; i < parameters.length; i++) { 19 | String[] pair = parameters[i].split("=", 2); 20 | if (pair.length == 2) { 21 | httpHeaders.put(pair[0], pair[1]); 22 | } 23 | } 24 | 25 | return parameters[0]; 26 | } 27 | 28 | public static DefaultUriDataSource createDefaultUriDataSource(Context context, String userAgent, 29 | HashMap httpHeaders) { 30 | 31 | DefaultHttpDataSource httpDataSource = new DefaultHttpDataSource(userAgent, null, null, 32 | DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, 33 | DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false); 34 | 35 | for (Map.Entry header : httpHeaders.entrySet()) { 36 | httpDataSource.setRequestProperty(header.getKey(), header.getValue()); 37 | } 38 | 39 | return new DefaultUriDataSource(context, null, httpDataSource); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/util/SyncUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.util; 18 | 19 | import android.accounts.Account; 20 | import android.accounts.AccountManager; 21 | import android.content.ContentResolver; 22 | import android.content.Context; 23 | import android.media.tv.TvContract; 24 | import android.os.Bundle; 25 | import android.util.Log; 26 | 27 | import at.pansy.iptv.service.AccountService; 28 | import at.pansy.iptv.sync.SyncAdapter; 29 | 30 | /** 31 | * Static helper methods for working with the SyncAdapter framework. 32 | */ 33 | public class SyncUtil { 34 | 35 | public static final String ACCOUNT_TYPE = "at.pansy.iptv.account"; 36 | 37 | private static final String TAG = "SyncUtil"; 38 | private static final String CONTENT_AUTHORITY = TvContract.AUTHORITY; 39 | 40 | public static void setUpPeriodicSync(Context context, String inputId) { 41 | Account account = AccountService.getAccount(ACCOUNT_TYPE); 42 | AccountManager accountManager = 43 | (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); 44 | if (!accountManager.addAccountExplicitly(account, null, null)) { 45 | Log.e(TAG, "Account already exists."); 46 | } 47 | ContentResolver.setIsSyncable(account, CONTENT_AUTHORITY, 1); 48 | ContentResolver.setSyncAutomatically(account, CONTENT_AUTHORITY, true); 49 | Bundle bundle = new Bundle(); 50 | bundle.putString(SyncAdapter.BUNDLE_KEY_INPUT_ID, inputId); 51 | ContentResolver.addPeriodicSync(account, CONTENT_AUTHORITY, bundle, 52 | SyncAdapter.FULL_SYNC_FREQUENCY_SEC); 53 | } 54 | 55 | public static void requestSync(String inputId, boolean currentProgramOnly) { 56 | Bundle bundle = new Bundle(); 57 | bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 58 | bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 59 | bundle.putString(SyncAdapter.BUNDLE_KEY_INPUT_ID, inputId); 60 | bundle.putBoolean(SyncAdapter.BUNDLE_KEY_CURRENT_PROGRAM_ONLY, currentProgramOnly); 61 | ContentResolver.requestSync(AccountService.getAccount(ACCOUNT_TYPE), CONTENT_AUTHORITY, 62 | bundle); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/util/TvContractUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.util; 18 | 19 | import android.content.ContentResolver; 20 | import android.content.ContentValues; 21 | import android.content.Context; 22 | import android.database.Cursor; 23 | import android.media.tv.TvContentRating; 24 | import android.media.tv.TvContract; 25 | import android.media.tv.TvContract.Channels; 26 | import android.media.tv.TvContract.Programs; 27 | import android.net.Uri; 28 | import android.os.AsyncTask; 29 | import android.text.TextUtils; 30 | import android.util.Log; 31 | import android.util.LongSparseArray; 32 | import android.util.Pair; 33 | import android.util.SparseArray; 34 | 35 | import java.io.IOException; 36 | import java.io.InputStream; 37 | import java.io.OutputStream; 38 | import java.net.MalformedURLException; 39 | import java.net.URL; 40 | import java.util.ArrayList; 41 | import java.util.HashMap; 42 | import java.util.List; 43 | import java.util.Map; 44 | 45 | import at.pansy.iptv.domain.Channel; 46 | import at.pansy.iptv.domain.PlaybackInfo; 47 | import at.pansy.iptv.domain.Program; 48 | import at.pansy.iptv.xmltv.XmlTvParser; 49 | 50 | /** 51 | * Static helper methods for working with {@link TvContract}. 52 | */ 53 | public class TvContractUtil { 54 | private static final String TAG = "TvContractUtils"; 55 | private static final boolean DEBUG = true; 56 | 57 | private static final SparseArray VIDEO_HEIGHT_TO_FORMAT_MAP = new SparseArray<>(); 58 | 59 | static { 60 | VIDEO_HEIGHT_TO_FORMAT_MAP.put(480, Channels.VIDEO_FORMAT_480P); 61 | VIDEO_HEIGHT_TO_FORMAT_MAP.put(576, Channels.VIDEO_FORMAT_576P); 62 | VIDEO_HEIGHT_TO_FORMAT_MAP.put(720, Channels.VIDEO_FORMAT_720P); 63 | VIDEO_HEIGHT_TO_FORMAT_MAP.put(1080, Channels.VIDEO_FORMAT_1080P); 64 | VIDEO_HEIGHT_TO_FORMAT_MAP.put(2160, Channels.VIDEO_FORMAT_2160P); 65 | VIDEO_HEIGHT_TO_FORMAT_MAP.put(4320, Channels.VIDEO_FORMAT_4320P); 66 | } 67 | 68 | private TvContractUtil() {} 69 | 70 | public static void updateChannels( 71 | Context context, String inputId, List channels) { 72 | // Create a map from original network ID to channel row ID for existing channels. 73 | SparseArray mExistingChannelsMap = new SparseArray<>(); 74 | Uri channelsUri = TvContract.buildChannelsUriForInput(inputId); 75 | String[] projection = {Channels._ID, Channels.COLUMN_ORIGINAL_NETWORK_ID}; 76 | Cursor cursor = null; 77 | ContentResolver resolver = context.getContentResolver(); 78 | try { 79 | cursor = resolver.query(channelsUri, projection, null, null, null); 80 | while (cursor != null && cursor.moveToNext()) { 81 | long rowId = cursor.getLong(0); 82 | int originalNetworkId = cursor.getInt(1); 83 | mExistingChannelsMap.put(originalNetworkId, rowId); 84 | } 85 | } finally { 86 | if (cursor != null) { 87 | cursor.close(); 88 | } 89 | } 90 | 91 | // If a channel exists, update it. If not, insert a new one. 92 | ContentValues values = new ContentValues(); 93 | values.put(Channels.COLUMN_INPUT_ID, inputId); 94 | Map logos = new HashMap<>(); 95 | for (XmlTvParser.XmlTvChannel channel : channels) { 96 | values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.displayNumber); 97 | values.put(Channels.COLUMN_DISPLAY_NAME, channel.displayName); 98 | values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId); 99 | values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId); 100 | values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId); 101 | values.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.url); 102 | Long rowId = mExistingChannelsMap.get(channel.originalNetworkId); 103 | Uri uri; 104 | if (rowId == null) { 105 | uri = resolver.insert(Channels.CONTENT_URI, values); 106 | } else { 107 | uri = TvContract.buildChannelUri(rowId); 108 | resolver.update(uri, values, null, null); 109 | mExistingChannelsMap.remove(channel.originalNetworkId); 110 | } 111 | if (!TextUtils.isEmpty(channel.icon.src)) { 112 | logos.put(TvContract.buildChannelLogoUri(uri), channel.icon.src); 113 | } 114 | } 115 | if (!logos.isEmpty()) { 116 | new InsertLogosTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, logos); 117 | } 118 | 119 | // Deletes channels which don't exist in the new feed. 120 | int size = mExistingChannelsMap.size(); 121 | for (int i = 0; i < size; i++) { 122 | Long rowId = mExistingChannelsMap.valueAt(i); 123 | resolver.delete(TvContract.buildChannelUri(rowId), null, null); 124 | } 125 | } 126 | 127 | private static String getVideoFormat(int videoHeight) { 128 | return VIDEO_HEIGHT_TO_FORMAT_MAP.get(videoHeight); 129 | } 130 | 131 | public static LongSparseArray buildChannelMap( 132 | ContentResolver resolver, String inputId, List channels) { 133 | Uri uri = TvContract.buildChannelsUriForInput(inputId); 134 | String[] projection = { 135 | Channels._ID, 136 | Channels.COLUMN_DISPLAY_NUMBER 137 | }; 138 | 139 | LongSparseArray channelMap = new LongSparseArray<>(); 140 | Cursor cursor = null; 141 | try { 142 | cursor = resolver.query(uri, projection, null, null, null); 143 | if (cursor == null || cursor.getCount() == 0) { 144 | return null; 145 | } 146 | 147 | while (cursor.moveToNext()) { 148 | long channelId = cursor.getLong(0); 149 | String channelNumber = cursor.getString(1); 150 | channelMap.put(channelId, getChannelByNumber(channelNumber, channels)); 151 | } 152 | } catch (Exception e) { 153 | Log.d(TAG, "Content provider query: " + e.getStackTrace()); 154 | return null; 155 | } finally { 156 | if (cursor != null) { 157 | cursor.close(); 158 | } 159 | } 160 | return channelMap; 161 | } 162 | 163 | public static Channel getChannel(ContentResolver resolver, Uri channelUri) { 164 | Cursor cursor = null; 165 | try { 166 | // TvProvider returns programs chronological order by default. 167 | cursor = resolver.query(channelUri, null, null, null, null); 168 | if (cursor == null || cursor.getCount() == 0) { 169 | return null; 170 | } 171 | if (cursor.moveToNext()) { 172 | return Channel.fromCursor(cursor); 173 | } 174 | } catch (Exception e) { 175 | Log.w(TAG, "Unable to get channel for " + channelUri, e); 176 | } finally { 177 | if (cursor != null) { 178 | cursor.close(); 179 | } 180 | } 181 | return null; 182 | } 183 | 184 | public static List getPrograms(ContentResolver resolver, Uri channelUri) { 185 | Uri uri = TvContract.buildProgramsUriForChannel(channelUri); 186 | Cursor cursor = null; 187 | List programs = new ArrayList<>(); 188 | try { 189 | // TvProvider returns programs chronological order by default. 190 | cursor = resolver.query(uri, null, null, null, null); 191 | if (cursor == null || cursor.getCount() == 0) { 192 | return programs; 193 | } 194 | while (cursor.moveToNext()) { 195 | programs.add(Program.fromCursor(cursor)); 196 | } 197 | } catch (Exception e) { 198 | Log.w(TAG, "Unable to get programs for " + channelUri, e); 199 | } finally { 200 | if (cursor != null) { 201 | cursor.close(); 202 | } 203 | } 204 | return programs; 205 | } 206 | 207 | public static List getProgramPlaybackInfo( 208 | ContentResolver resolver, Uri channelUri, long startTimeMs, long endTimeMs, 209 | int maxProgramInReturn) { 210 | Uri uri = TvContract.buildProgramsUriForChannel(channelUri, startTimeMs, 211 | endTimeMs); 212 | String[] projection = { Programs.COLUMN_START_TIME_UTC_MILLIS, 213 | Programs.COLUMN_END_TIME_UTC_MILLIS, 214 | Programs.COLUMN_CONTENT_RATING, 215 | Programs.COLUMN_INTERNAL_PROVIDER_DATA, 216 | Programs.COLUMN_CANONICAL_GENRE }; 217 | Cursor cursor = null; 218 | List list = new ArrayList<>(); 219 | try { 220 | cursor = resolver.query(uri, projection, null, null, null); 221 | while (cursor != null && cursor.moveToNext()) { 222 | long startMs = cursor.getLong(0); 223 | long endMs = cursor.getLong(1); 224 | TvContentRating[] ratings = stringToContentRatings(cursor.getString(2)); 225 | Pair values = parseInternalProviderData(cursor.getString(3)); 226 | int videoType = values.first; 227 | String videoUrl = values.second; 228 | list.add(new PlaybackInfo(startMs, endMs, videoUrl, videoType, ratings)); 229 | if (list.size() > maxProgramInReturn) { 230 | break; 231 | } 232 | } 233 | } catch (Exception e) { 234 | Log.e(TAG, "Failed to get program playback info from TvProvider.", e); 235 | } finally { 236 | if (cursor != null) { 237 | cursor.close(); 238 | } 239 | } 240 | return list; 241 | } 242 | 243 | public static String convertVideoInfoToInternalProviderData(int videotype, String videoUrl) { 244 | return videotype + "," + videoUrl; 245 | } 246 | 247 | public static Pair parseInternalProviderData(String internalData) { 248 | String[] values = internalData.split(",", 2); 249 | if (values.length != 2) { 250 | throw new IllegalArgumentException(internalData); 251 | } 252 | return new Pair<>(Integer.parseInt(values[0]), values[1]); 253 | } 254 | 255 | public static void insertUrl(Context context, Uri contentUri, URL sourceUrl) { 256 | if (DEBUG) { 257 | Log.d(TAG, "Inserting " + sourceUrl + " to " + contentUri); 258 | } 259 | InputStream is = null; 260 | OutputStream os = null; 261 | try { 262 | is = sourceUrl.openStream(); 263 | os = context.getContentResolver().openOutputStream(contentUri); 264 | copy(is, os); 265 | } catch (IOException ioe) { 266 | Log.e(TAG, "Failed to write " + sourceUrl + " to " + contentUri, ioe); 267 | } finally { 268 | if (is != null) { 269 | try { 270 | is.close(); 271 | } catch (IOException e) { 272 | // Ignore exception. 273 | } 274 | } 275 | if (os != null) { 276 | try { 277 | os.close(); 278 | } catch (IOException e) { 279 | // Ignore exception. 280 | } 281 | } 282 | } 283 | } 284 | 285 | public static void copy(InputStream is, OutputStream os) throws IOException { 286 | byte[] buffer = new byte[1024]; 287 | int len; 288 | while ((len = is.read(buffer)) != -1) { 289 | os.write(buffer, 0, len); 290 | } 291 | } 292 | 293 | public static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) { 294 | if (TextUtils.isEmpty(commaSeparatedRatings)) { 295 | return null; 296 | } 297 | String[] ratings = commaSeparatedRatings.split("\\s*,\\s*"); 298 | TvContentRating[] contentRatings = new TvContentRating[ratings.length]; 299 | for (int i = 0; i < contentRatings.length; ++i) { 300 | contentRatings[i] = TvContentRating.unflattenFromString(ratings[i]); 301 | } 302 | return contentRatings; 303 | } 304 | 305 | public static String contentRatingsToString(TvContentRating[] contentRatings) { 306 | if (contentRatings == null || contentRatings.length == 0) { 307 | return null; 308 | } 309 | final String DELIMITER = ","; 310 | StringBuilder ratings = new StringBuilder(contentRatings[0].flattenToString()); 311 | for (int i = 1; i < contentRatings.length; ++i) { 312 | ratings.append(DELIMITER); 313 | ratings.append(contentRatings[i].flattenToString()); 314 | } 315 | return ratings.toString(); 316 | } 317 | 318 | private static XmlTvParser.XmlTvChannel getChannelByNumber(String channelNumber, 319 | List channels) { 320 | for (XmlTvParser.XmlTvChannel channel : channels) { 321 | if (channelNumber.equals(channel.displayNumber)) { 322 | return channel; 323 | } 324 | } 325 | throw new IllegalArgumentException("Unknown channel: " + channelNumber); 326 | } 327 | 328 | public static class InsertLogosTask extends AsyncTask, Void, Void> { 329 | private final Context context; 330 | 331 | InsertLogosTask(Context context) { 332 | this.context = context; 333 | } 334 | 335 | @Override 336 | public Void doInBackground(Map... logosList) { 337 | for (Map logos : logosList) { 338 | for (Uri uri : logos.keySet()) { 339 | try { 340 | insertUrl(context, uri, new URL(logos.get(uri))); 341 | } catch (MalformedURLException e) { 342 | Log.e(TAG, "Can't load " + logos.get(uri), e); 343 | } 344 | } 345 | } 346 | return null; 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /app/src/main/java/at/pansy/iptv/xmltv/XmlTvParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package at.pansy.iptv.xmltv; 18 | 19 | import android.media.tv.TvContentRating; 20 | import android.text.TextUtils; 21 | import android.util.Xml; 22 | 23 | import com.google.android.exoplayer.ParserException; 24 | 25 | import org.xmlpull.v1.XmlPullParser; 26 | import org.xmlpull.v1.XmlPullParserException; 27 | 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.text.ParseException; 31 | import java.text.SimpleDateFormat; 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | import at.pansy.iptv.domain.PlaybackInfo; 36 | 37 | /** 38 | * XMLTV document parser which conforms to http://wiki.xmltv.org/index.php/Main_Page 39 | * 40 | *

Please note that xmltv.dtd are extended to be align with Android TV Input Framework and 41 | * contain static video contents: 42 | * 43 | * 44 | * 47 | * 50 | * video-type CDATA #IMPLIED > 51 | * 52 | * display-number : The channel number that is displayed to the user. 53 | * repeat-programs : If "true", the programs in the xml document are scheduled sequentially in a 54 | * loop regardless of their start and end time. This is introduced to simulate a live 55 | * channel in this sample. 56 | * video-src : The video URL for the given program. This can be omitted if the xml will be used 57 | * only for the program guide update. 58 | * video-type : The video type. Should be one of "HTTP_PROGRESSIVE", "HLS", and "MPEG-DASH". This 59 | * can be omitted if the xml will be used only for the program guide update. 60 | */ 61 | public class XmlTvParser { 62 | private static final String TAG_TV = "tv"; 63 | private static final String TAG_CHANNEL = "channel"; 64 | private static final String TAG_DISPLAY_NAME = "display-name"; 65 | private static final String TAG_ICON = "icon"; 66 | private static final String TAG_PROGRAM = "programme"; 67 | private static final String TAG_TITLE = "title"; 68 | private static final String TAG_DESC = "desc"; 69 | private static final String TAG_CATEGORY = "category"; 70 | private static final String TAG_RATING = "rating"; 71 | private static final String TAG_VALUE = "value"; 72 | private static final String TAG_DISPLAY_NUMBER = "display-number"; 73 | 74 | private static final String ATTR_ID = "id"; 75 | private static final String ATTR_START = "start"; 76 | private static final String ATTR_STOP = "stop"; 77 | private static final String ATTR_CHANNEL = "channel"; 78 | private static final String ATTR_SYSTEM = "system"; 79 | private static final String ATTR_SRC = "src"; 80 | private static final String ATTR_REPEAT_PROGRAMS = "repeat-programs"; 81 | private static final String ATTR_VIDEO_SRC = "video-src"; 82 | private static final String ATTR_VIDEO_TYPE = "video-type"; 83 | 84 | private static final String VALUE_VIDEO_TYPE_HTTP_PROGRESSIVE = "HTTP_PROGRESSIVE"; 85 | private static final String VALUE_VIDEO_TYPE_HLS = "HLS"; 86 | private static final String VALUE_VIDEO_TYPE_MPEG_DASH = "MPEG_DASH"; 87 | 88 | private static final String ANDROID_TV_RATING = "com.android.tv"; 89 | 90 | private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss Z"); 91 | 92 | private XmlTvParser() { 93 | } 94 | 95 | public static TvContentRating[] xmlTvRatingToTvContentRating( 96 | XmlTvRating[] ratings) { 97 | List list = new ArrayList<>(); 98 | for (XmlTvRating rating : ratings) { 99 | if (ANDROID_TV_RATING.equals(rating.system)) { 100 | list.add(TvContentRating.unflattenFromString(rating.value)); 101 | } 102 | } 103 | return list.toArray(new TvContentRating[list.size()]); 104 | } 105 | 106 | public static TvListing parse(InputStream inputStream) { 107 | try { 108 | XmlPullParser parser = Xml.newPullParser(); 109 | parser.setInput(inputStream, null); 110 | int eventType = parser.next(); 111 | if (eventType != XmlPullParser.START_TAG || !TAG_TV.equals(parser.getName())) { 112 | throw new ParserException( 113 | "inputStream does not contain a xml tv description"); 114 | } 115 | return parseTvListings(parser); 116 | } catch (XmlPullParserException | IOException | ParseException e) { 117 | e.printStackTrace(); 118 | } 119 | return null; 120 | } 121 | 122 | private static TvListing parseTvListings(XmlPullParser parser) 123 | throws IOException, XmlPullParserException, ParseException { 124 | List channels = new ArrayList<>(); 125 | List programs = new ArrayList<>(); 126 | while (parser.next() != XmlPullParser.END_DOCUMENT) { 127 | if (parser.getEventType() == XmlPullParser.START_TAG 128 | && TAG_CHANNEL.equalsIgnoreCase(parser.getName())) { 129 | channels.add(parseChannel(parser)); 130 | } 131 | if (parser.getEventType() == XmlPullParser.START_TAG 132 | && TAG_PROGRAM.equalsIgnoreCase(parser.getName())) { 133 | programs.add(parseProgram(parser)); 134 | } 135 | } 136 | return new TvListing(channels, programs); 137 | } 138 | 139 | private static XmlTvChannel parseChannel(XmlPullParser parser) 140 | throws IOException, XmlPullParserException { 141 | String id = null; 142 | boolean repeatPrograms = false; 143 | for (int i = 0; i < parser.getAttributeCount(); ++i) { 144 | String attr = parser.getAttributeName(i); 145 | String value = parser.getAttributeValue(i); 146 | if (ATTR_ID.equalsIgnoreCase(attr)) { 147 | id = value; 148 | } else if (ATTR_REPEAT_PROGRAMS.equalsIgnoreCase(attr)) { 149 | repeatPrograms = "TRUE".equalsIgnoreCase(value); 150 | } 151 | } 152 | String displayName = null; 153 | String displayNumber = null; 154 | XmlTvIcon icon = null; 155 | while (parser.next() != XmlPullParser.END_DOCUMENT) { 156 | if (parser.getEventType() == XmlPullParser.START_TAG) { 157 | if (TAG_DISPLAY_NAME.equalsIgnoreCase(parser.getName()) 158 | && displayName == null) { 159 | // TODO: support multiple display names. 160 | displayName = parser.nextText(); 161 | } else if (TAG_DISPLAY_NUMBER.equalsIgnoreCase(parser.getName()) 162 | && displayNumber == null) { 163 | displayNumber = parser.nextText(); 164 | } else if (TAG_ICON.equalsIgnoreCase(parser.getName()) && icon == null) { 165 | icon = parseIcon(parser); 166 | } 167 | } else if (TAG_CHANNEL.equalsIgnoreCase(parser.getName()) 168 | && parser.getEventType() == XmlPullParser.END_TAG) { 169 | break; 170 | } 171 | } 172 | if (TextUtils.isEmpty(id) || TextUtils.isEmpty(displayName)) { 173 | throw new IllegalArgumentException("id and display-name can not be null."); 174 | } 175 | 176 | // Developers should assign original network ID in the right way not using the fake ID. 177 | int fakeOriginalNetworkId = (displayNumber + displayName).hashCode(); 178 | return new XmlTvChannel(id, displayName, displayNumber, icon, fakeOriginalNetworkId, 0, 0, 179 | repeatPrograms); 180 | } 181 | 182 | private static XmlTvProgram parseProgram(XmlPullParser parser) 183 | throws IOException, XmlPullParserException, ParseException { 184 | String channelId = null; 185 | Long startTimeUtcMillis = null; 186 | Long endTimeUtcMillis = null; 187 | String videoSrc = null; 188 | int videoType = PlaybackInfo.VIDEO_TYPE_HLS; 189 | for (int i = 0; i < parser.getAttributeCount(); ++i) { 190 | String attr = parser.getAttributeName(i); 191 | String value = parser.getAttributeValue(i); 192 | if (ATTR_CHANNEL.equalsIgnoreCase(attr)) { 193 | channelId = value; 194 | } else if (ATTR_START.equalsIgnoreCase(attr)) { 195 | startTimeUtcMillis = DATE_FORMAT.parse(value).getTime(); 196 | } else if (ATTR_STOP.equalsIgnoreCase(attr)) { 197 | endTimeUtcMillis = DATE_FORMAT.parse(value).getTime(); 198 | } else if (ATTR_VIDEO_SRC.equalsIgnoreCase(attr)) { 199 | videoSrc = value; 200 | } else if (ATTR_VIDEO_TYPE.equalsIgnoreCase(attr)) { 201 | if (VALUE_VIDEO_TYPE_HTTP_PROGRESSIVE.equals(value)) { 202 | videoType = PlaybackInfo.VIDEO_TYPE_HTTP_PROGRESSIVE; 203 | } else if (VALUE_VIDEO_TYPE_HLS.equals(value)) { 204 | videoType = PlaybackInfo.VIDEO_TYPE_HLS; 205 | } else if (VALUE_VIDEO_TYPE_MPEG_DASH.equals(value)) { 206 | videoType = PlaybackInfo.VIDEO_TYPE_MPEG_DASH; 207 | } 208 | } 209 | } 210 | String title = null; 211 | String description = null; 212 | XmlTvIcon icon = null; 213 | List category = new ArrayList<>(); 214 | List rating = new ArrayList<>(); 215 | while (parser.next() != XmlPullParser.END_DOCUMENT) { 216 | String tagName = parser.getName(); 217 | if (parser.getEventType() == XmlPullParser.START_TAG) { 218 | if (TAG_TITLE.equalsIgnoreCase(parser.getName())) { 219 | title = parser.nextText(); 220 | } else if (TAG_DESC.equalsIgnoreCase(tagName)) { 221 | description = parser.nextText(); 222 | } else if (TAG_ICON.equalsIgnoreCase(tagName)) { 223 | icon = parseIcon(parser); 224 | } else if (TAG_CATEGORY.equalsIgnoreCase(tagName)) { 225 | category.add(parser.nextText()); 226 | } else if (TAG_RATING.equalsIgnoreCase(tagName)) { 227 | try { 228 | rating.add(parseRating(parser)); 229 | } catch (IllegalArgumentException e) { 230 | // do not add wrong rating values 231 | } 232 | } 233 | } else if (TAG_PROGRAM.equalsIgnoreCase(tagName) 234 | && parser.getEventType() == XmlPullParser.END_TAG) { 235 | break; 236 | } 237 | } 238 | if (TextUtils.isEmpty(channelId) || startTimeUtcMillis == null 239 | || endTimeUtcMillis == null) { 240 | throw new IllegalArgumentException("channel, start, and end can not be null."); 241 | } 242 | return new XmlTvProgram(channelId, title, description, icon, 243 | category.toArray(new String[category.size()]), startTimeUtcMillis, endTimeUtcMillis, 244 | rating.toArray(new XmlTvRating[rating.size()]), videoSrc, videoType); 245 | } 246 | 247 | private static XmlTvIcon parseIcon(XmlPullParser parser) 248 | throws IOException, XmlPullParserException { 249 | String src = null; 250 | for (int i = 0; i < parser.getAttributeCount(); ++i) { 251 | String attr = parser.getAttributeName(i); 252 | String value = parser.getAttributeValue(i); 253 | if (ATTR_SRC.equalsIgnoreCase(attr)) { 254 | src = value; 255 | } 256 | } 257 | while (parser.next() != XmlPullParser.END_DOCUMENT) { 258 | if (TAG_ICON.equalsIgnoreCase(parser.getName()) 259 | && parser.getEventType() == XmlPullParser.END_TAG) { 260 | break; 261 | } 262 | } 263 | if (TextUtils.isEmpty(src)) { 264 | throw new IllegalArgumentException("src cannot be null."); 265 | } 266 | return new XmlTvIcon(src); 267 | } 268 | 269 | private static XmlTvRating parseRating(XmlPullParser parser) 270 | throws IOException, XmlPullParserException { 271 | String system = null; 272 | for (int i = 0; i < parser.getAttributeCount(); ++i) { 273 | String attr = parser.getAttributeName(i); 274 | String value = parser.getAttributeValue(i); 275 | if (ATTR_SYSTEM.equalsIgnoreCase(attr)) { 276 | system = value; 277 | } 278 | } 279 | String value = null; 280 | while (parser.next() != XmlPullParser.END_DOCUMENT) { 281 | if (parser.getEventType() == XmlPullParser.START_TAG) { 282 | if (TAG_VALUE.equalsIgnoreCase(parser.getName())) { 283 | value = parser.nextText(); 284 | } 285 | } else if (TAG_RATING.equalsIgnoreCase(parser.getName()) 286 | && parser.getEventType() == XmlPullParser.END_TAG) { 287 | break; 288 | } 289 | } 290 | if (TextUtils.isEmpty(system) || TextUtils.isEmpty(value)) { 291 | throw new IllegalArgumentException("system and value cannot be null."); 292 | } 293 | return new XmlTvRating(system, value); 294 | } 295 | 296 | public static class TvListing { 297 | public List channels; 298 | public final List programs; 299 | 300 | public TvListing(List channels, List programs) { 301 | this.channels = channels; 302 | this.programs = programs; 303 | } 304 | 305 | public void setChannels(List channels) { 306 | this.channels = channels; 307 | } 308 | } 309 | 310 | public static class XmlTvChannel { 311 | public final String id; 312 | public final String displayName; 313 | public final String displayNumber; 314 | public final XmlTvIcon icon; 315 | public final int originalNetworkId; 316 | public final int transportStreamId; 317 | public final int serviceId; 318 | public final boolean repeatPrograms; 319 | public String url; 320 | 321 | public XmlTvChannel(String id, String displayName, String displayNumber, XmlTvIcon icon, 322 | int originalNetworkId, int transportStreamId, int serviceId, 323 | boolean repeatPrograms) { 324 | this(id, displayName, displayNumber, icon, originalNetworkId, transportStreamId, 325 | serviceId, repeatPrograms, null); 326 | } 327 | 328 | public XmlTvChannel(String id, String displayName, String displayNumber, XmlTvIcon icon, 329 | int originalNetworkId, int transportStreamId, int serviceId, 330 | boolean repeatPrograms, String url) { 331 | this.id = id; 332 | this.displayName = displayName; 333 | this.displayNumber = displayNumber; 334 | this.icon = icon; 335 | this.originalNetworkId = originalNetworkId; 336 | this.transportStreamId = transportStreamId; 337 | this.serviceId = serviceId; 338 | this.repeatPrograms = repeatPrograms; 339 | this.url = url; 340 | } 341 | } 342 | 343 | public static class XmlTvProgram { 344 | public final String channelId; 345 | public final String title; 346 | public final String description; 347 | public final XmlTvIcon icon; 348 | public final String[] category; 349 | public final long startTimeUtcMillis; 350 | public final long endTimeUtcMillis; 351 | public final XmlTvRating[] rating; 352 | public final String videoSrc; 353 | public final int videoType; 354 | 355 | private XmlTvProgram(String channelId, String title, String description, XmlTvIcon icon, 356 | String[] category, long startTimeUtcMillis, long endTimeUtcMillis, 357 | XmlTvRating[] rating, String videoSrc, int videoType) { 358 | this.channelId = channelId; 359 | this.title = title; 360 | this.description = description; 361 | this.icon = icon; 362 | this.category = category; 363 | this.startTimeUtcMillis = startTimeUtcMillis; 364 | this.endTimeUtcMillis = endTimeUtcMillis; 365 | this.rating = rating; 366 | this.videoSrc = videoSrc; 367 | this.videoType = videoType; 368 | } 369 | 370 | public long getDurationMillis() { 371 | return endTimeUtcMillis - startTimeUtcMillis; 372 | } 373 | } 374 | 375 | public static class XmlTvIcon { 376 | public final String src; 377 | 378 | public XmlTvIcon(String src) { 379 | this.src = src; 380 | } 381 | } 382 | 383 | public static class XmlTvRating { 384 | public final String system; 385 | public final String value; 386 | 387 | public XmlTvRating(String system, String value) { 388 | this.system = system; 389 | this.value = value; 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/default_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/overlay_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/setup_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notz/iptv-live-channels/ed8b5826aafa1034acfcf010b1f758201aceea31/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #000000 3 | #DDDDDD 4 | #0096a6 5 | #ffaa3f 6 | #ffaa3f 7 | #0096a6 8 | #30000000 9 | #30FF0000 10 | #00000000 11 | #AA000000 12 | #59000000 13 | #FFFFFF 14 | #AAFADCA7 15 | #FADCA7 16 | #EEFF41 17 | #3d3d3d 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | IPTV Live Channels 3 | IPTV Live Channels 4 | 5 | http://tv.iptv.ink/iptv.ink 6 | http://epg.iptv.ink/iptv.epg.gz 7 | http://logo.iptv.ink/ 8 | 9 | Add Channels 10 | Update Channels 11 | Cancel Setup 12 | In Progress… 13 | TV Inputs by Your Company 14 | No feed. Check your connection! 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/authenticator.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/xml/syncadapter.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/xml/tvinputservice.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.3.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------