• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 com.android.tv.data.epg;
18 
19 import android.content.ContentProviderOperation;
20 import android.content.Context;
21 import android.content.OperationApplicationException;
22 import android.database.Cursor;
23 import android.media.tv.TvContract;
24 import android.media.tv.TvContract.Programs;
25 import android.os.RemoteException;
26 import android.preference.PreferenceManager;
27 import android.support.annotation.WorkerThread;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import com.android.tv.data.Program;
32 
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.concurrent.TimeUnit;
37 
38 /** The helper class for {@link com.android.tv.data.epg.EpgFetcher} */
39 class EpgFetchHelper {
40     private static final String TAG = "EpgFetchHelper";
41     private static final boolean DEBUG = false;
42 
43     private static final long PROGRAM_QUERY_DURATION_MS = TimeUnit.DAYS.toMillis(30);
44     private static final int BATCH_OPERATION_COUNT = 100;
45 
46     // Value: Long
47     private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
48             "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
49     // Value: String
50     private static final String KEY_LAST_LINEUP_ID =
51             "com.android.tv.data.epg.EpgFetcher.LastLineupId";
52 
53     private static long sLastEpgUpdatedTimestamp = -1;
54     private static String sLastLineupId;
55 
EpgFetchHelper()56     private EpgFetchHelper() { }
57 
58     /**
59      * Updates newly fetched EPG data for the given channel to local providers. The method will
60      * compare the broadcasting time and try to match each newly fetched program with old programs
61      * of that channel in the database one by one. It will update the matched old program, or insert
62      * the new program if there is no matching program can be found in the database and at the same
63      * time remove those old programs which conflicts with the inserted one.
64 
65      * @param channelId the target channel ID.
66      * @param fetchedPrograms the newly fetched program data.
67      * @return {@code true} if new program data are successfully updated. Otherwise {@code false}.
68      */
updateEpgData(Context context, long channelId, List<Program> fetchedPrograms)69     static boolean updateEpgData(Context context, long channelId, List<Program> fetchedPrograms) {
70         final int fetchedProgramsCount = fetchedPrograms.size();
71         if (fetchedProgramsCount == 0) {
72             return false;
73         }
74         boolean updated = false;
75         long startTimeMs = System.currentTimeMillis();
76         long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS;
77         List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs);
78         int oldProgramsIndex = 0;
79         int newProgramsIndex = 0;
80 
81         // Compare the new programs with old programs one by one and update/delete the old one
82         // or insert new program if there is no matching program in the database.
83         ArrayList<ContentProviderOperation> ops = new ArrayList<>();
84         while (newProgramsIndex < fetchedProgramsCount) {
85             Program oldProgram = oldProgramsIndex < oldPrograms.size()
86                     ? oldPrograms.get(oldProgramsIndex) : null;
87             Program newProgram = fetchedPrograms.get(newProgramsIndex);
88             boolean addNewProgram = false;
89             if (oldProgram != null) {
90                 if (oldProgram.equals(newProgram)) {
91                     // Exact match. No need to update. Move on to the next programs.
92                     oldProgramsIndex++;
93                     newProgramsIndex++;
94                 } else if (hasSameTitleAndOverlap(oldProgram, newProgram)) {
95                     // Partial match. Update the old program with the new one.
96                     // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There
97                     // could be application specific settings which belong to the old program.
98                     ops.add(ContentProviderOperation.newUpdate(
99                             TvContract.buildProgramUri(oldProgram.getId()))
100                             .withValues(Program.toContentValues(newProgram))
101                             .build());
102                     oldProgramsIndex++;
103                     newProgramsIndex++;
104                 } else if (oldProgram.getEndTimeUtcMillis() < newProgram.getEndTimeUtcMillis()) {
105                     // No match. Remove the old program first to see if the next program in
106                     // {@code oldPrograms} partially matches the new program.
107                     ops.add(ContentProviderOperation.newDelete(
108                             TvContract.buildProgramUri(oldProgram.getId()))
109                             .build());
110                     oldProgramsIndex++;
111                 } else {
112                     // No match. The new program does not match any of the old programs. Insert
113                     // it as a new program.
114                     addNewProgram = true;
115                     newProgramsIndex++;
116                 }
117             } else {
118                 // No old programs. Just insert new programs.
119                 addNewProgram = true;
120                 newProgramsIndex++;
121             }
122             if (addNewProgram) {
123                 ops.add(ContentProviderOperation
124                         .newInsert(Programs.CONTENT_URI)
125                         .withValues(Program.toContentValues(newProgram))
126                         .build());
127             }
128             // Throttle the batch operation not to cause TransactionTooLargeException.
129             if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
130                 try {
131                     if (DEBUG) {
132                         int size = ops.size();
133                         Log.d(TAG, "Running " + size + " operations for channel " + channelId);
134                         for (int i = 0; i < size; ++i) {
135                             Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
136                         }
137                     }
138                     context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
139                     updated = true;
140                 } catch (RemoteException | OperationApplicationException e) {
141                     Log.e(TAG, "Failed to insert programs.", e);
142                     return updated;
143                 }
144                 ops.clear();
145             }
146         }
147         if (DEBUG) {
148             Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId);
149         }
150         return updated;
151     }
152 
queryPrograms(Context context, long channelId, long startTimeMs, long endTimeMs)153     private static List<Program> queryPrograms(Context context, long channelId,
154             long startTimeMs, long endTimeMs) {
155         try (Cursor c = context.getContentResolver().query(
156                 TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
157                 Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
158             if (c == null) {
159                 return Collections.emptyList();
160             }
161             ArrayList<Program> programs = new ArrayList<>();
162             while (c.moveToNext()) {
163                 programs.add(Program.fromCursor(c));
164             }
165             return programs;
166         }
167     }
168 
169     /**
170      * Returns {@code true} if the {@code oldProgram} needs to be updated with the
171      * {@code newProgram}.
172      */
hasSameTitleAndOverlap(Program oldProgram, Program newProgram)173     private static boolean hasSameTitleAndOverlap(Program oldProgram, Program newProgram) {
174         // NOTE: Here, we update the old program if it has the same title and overlaps with the
175         // new program. The test logic is just an example and you can modify this. E.g. check
176         // whether the both programs have the same program ID if your EPG supports any ID for
177         // the programs.
178         return TextUtils.equals(oldProgram.getTitle(), newProgram.getTitle())
179                 && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
180                 && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
181     }
182 
183     /**
184      * Sets the last known lineup ID into shared preferences for future usage. If channels are not
185      * re-scanned, EPG fetcher can directly use this value instead of checking the correct lineup ID
186      * every time when it needs to fetch EPG data.
187      */
188     @WorkerThread
setLastLineupId(Context context, String lineupId)189     synchronized static void setLastLineupId(Context context, String lineupId) {
190         if (DEBUG) {
191             if (lineupId == null) {
192                 Log.d(TAG, "Clear stored lineup id: " + sLastLineupId);
193             }
194         }
195         sLastLineupId = lineupId;
196         PreferenceManager.getDefaultSharedPreferences(context).edit()
197                 .putString(KEY_LAST_LINEUP_ID, lineupId).apply();
198     }
199 
200     /**
201      * Gets the last known lineup ID from shared preferences.
202      */
getLastLineupId(Context context)203     synchronized static String getLastLineupId(Context context) {
204         if (sLastLineupId == null) {
205             sLastLineupId = PreferenceManager.getDefaultSharedPreferences(context)
206                     .getString(KEY_LAST_LINEUP_ID, null);
207         }
208         if (DEBUG) Log.d(TAG, "Last lineup is " + sLastLineupId);
209         return sLastLineupId;
210     }
211 
212     /**
213      * Sets the last updated timestamp of EPG data into shared preferences. If the EPG data is not
214      * out-dated, it's not necessary for EPG fetcher to fetch EPG again.
215      */
216     @WorkerThread
setLastEpgUpdatedTimestamp(Context context, long timestamp)217     synchronized static void setLastEpgUpdatedTimestamp(Context context, long timestamp) {
218         sLastEpgUpdatedTimestamp = timestamp;
219         PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(
220                 KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).apply();
221     }
222 
223     /**
224      * Gets the last updated timestamp of EPG data.
225      */
getLastEpgUpdatedTimestamp(Context context)226     synchronized static long getLastEpgUpdatedTimestamp(Context context) {
227         if (sLastEpgUpdatedTimestamp < 0) {
228             sLastEpgUpdatedTimestamp = PreferenceManager.getDefaultSharedPreferences(context)
229                     .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
230         }
231         return sLastEpgUpdatedTimestamp;
232     }
233 }