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