• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.ContentResolver;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.OperationApplicationException;
24 import android.database.Cursor;
25 import android.media.tv.TvContract;
26 import android.media.tv.TvContract.Programs;
27 import android.media.tv.TvInputInfo;
28 import android.media.tv.TvInputManager.TvInputCallback;
29 import android.os.HandlerThread;
30 import android.os.Looper;
31 import android.os.Message;
32 import android.os.RemoteException;
33 import android.preference.PreferenceManager;
34 import android.support.annotation.NonNull;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 import com.android.tv.Features;
39 import com.android.tv.TvApplication;
40 import com.android.tv.common.WeakHandler;
41 import com.android.tv.data.Channel;
42 import com.android.tv.data.Program;
43 import com.android.tv.util.RecurringRunner;
44 import com.android.tv.util.TvInputManagerHelper;
45 import com.android.tv.util.Utils;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50 import java.util.Objects;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * An utility class to fetch the EPG. This class isn't thread-safe.
55  */
56 public class EpgFetcher {
57     private static final String TAG = "EpgFetcher";
58     private static final boolean DEBUG = false;
59 
60     private static final int MSG_FETCH_EPG = 1;
61 
62     private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4);
63     private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1);
64     private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
65 
66     private static final int BATCH_OPERATION_COUNT = 100;
67 
68     // Value: Long
69     private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
70             "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
71 
72     private final Context mContext;
73     private final TvInputManagerHelper mInputHelper;
74     private final TvInputCallback mInputCallback;
75     private HandlerThread mHandlerThread;
76     private EpgFetcherHandler mHandler;
77     private RecurringRunner mRecurringRunner;
78 
79     private long mLastEpgTimestamp = -1;
80 
EpgFetcher(Context context)81     public EpgFetcher(Context context) {
82         mContext = context;
83         mInputHelper = TvApplication.getSingletons(mContext).getTvInputManagerHelper();
84         mInputCallback = new TvInputCallback() {
85             @Override
86             public void onInputAdded(String inputId) {
87                 if (Utils.isInternalTvInput(mContext, inputId)) {
88                     mHandler.removeMessages(MSG_FETCH_EPG);
89                     mHandler.sendEmptyMessage(MSG_FETCH_EPG);
90                 }
91             }
92         };
93     }
94 
95     /**
96      * Starts fetching EPG.
97      */
start()98     public void start() {
99         if (DEBUG) Log.d(TAG, "Request to start fetching EPG.");
100         if (!Features.FETCH_EPG.isEnabled(mContext)) {
101             return;
102         }
103         if (mHandlerThread == null) {
104             mHandlerThread = new HandlerThread("EpgFetcher");
105             mHandlerThread.start();
106             mHandler = new EpgFetcherHandler(mHandlerThread.getLooper(), this);
107             mInputHelper.addCallback(mInputCallback);
108             mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS,
109                     new Runnable() {
110                         @Override
111                         public void run() {
112                             mHandler.removeMessages(MSG_FETCH_EPG);
113                             mHandler.sendEmptyMessage(MSG_FETCH_EPG);
114                         }
115                     }, null);
116             mRecurringRunner.start();
117         }
118     }
119 
120     /**
121      * Stops fetching EPG.
122      */
stop()123     public void stop() {
124         if (mHandlerThread == null) {
125             return;
126         }
127         mRecurringRunner.stop();
128         mHandler.removeCallbacksAndMessages(null);
129         mHandler = null;
130         mHandlerThread.quit();
131         mHandlerThread = null;
132     }
133 
onFetchEpg()134     private void onFetchEpg() {
135         if (DEBUG) Log.d(TAG, "Start fetching EPG.");
136         // Check for the internal inputs.
137         boolean hasInternalInput = false;
138         for (TvInputInfo input : mInputHelper.getTvInputInfos(true, true)) {
139             if (Utils.isInternalTvInput(mContext, input.getId())) {
140                 hasInternalInput = true;
141                 break;
142             }
143         }
144         if (!hasInternalInput) {
145             if (DEBUG) Log.d(TAG, "No internal input found.");
146             return;
147         }
148         // Check if EPG reader is available.
149         EpgReader epgReader = new StubEpgReader(mContext);
150         if (!epgReader.isAvailable()) {
151             if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available.");
152             mHandler.removeMessages(MSG_FETCH_EPG);
153             mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, EPG_READER_INIT_WAIT_MS);
154             return;
155         }
156         // Check the EPG Timestamp.
157         long epgTimestamp = epgReader.getEpgTimestamp();
158         if (epgTimestamp <= getLastUpdatedEpgTimestamp()) {
159             if (DEBUG) Log.d(TAG, "No new EPG.");
160             return;
161         }
162 
163         List<Channel> channels = epgReader.getChannels();
164         for (Channel channel : channels) {
165             List<Program> programs = new ArrayList<>(epgReader.getPrograms(channel.getId()));
166             Collections.sort(programs);
167             if (DEBUG) {
168                 Log.d(TAG, "Fetching " + programs.size() + " programs for channel " + channel);
169             }
170             updateEpg(channel.getId(), programs);
171         }
172 
173         setLastUpdatedEpgTimestamp(epgTimestamp);
174     }
175 
getLastUpdatedEpgTimestamp()176     private long getLastUpdatedEpgTimestamp() {
177         if (mLastEpgTimestamp < 0) {
178             mLastEpgTimestamp = PreferenceManager.getDefaultSharedPreferences(mContext).getLong(
179                     KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
180         }
181         return mLastEpgTimestamp;
182     }
183 
setLastUpdatedEpgTimestamp(long timestamp)184     private void setLastUpdatedEpgTimestamp(long timestamp) {
185         mLastEpgTimestamp = timestamp;
186         PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong(
187                 KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp);
188     }
189 
updateEpg(long channelId, List<Program> newPrograms)190     private void updateEpg(long channelId, List<Program> newPrograms) {
191         final int fetchedProgramsCount = newPrograms.size();
192         if (fetchedProgramsCount == 0) {
193             return;
194         }
195         long startTimeMs = System.currentTimeMillis();
196         long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION;
197         List<Program> oldPrograms = queryPrograms(mContext.getContentResolver(), channelId,
198                 startTimeMs, endTimeMs);
199         Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null;
200         int oldProgramsIndex = 0;
201         int newProgramsIndex = 0;
202         // Skip the past programs. They will be automatically removed by the system.
203         if (currentOldProgram != null) {
204             long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis();
205             for (Program program : newPrograms) {
206                 if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) {
207                     break;
208                 }
209                 newProgramsIndex++;
210             }
211         }
212         // Compare the new programs with old programs one by one and update/delete the old one
213         // or insert new program if there is no matching program in the database.
214         ArrayList<ContentProviderOperation> ops = new ArrayList<>();
215         while (newProgramsIndex < fetchedProgramsCount) {
216             // TODO: Extract to method and make test.
217             Program oldProgram = oldProgramsIndex < oldPrograms.size()
218                     ? oldPrograms.get(oldProgramsIndex) : null;
219             Program newProgram = newPrograms.get(newProgramsIndex);
220             boolean addNewProgram = false;
221             if (oldProgram != null) {
222                 if (oldProgram.equals(newProgram)) {
223                     // Exact match. No need to update. Move on to the next programs.
224                     oldProgramsIndex++;
225                     newProgramsIndex++;
226                 } else if (isSameTitleAndOverlap(oldProgram, newProgram)) {
227                     if (!oldProgram.equals(oldProgram)) {
228                         // Partial match. Update the old program with the new one.
229                         // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There
230                         // could be application specific settings which belong to the old program.
231                         ops.add(ContentProviderOperation.newUpdate(
232                                 TvContract.buildProgramUri(oldProgram.getId()))
233                                 .withValues(toContentValues(newProgram))
234                                 .build());
235                     }
236                     oldProgramsIndex++;
237                     newProgramsIndex++;
238                 } else if (oldProgram.getEndTimeUtcMillis()
239                         < newProgram.getEndTimeUtcMillis()) {
240                     // No match. Remove the old program first to see if the next program in
241                     // {@code oldPrograms} partially matches the new program.
242                     ops.add(ContentProviderOperation.newDelete(
243                             TvContract.buildProgramUri(oldProgram.getId()))
244                             .build());
245                     oldProgramsIndex++;
246                 } else {
247                     // No match. The new program does not match any of the old programs. Insert
248                     // it as a new program.
249                     addNewProgram = true;
250                     newProgramsIndex++;
251                 }
252             } else {
253                 // No old programs. Just insert new programs.
254                 addNewProgram = true;
255                 newProgramsIndex++;
256             }
257             if (addNewProgram) {
258                 ops.add(ContentProviderOperation
259                         .newInsert(TvContract.Programs.CONTENT_URI)
260                         .withValues(toContentValues(newProgram))
261                         .build());
262             }
263             // Throttle the batch operation not to cause TransactionTooLargeException.
264             if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
265                 try {
266                     if (DEBUG) {
267                         int size = ops.size();
268                         Log.d(TAG, "Running " + size + " operations for channel " + channelId);
269                         for (int i = 0; i < size; ++i) {
270                             Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
271                         }
272                     }
273                     mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
274                 } catch (RemoteException | OperationApplicationException e) {
275                     Log.e(TAG, "Failed to insert programs.", e);
276                     return;
277                 }
278                 ops.clear();
279             }
280         }
281         if (DEBUG) {
282             Log.d(TAG, "Fetched " + fetchedProgramsCount + " programs for channel " + channelId);
283         }
284     }
285 
286     private List<Program> queryPrograms(ContentResolver contentResolver, long channelId,
287             long startTimeMs, long endTimeMs) {
288         try (Cursor c = mContext.getContentResolver().query(
289                 TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
290                 Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
291             if (c == null) {
292                 return Collections.EMPTY_LIST;
293             }
294             ArrayList<Program> programs = new ArrayList<>();
295             while (c.moveToNext()) {
296                 programs.add(Program.fromCursor(c));
297             }
298             return programs;
299         }
300     }
301 
302     /**
303      * Returns {@code true} if the {@code oldProgram} program needs to be updated with the
304      * {@code newProgram} program.
305      */
306     private boolean isSameTitleAndOverlap(Program oldProgram, Program newProgram) {
307         // NOTE: Here, we update the old program if it has the same title and overlaps with the
308         // new program. The test logic is just an example and you can modify this. E.g. check
309         // whether the both programs have the same program ID if your EPG supports any ID for
310         // the programs.
311         return Objects.equals(oldProgram.getTitle(), newProgram.getTitle())
312                 && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
313                 && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
314     }
315 
316     private static ContentValues toContentValues(Program program) {
317         ContentValues values = new ContentValues();
318         values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
319         putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle());
320         putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle());
321         putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
322         putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
323         putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
324         putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
325         values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
326                 program.getStartTimeUtcMillis());
327         values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis());
328         return values;
329     }
330 
331     private static void putValue(ContentValues contentValues, String key, String value) {
332         if (TextUtils.isEmpty(value)) {
333             contentValues.putNull(key);
334         } else {
335             contentValues.put(key, value);
336         }
337     }
338 
339     private static class EpgFetcherHandler extends WeakHandler<EpgFetcher> {
340         public EpgFetcherHandler (@NonNull Looper looper, EpgFetcher ref) {
341             super(looper, ref);
342         }
343 
344         @Override
345         public void handleMessage(Message msg, @NonNull EpgFetcher epgFetcher) {
346             switch (msg.what) {
347                 case MSG_FETCH_EPG:
348                     epgFetcher.onFetchEpg();
349                     break;
350                 default:
351                     super.handleMessage(msg);
352                     break;
353             }
354         }
355     }
356 }
357