package com.felkertech.cumulustv.services; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PersistableBundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.felkertech.cumulustv.fileio.CumulusXmlParser; import com.felkertech.cumulustv.model.ChannelDatabase; import com.felkertech.cumulustv.model.JsonChannel; import com.felkertech.cumulustv.tv.activities.PlaybackQuickSettingsActivity; import com.felkertech.cumulustv.utils.AppUtils; import com.felkertech.n.cumulustv.R; import com.google.android.media.tv.companionlibrary.EpgSyncJobService; import com.google.android.media.tv.companionlibrary.XmlTvParser; import com.google.android.media.tv.companionlibrary.model.Advertisement; import com.google.android.media.tv.companionlibrary.model.Channel; import com.google.android.media.tv.companionlibrary.model.InternalProviderData; import com.google.android.media.tv.companionlibrary.model.Program; import com.google.android.media.tv.companionlibrary.utils.TvContractUtils; import junit.framework.Assert; import org.json.JSONException; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * A periodic task that can be run to synchronize data from Google Drive to the system's internal * TIF database and local cache. */ public class CumulusJobService extends EpgSyncJobService { private static final String TAG = CumulusJobService.class.getSimpleName(); private static final String TEST_AD_REQUEST_URL = "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/" + "single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast" + "&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct" + "%3Dlinear&correlator="; private static HashMap<String, CumulusXmlParser.TvListing> epgData; public static final long DEFAULT_IMMEDIATE_EPG_DURATION_MILLIS = 1000 * 60 * 60; // 1 Hour /** * This method needs to be overridden so that we can do asynchronous actions beforehand. */ @Override public boolean onStartJob(final JobParameters params) { // Broadcast status Intent intent = new Intent(ACTION_SYNC_STATUS_CHANGED); intent.putExtra(BUNDLE_KEY_INPUT_ID, params.getExtras().getString(BUNDLE_KEY_INPUT_ID)); Log.d(TAG, "Sync program data for " + params.getExtras().getString(BUNDLE_KEY_INPUT_ID)); intent.putExtra(SYNC_STATUS, SYNC_STARTED); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); if (AppUtils.isTV(getApplicationContext())) { // Epg syncing only happens on TVs new EpgDataSyncThread(this, new EpgDataCallback() { @Override public void onComplete() { Log.d(TAG, "Epg data syncing is complete"); Handler h = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { super.handleMessage(msg); Log.d(TAG, "Run in main thread"); EpgSyncTask epgSyncTask = new EpgSyncTask(params); epgSyncTask.execute(); } }; h.sendEmptyMessage(0); } }).start(); } return true; } @Override public List<Channel> getChannels() { // Build advertisement list for the channel. Advertisement channelAd = new Advertisement.Builder() .setType(Advertisement.TYPE_VAST) .setRequestUrl(TEST_AD_REQUEST_URL) .build(); List<Advertisement> channelAdList = new ArrayList<>(); channelAdList.add(channelAd); InternalProviderData ipd = new InternalProviderData(); // ipd.setAds(channelAdList); ipd.setRepeatable(true); ipd.setVideoType(TvContractUtils.SOURCE_TYPE_HLS); try { List<Channel> channels = ChannelDatabase.getInstance(this).getChannels(ipd); // Add app linking for (int i = 0; i < channels.size(); i++) { JsonChannel jsonChannel = ChannelDatabase.getInstance(this).findChannelByMediaUrl( channels.get(i).getInternalProviderData().getVideoUrl()); Channel channel = new Channel.Builder(channels.get(i)) .setAppLinkText(getString(R.string.quick_settings)) .setAppLinkIconUri("https://github.com/Fleker/CumulusTV/blob/master/app/src/m" + "ain/res/drawable-xhdpi/ic_play_action_normal.png?raw=true") .setAppLinkPosterArtUri(channels.get(i).getChannelLogo()) .setAppLinkIntent(PlaybackQuickSettingsActivity.getIntent(this, jsonChannel)) .build(); Log.d(TAG, "Adding channel " + channel.getDisplayName()); channels.set(i, channel); } Log.d(TAG, "Returning with " + channels.size() + " channels"); return channels; } catch (JSONException e) { e.printStackTrace(); } Log.w(TAG, "No channels found"); return null; } @Override public List<Program> getProgramsForChannel(Uri channelUri, Channel channel, long startMs, long endMs) { List<Program> programs = new ArrayList<>(); ChannelDatabase channelDatabase = ChannelDatabase.getInstance(this); Log.d(TAG, "Get programs for " + channel.toString()); JsonChannel jsonChannel = channelDatabase.findChannelByMediaUrl( channel.getInternalProviderData().getVideoUrl()); if (jsonChannel != null && jsonChannel.getEpgUrl() != null && !jsonChannel.getEpgUrl().isEmpty() && epgData.containsKey(jsonChannel.getEpgUrl())) { List<Program> programForGivenTime = new ArrayList<>(); CumulusXmlParser.TvListing tvListing = epgData.get(jsonChannel.getEpgUrl()); if (tvListing == null) { return programs; // Return empty programs. } List<Program> programList = tvListing.getAllPrograms(); // If repeat-programs is on, schedule the programs sequentially in a loop. To make // every device play the same program in a given channel and time, we assumes the // loop started from the epoch time. long totalDurationMs = 0; for (Program program : programList) { totalDurationMs += program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); } long programStartTimeMs = startMs - startMs % totalDurationMs; int i = 0; final int programCount = programList.size(); while (programStartTimeMs < endMs) { Program currentProgram = programList.get(i++ % programCount); long programEndTimeMs = currentProgram.getEndTimeUtcMillis(); if (programEndTimeMs < startMs) { programStartTimeMs = programEndTimeMs; continue; } programForGivenTime.add(new Program.Builder() .setTitle(currentProgram.getTitle()) .setDescription(currentProgram.getDescription()) .setContentRatings(currentProgram.getContentRatings()) .setCanonicalGenres(currentProgram.getCanonicalGenres()) .setPosterArtUri(currentProgram.getPosterArtUri()) .setThumbnailUri(currentProgram.getThumbnailUri()) .setInternalProviderData(currentProgram.getInternalProviderData()) .setStartTimeUtcMillis(programStartTimeMs) .setEndTimeUtcMillis(programEndTimeMs) .build() ); programStartTimeMs = programEndTimeMs; } return programForGivenTime; } else { programs.add(new Program.Builder() .setInternalProviderData(channel.getInternalProviderData()) .setTitle(channel.getDisplayName() + " Live") .setDescription(getString(R.string.currently_streaming)) .setPosterArtUri(channel.getChannelLogo()) .setThumbnailUri(channel.getChannelLogo()) .setCanonicalGenres(jsonChannel != null ? jsonChannel.getGenres() : null) .setStartTimeUtcMillis(startMs) .setEndTimeUtcMillis(startMs + 1000 * 60 * 60) // 60 minutes .build()); } return programs; } private final class EpgDataSyncThread extends Thread { private Context mContext; private EpgDataCallback mCallback; EpgDataSyncThread(Context context, EpgDataCallback callback) { super(); mContext = context; mCallback = callback; } @Override public void run() { super.run(); epgData = new HashMap<>(); ChannelDatabase cdn; try { cdn = ChannelDatabase.getInstance(mContext); } catch (ChannelDatabase.MalformedChannelDataException e) { return; // Stop execution now. } try { List<JsonChannel> channels = cdn.getJsonChannels(); for (JsonChannel jsonChannel : channels) { if (jsonChannel.getEpgUrl() != null && !jsonChannel.getEpgUrl().isEmpty()) { List<Program> programForGivenTime = new ArrayList<>(); // Load from the EPG url URLConnection urlConnection = null; try { urlConnection = new URL(jsonChannel.getEpgUrl()).openConnection(); urlConnection.setConnectTimeout(1000 * 5); urlConnection.setReadTimeout(1000 * 5); InputStream inputStream = urlConnection.getInputStream(); InputStream epgInputStream = new BufferedInputStream(inputStream); CumulusXmlParser.TvListing tvListing = CumulusXmlParser.parse(epgInputStream); epgData.put(jsonChannel.getEpgUrl(), tvListing); } catch (IOException | CumulusXmlParser.XmlTvParseException e) { e.printStackTrace(); } } } mCallback.onComplete(); } catch (JSONException e) { e.printStackTrace(); } } } private interface EpgDataCallback { void onComplete(); } @Deprecated public static void requestImmediateSync1(Context context, String inputId, long syncDuration, ComponentName jobServiceComponent) { if (jobServiceComponent.getClass().isAssignableFrom(EpgSyncJobService.class)) { throw new IllegalArgumentException("This class does not extend EpgSyncJobService"); } PersistableBundle persistableBundle = new PersistableBundle(); if (Build.VERSION.SDK_INT >= 22) { persistableBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); persistableBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); } persistableBundle.putString(EpgSyncJobService.BUNDLE_KEY_INPUT_ID, inputId); persistableBundle.putLong("bundle_key_sync_period", syncDuration); JobInfo.Builder builder = new JobInfo.Builder(1, jobServiceComponent); JobInfo jobInfo = builder .setExtras(persistableBundle) .setOverrideDeadline(1000) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .build(); scheduleJob(context, jobInfo); Log.d(TAG, "Single job scheduled"); } /** Send the job to JobScheduler. */ private static void scheduleJob(Context context, JobInfo job) { JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); int result = jobScheduler.schedule(job); Assert.assertEquals(result, JobScheduler.RESULT_SUCCESS); Log.d(TAG, "Scheduling result is " + result); } }