aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/data/epg/EpgFetchHelper.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/data/epg/EpgFetchHelper.java')
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java233
1 files changed, 233 insertions, 0 deletions
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
new file mode 100644
index 00000000..5693c877
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data.epg;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.data.Program;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** The helper class for {@link com.android.tv.data.epg.EpgFetcher} */
+class EpgFetchHelper {
+ private static final String TAG = "EpgFetchHelper";
+ private static final boolean DEBUG = false;
+
+ private static final long PROGRAM_QUERY_DURATION_MS = TimeUnit.DAYS.toMillis(30);
+ private static final int BATCH_OPERATION_COUNT = 100;
+
+ // 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 long sLastEpgUpdatedTimestamp = -1;
+ private static String sLastLineupId;
+
+ private EpgFetchHelper() { }
+
+ /**
+ * Updates newly fetched EPG data for the given channel to local providers. The method will
+ * compare the broadcasting time and try to match each newly fetched program with old programs
+ * of that channel in the database one by one. It will update the matched old program, or insert
+ * the new program if there is no matching program can be found in the database and at the same
+ * time remove those old programs which conflicts with the inserted one.
+
+ * @param channelId the target channel ID.
+ * @param fetchedPrograms the newly fetched program data.
+ * @return {@code true} if new program data are successfully updated. Otherwise {@code false}.
+ */
+ static boolean updateEpgData(Context context, long channelId, List<Program> fetchedPrograms) {
+ final int fetchedProgramsCount = fetchedPrograms.size();
+ if (fetchedProgramsCount == 0) {
+ return false;
+ }
+ boolean updated = false;
+ long startTimeMs = System.currentTimeMillis();
+ long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS;
+ List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs);
+ int oldProgramsIndex = 0;
+ int newProgramsIndex = 0;
+
+ // Compare the new programs with old programs one by one and update/delete the old one
+ // or insert new program if there is no matching program in the database.
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ while (newProgramsIndex < fetchedProgramsCount) {
+ Program oldProgram = oldProgramsIndex < oldPrograms.size()
+ ? oldPrograms.get(oldProgramsIndex) : null;
+ Program newProgram = fetchedPrograms.get(newProgramsIndex);
+ boolean addNewProgram = false;
+ if (oldProgram != null) {
+ if (oldProgram.equals(newProgram)) {
+ // Exact match. No need to update. Move on to the next programs.
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (hasSameTitleAndOverlap(oldProgram, newProgram)) {
+ // 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(Program.toContentValues(newProgram))
+ .build());
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (oldProgram.getEndTimeUtcMillis() < newProgram.getEndTimeUtcMillis()) {
+ // No match. Remove the old program first to see if the next program in
+ // {@code oldPrograms} partially matches the new program.
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildProgramUri(oldProgram.getId()))
+ .build());
+ oldProgramsIndex++;
+ } else {
+ // No match. The new program does not match any of the old programs. Insert
+ // it as a new program.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ } else {
+ // No old programs. Just insert new programs.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ if (addNewProgram) {
+ ops.add(ContentProviderOperation
+ .newInsert(Programs.CONTENT_URI)
+ .withValues(Program.toContentValues(newProgram))
+ .build());
+ }
+ // Throttle the batch operation not to cause TransactionTooLargeException.
+ if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
+ try {
+ if (DEBUG) {
+ int size = ops.size();
+ Log.d(TAG, "Running " + size + " operations for channel " + channelId);
+ for (int i = 0; i < size; ++i) {
+ Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
+ }
+ }
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ updated = true;
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to insert programs.", e);
+ return updated;
+ }
+ ops.clear();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId);
+ }
+ return updated;
+ }
+
+ private static List<Program> queryPrograms(Context context, long channelId,
+ long startTimeMs, long endTimeMs) {
+ try (Cursor c = context.getContentResolver().query(
+ TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+ Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
+ if (c == null) {
+ return Collections.emptyList();
+ }
+ ArrayList<Program> programs = new ArrayList<>();
+ while (c.moveToNext()) {
+ programs.add(Program.fromCursor(c));
+ }
+ return programs;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@code oldProgram} needs to be updated with the
+ * {@code newProgram}.
+ */
+ private static boolean hasSameTitleAndOverlap(Program oldProgram, Program newProgram) {
+ // NOTE: Here, we update the old program if it has the same title and overlaps with the
+ // new program. The test logic is just an example and you can modify this. E.g. check
+ // whether the both programs have the same program ID if your EPG supports any ID for
+ // the programs.
+ return TextUtils.equals(oldProgram.getTitle(), newProgram.getTitle())
+ && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
+ && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
+ }
+
+ /**
+ * Sets the last known lineup ID into shared preferences for future usage. If channels are not
+ * re-scanned, EPG fetcher can directly use this value instead of checking the correct lineup ID
+ * every time when it needs to fetch EPG data.
+ */
+ @WorkerThread
+ synchronized static void setLastLineupId(Context context, String lineupId) {
+ if (DEBUG) {
+ if (lineupId == null) {
+ Log.d(TAG, "Clear stored lineup id: " + sLastLineupId);
+ }
+ }
+ sLastLineupId = lineupId;
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putString(KEY_LAST_LINEUP_ID, lineupId).apply();
+ }
+
+ /**
+ * Gets the last known lineup ID from shared preferences.
+ */
+ synchronized static String getLastLineupId(Context context) {
+ if (sLastLineupId == null) {
+ sLastLineupId = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(KEY_LAST_LINEUP_ID, null);
+ }
+ if (DEBUG) Log.d(TAG, "Last lineup is " + sLastLineupId);
+ return sLastLineupId;
+ }
+
+ /**
+ * Sets the last updated timestamp of EPG data into shared preferences. If the EPG data is not
+ * out-dated, it's not necessary for EPG fetcher to fetch EPG again.
+ */
+ @WorkerThread
+ synchronized static void setLastEpgUpdatedTimestamp(Context context, long timestamp) {
+ sLastEpgUpdatedTimestamp = timestamp;
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(
+ KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).apply();
+ }
+
+ /**
+ * Gets the last updated timestamp of EPG data.
+ */
+ synchronized static long getLastEpgUpdatedTimestamp(Context context) {
+ if (sLastEpgUpdatedTimestamp < 0) {
+ sLastEpgUpdatedTimestamp = PreferenceManager.getDefaultSharedPreferences(context)
+ .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
+ }
+ return sLastEpgUpdatedTimestamp;
+ }
+} \ No newline at end of file