diff options
Diffstat (limited to 'src/com/android/tv/data/epg/EpgFetcher.java')
-rw-r--r-- | src/com/android/tv/data/epg/EpgFetcher.java | 387 |
1 files changed, 308 insertions, 79 deletions
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index 9ff527d8..3b093b6a 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -16,37 +16,48 @@ package com.android.tv.data.epg; +import android.Manifest; +import android.annotation.SuppressLint; import android.content.ContentProviderOperation; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; +import android.content.pm.PackageManager; import android.database.Cursor; +import android.location.Address; +import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvContract.Programs; +import android.media.tv.TvContract.Programs.Genres; import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager.TvInputCallback; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.preference.PreferenceManager; +import android.support.annotation.MainThread; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.util.Log; -import com.android.tv.Features; import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.InternalDataUtils; +import com.android.tv.data.Lineup; import com.android.tv.data.Program; +import com.android.tv.util.LocationUtils; import com.android.tv.util.RecurringRunner; -import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -61,116 +72,275 @@ public class EpgFetcher { private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4); private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); + private static final long LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10); + private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1); private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30); private static final int BATCH_OPERATION_COUNT = 100; + private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry(); + private static final String CONTENT_RATING_SEPARATOR = ","; + // Value: Long private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP = "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp"; + // Value: String + private static final String KEY_LAST_LINEUP_ID = + "com.android.tv.data.epg.EpgFetcher.LastLineupId"; + + private static EpgFetcher sInstance; private final Context mContext; - private final TvInputManagerHelper mInputHelper; - private final TvInputCallback mInputCallback; - private HandlerThread mHandlerThread; + private final ChannelDataManager mChannelDataManager; + private final EpgReader mEpgReader; private EpgFetcherHandler mHandler; private RecurringRunner mRecurringRunner; + private boolean mStarted; private long mLastEpgTimestamp = -1; + private String mLineupId; + + public static synchronized EpgFetcher getInstance(Context context) { + if (sInstance == null) { + sInstance = new EpgFetcher(context.getApplicationContext()); + } + return sInstance; + } + + /** + * Creates and returns {@link EpgReader}. + */ + public static EpgReader createEpgReader(Context context) { + return new StubEpgReader(context); + } - public EpgFetcher(Context context) { + private EpgFetcher(Context context) { mContext = context; - mInputHelper = TvApplication.getSingletons(mContext).getTvInputManagerHelper(); - mInputCallback = new TvInputCallback() { + mEpgReader = new StubEpgReader(mContext); + mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); + mChannelDataManager.addListener(new ChannelDataManager.Listener() { @Override - public void onInputAdded(String inputId) { - if (Utils.isInternalTvInput(mContext, inputId)) { - mHandler.removeMessages(MSG_FETCH_EPG); - mHandler.sendEmptyMessage(MSG_FETCH_EPG); - } + public void onLoadFinished() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); + handleChannelChanged(); + } + + @Override + public void onChannelListUpdated() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); + handleChannelChanged(); + } + + @Override + public void onChannelBrowsableChanged() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()"); + handleChannelChanged(); + } + }); + } + + private void handleChannelChanged() { + if (mStarted) { + if (needToStop()) { + stop(); } - }; + } else { + start(); + } + } + + private boolean needToStop() { + return !canStart(); + } + + private boolean canStart() { + if (DEBUG) Log.d(TAG, "canStart()"); + boolean hasInternalTunerChannel = false; + for (TvInputInfo input : TvApplication.getSingletons(mContext).getTvInputManagerHelper() + .getTvInputInfos(true, true)) { + String inputId = input.getId(); + if (Utils.isInternalTvInput(mContext, inputId) + && mChannelDataManager.getChannelCountForInput(inputId) > 0) { + hasInternalTunerChannel = true; + break; + } + } + if (!hasInternalTunerChannel) { + if (DEBUG) Log.d(TAG, "No internal tuner channels."); + return false; + } + + if (!TextUtils.isEmpty(getLastLineupId())) { + return true; + } + if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + if (DEBUG) Log.d(TAG, "No permission to check the current location."); + return false; + } + + try { + Address address = LocationUtils.getCurrentAddress(mContext); + if (address != null + && !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { + if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode()); + return false; + } + } catch (SecurityException e) { + Log.w(TAG, "No permission to get the current location", e); + return false; + } catch (IOException e) { + Log.w(TAG, "IO Exception when getting the current location", e); + } + return true; } /** * Starts fetching EPG. */ + @MainThread public void start() { - if (DEBUG) Log.d(TAG, "Request to start fetching EPG."); - if (!Features.FETCH_EPG.isEnabled(mContext)) { + if (DEBUG) Log.d(TAG, "start()"); + if (mStarted) { + if (DEBUG) Log.d(TAG, "EpgFetcher thread already started."); return; } - if (mHandlerThread == null) { - mHandlerThread = new HandlerThread("EpgFetcher"); - mHandlerThread.start(); - mHandler = new EpgFetcherHandler(mHandlerThread.getLooper(), this); - mInputHelper.addCallback(mInputCallback); - mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS, - new Runnable() { - @Override - public void run() { - mHandler.removeMessages(MSG_FETCH_EPG); - mHandler.sendEmptyMessage(MSG_FETCH_EPG); - } - }, null); - mRecurringRunner.start(); + if (!canStart()) { + return; + } + mStarted = true; + if (DEBUG) Log.d(TAG, "Starting EpgFetcher thread."); + HandlerThread handlerThread = new HandlerThread("EpgFetcher"); + handlerThread.start(); + mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this); + mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS, + new EpgRunner(), null); + mRecurringRunner.start(); + if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully."); + } + + /** + * Starts fetching EPG immediately if possible without waiting for the timer. + */ + @MainThread + public void startImmediately() { + start(); + if (mStarted) { + if (DEBUG) Log.d(TAG, "Starting fetcher immediately"); + fetchEpg(); } } /** * Stops fetching EPG. */ + @MainThread public void stop() { - if (mHandlerThread == null) { + if (DEBUG) Log.d(TAG, "stop()"); + if (!mStarted) { return; } + mStarted = false; mRecurringRunner.stop(); mHandler.removeCallbacksAndMessages(null); - mHandler = null; - mHandlerThread.quit(); - mHandlerThread = null; + mHandler.getLooper().quit(); + } + + private void fetchEpg() { + fetchEpg(0); + } + + private void fetchEpg(long delay) { + mHandler.removeMessages(MSG_FETCH_EPG); + mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay); } private void onFetchEpg() { if (DEBUG) Log.d(TAG, "Start fetching EPG."); - // Check for the internal inputs. - boolean hasInternalInput = false; - for (TvInputInfo input : mInputHelper.getTvInputInfos(true, true)) { - if (Utils.isInternalTvInput(mContext, input.getId())) { - hasInternalInput = true; - break; - } - } - if (!hasInternalInput) { - if (DEBUG) Log.d(TAG, "No internal input found."); - return; - } - // Check if EPG reader is available. - EpgReader epgReader = new StubEpgReader(mContext); - if (!epgReader.isAvailable()) { + if (!mEpgReader.isAvailable()) { if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available."); - mHandler.removeMessages(MSG_FETCH_EPG); - mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, EPG_READER_INIT_WAIT_MS); + fetchEpg(EPG_READER_INIT_WAIT_MS); return; } + String lineupId = getLastLineupId(); + if (lineupId == null) { + Address address; + try { + address = LocationUtils.getCurrentAddress(mContext); + } catch (IOException e) { + if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); + fetchEpg(LOCATION_ERROR_WAIT_MS); + return; + } catch (SecurityException e) { + Log.w(TAG, "No permission to get the current location."); + return; + } + if (address == null) { + if (DEBUG) Log.d(TAG, "Null address returned."); + fetchEpg(LOCATION_INIT_WAIT_MS); + return; + } + if (DEBUG) Log.d(TAG, "Current location is " + address); + + lineupId = getLineupForAddress(address); + if (lineupId != null) { + if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address); + setLastLineupId(lineupId); + } else { + if (DEBUG) Log.d(TAG, "No lineup found for " + address); + return; + } + } + // Check the EPG Timestamp. - long epgTimestamp = epgReader.getEpgTimestamp(); + long epgTimestamp = mEpgReader.getEpgTimestamp(); if (epgTimestamp <= getLastUpdatedEpgTimestamp()) { if (DEBUG) Log.d(TAG, "No new EPG."); return; } - List<Channel> channels = epgReader.getChannels(); + boolean updated = false; + List<Channel> channels = mEpgReader.getChannels(lineupId); for (Channel channel : channels) { - List<Program> programs = new ArrayList<>(epgReader.getPrograms(channel.getId())); + List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); Collections.sort(programs); if (DEBUG) { - Log.d(TAG, "Fetching " + programs.size() + " programs for channel " + channel); + Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel); + } + if (updateEpg(channel.getId(), programs)) { + updated = true; } - updateEpg(channel.getId(), programs); } + final boolean epgUpdated = updated; setLastUpdatedEpgTimestamp(epgTimestamp); + mHandler.removeMessages(MSG_FETCH_EPG); + if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); + } + + @Nullable + private String getLineupForAddress(Address address) { + String lineup = null; + if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { + String postalCode = address.getPostalCode(); + if (!TextUtils.isEmpty(postalCode)) { + lineup = getLineupForPostalCode(postalCode); + } + } + return lineup; + } + + @Nullable + private String getLineupForPostalCode(String postalCode) { + List<Lineup> lineups = mEpgReader.getLineups(postalCode); + for (Lineup lineup : lineups) { + // TODO(EPG): handle more than OTA digital + if (lineup.type == Lineup.LINEUP_BROADCAST_DIGITAL) { + if (DEBUG) Log.d(TAG, "Setting lineup to " + lineup.name + "(" + lineup.id + ")"); + return lineup.id; + } + } + return null; } private long getLastUpdatedEpgTimestamp() { @@ -184,18 +354,33 @@ public class EpgFetcher { private void setLastUpdatedEpgTimestamp(long timestamp) { mLastEpgTimestamp = timestamp; PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong( - KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp); + KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit(); + } + + private String getLastLineupId() { + if (mLineupId == null) { + mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext) + .getString(KEY_LAST_LINEUP_ID, null); + } + if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId); + return mLineupId; } - private void updateEpg(long channelId, List<Program> newPrograms) { + private void setLastLineupId(String lineupId) { + mLineupId = lineupId; + PreferenceManager.getDefaultSharedPreferences(mContext).edit() + .putString(KEY_LAST_LINEUP_ID, lineupId).commit(); + } + + private boolean updateEpg(long channelId, List<Program> newPrograms) { final int fetchedProgramsCount = newPrograms.size(); if (fetchedProgramsCount == 0) { - return; + return false; } + boolean updated = false; long startTimeMs = System.currentTimeMillis(); long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION; - List<Program> oldPrograms = queryPrograms(mContext.getContentResolver(), channelId, - startTimeMs, endTimeMs); + List<Program> oldPrograms = queryPrograms(channelId, startTimeMs, endTimeMs); Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null; int oldProgramsIndex = 0; int newProgramsIndex = 0; @@ -224,15 +409,13 @@ public class EpgFetcher { oldProgramsIndex++; newProgramsIndex++; } else if (isSameTitleAndOverlap(oldProgram, newProgram)) { - if (!oldProgram.equals(oldProgram)) { - // Partial match. Update the old program with the new one. - // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There - // could be application specific settings which belong to the old program. - ops.add(ContentProviderOperation.newUpdate( - TvContract.buildProgramUri(oldProgram.getId())) - .withValues(toContentValues(newProgram)) - .build()); - } + // Partial match. Update the old program with the new one. + // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There + // could be application specific settings which belong to the old program. + ops.add(ContentProviderOperation.newUpdate( + TvContract.buildProgramUri(oldProgram.getId())) + .withValues(toContentValues(newProgram)) + .build()); oldProgramsIndex++; newProgramsIndex++; } else if (oldProgram.getEndTimeUtcMillis() @@ -271,25 +454,26 @@ public class EpgFetcher { } } mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + updated = true; } catch (RemoteException | OperationApplicationException e) { Log.e(TAG, "Failed to insert programs.", e); - return; + return updated; } ops.clear(); } } if (DEBUG) { - Log.d(TAG, "Fetched " + fetchedProgramsCount + " programs for channel " + channelId); + Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId); } + return updated; } - private List<Program> queryPrograms(ContentResolver contentResolver, long channelId, - long startTimeMs, long endTimeMs) { + private List<Program> queryPrograms(long channelId, long startTimeMs, long endTimeMs) { try (Cursor c = mContext.getContentResolver().query( TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs), Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) { if (c == null) { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } ArrayList<Program> programs = new ArrayList<>(); while (c.moveToNext()) { @@ -313,18 +497,48 @@ public class EpgFetcher { && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis(); } + @SuppressLint("InlinedApi") + @SuppressWarnings("deprecation") private static ContentValues toContentValues(Program program) { ContentValues values = new ContentValues(); values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); - putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); + if (BuildCompat.isAtLeastN()) { + putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, + program.getSeasonNumber()); + putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, + program.getEpisodeNumber()); + } else { + putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); + putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); + } putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); + putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); + String[] canonicalGenres = program.getCanonicalGenres(); + if (canonicalGenres != null && canonicalGenres.length > 0) { + putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, + Genres.encode(canonicalGenres)); + } else { + putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); + } + TvContentRating[] ratings = program.getContentRatings(); + if (ratings != null && ratings.length > 0) { + StringBuilder sb = new StringBuilder(ratings[0].flattenToString()); + for (int i = 1; i < ratings.length; ++i) { + sb.append(CONTENT_RATING_SEPARATOR); + sb.append(ratings[i].flattenToString()); + } + putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString()); + } else { + putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, ""); + } values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis()); values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); + putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, + InternalDataUtils.serializeInternalProviderData(program)); return values; } @@ -336,6 +550,14 @@ public class EpgFetcher { } } + private static void putValue(ContentValues contentValues, String key, byte[] value) { + if (value == null || value.length == 0) { + contentValues.putNull(key); + } else { + contentValues.put(key, value); + } + } + private static class EpgFetcherHandler extends WeakHandler<EpgFetcher> { public EpgFetcherHandler (@NonNull Looper looper, EpgFetcher ref) { super(looper, ref); @@ -353,4 +575,11 @@ public class EpgFetcher { } } } + + private class EpgRunner implements Runnable { + @Override + public void run() { + fetchEpg(); + } + } } |