• 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.recommendation;
18 
19 import android.app.job.JobInfo;
20 import android.app.job.JobParameters;
21 import android.app.job.JobScheduler;
22 import android.app.job.JobService;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.os.AsyncTask;
26 import android.os.Build;
27 import android.support.annotation.RequiresApi;
28 import android.support.media.tv.TvContractCompat;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import com.android.tv.ApplicationSingletons;
33 import com.android.tv.TvApplication;
34 import com.android.tv.data.Channel;
35 import com.android.tv.data.PreviewDataManager;
36 import com.android.tv.data.PreviewProgramContent;
37 import com.android.tv.data.Program;
38 import com.android.tv.parental.ParentalControlSettings;
39 import com.android.tv.util.Utils;
40 
41 import java.util.ArrayList;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 import java.util.concurrent.TimeUnit;
46 
47 /** Class for updating the preview programs for {@link Channel}. */
48 @RequiresApi(Build.VERSION_CODES.O)
49 public class ChannelPreviewUpdater {
50     private static final String TAG = "ChannelPreviewUpdater";
51     // STOPSHIP: set it to false.
52     private static final boolean DEBUG = true;
53 
54     private static final int UPATE_PREVIEW_PROGRAMS_JOB_ID = 1000001;
55     private static final long ROUTINE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10);
56     // The left time of a program should meet the threshold so that it could be recommended.
57     private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS =
58             TimeUnit.MINUTES.toMillis(10);
59     private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90;  // 90%
60     private static final int RECOMMENDATION_COUNT = 6;
61     private static final int MIN_COUNT_TO_ADD_ROW = 4;
62 
63     private static ChannelPreviewUpdater sChannelPreviewUpdater;
64 
65     /**
66      * Creates and returns the {@link ChannelPreviewUpdater}.
67      */
getInstance(Context context)68     public static ChannelPreviewUpdater getInstance(Context context) {
69         if (sChannelPreviewUpdater == null) {
70             sChannelPreviewUpdater = new ChannelPreviewUpdater(context.getApplicationContext());
71         }
72         return sChannelPreviewUpdater;
73     }
74 
75     private final Context mContext;
76     private final Recommender mRecommender;
77     private final PreviewDataManager mPreviewDataManager;
78     private JobService mJobService;
79     private JobParameters mJobParams;
80 
81     private final ParentalControlSettings mParentalControlSettings;
82 
83     private boolean mNeedUpdateAfterRecommenderReady = false;
84 
85     private Recommender.Listener mRecommenderListener = new Recommender.Listener() {
86         @Override
87         public void onRecommenderReady() {
88             if (mNeedUpdateAfterRecommenderReady) {
89                 if (DEBUG) Log.d(TAG, "Recommender is ready");
90                 updatePreviewDataForChannelsImmediately();
91                 mNeedUpdateAfterRecommenderReady = false;
92             }
93         }
94 
95         @Override
96         public void onRecommendationChanged() {
97             updatePreviewDataForChannelsImmediately();
98         }
99     };
100 
ChannelPreviewUpdater(Context context)101     private ChannelPreviewUpdater(Context context) {
102         mContext = context;
103         mRecommender = new Recommender(context, mRecommenderListener, true);
104         mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1);
105         mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5);
106         mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0);
107         ApplicationSingletons appSingleton = TvApplication.getSingletons(context);
108         mPreviewDataManager = appSingleton.getPreviewDataManager();
109         mParentalControlSettings = appSingleton.getTvInputManagerHelper()
110                 .getParentalControlSettings();
111     }
112 
113     /**
114      * Starts the routine service for updating the preview programs.
115      */
startRoutineService()116     public void startRoutineService() {
117         JobScheduler jobScheduler =
118                 (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
119         if (jobScheduler.getPendingJob(UPATE_PREVIEW_PROGRAMS_JOB_ID) != null) {
120             if (DEBUG) Log.d(TAG, "UPDATE_PREVIEW_JOB already exists");
121             return;
122         }
123         JobInfo job = new JobInfo.Builder(UPATE_PREVIEW_PROGRAMS_JOB_ID,
124                 new ComponentName(mContext, ChannelPreviewUpdateService.class))
125                 .setPeriodic(ROUTINE_INTERVAL_MS)
126                 .setPersisted(true)
127                 .build();
128         if (jobScheduler.schedule(job) < 0) {
129             Log.i(TAG, "JobScheduler failed to schedule the job");
130         }
131     }
132 
133     /** Called when {@link ChannelPreviewUpdateService} is started. */
onStartJob(JobService service, JobParameters params)134     void onStartJob(JobService service, JobParameters params) {
135         if (DEBUG) Log.d(TAG, "onStartJob");
136         mJobService = service;
137         mJobParams = params;
138         updatePreviewDataForChannelsImmediately();
139     }
140 
141     /**
142      * Updates the preview programs table.
143      */
updatePreviewDataForChannelsImmediately()144     public void updatePreviewDataForChannelsImmediately() {
145         if (!mRecommender.isReady()) {
146             mNeedUpdateAfterRecommenderReady = true;
147             return;
148         }
149 
150         if (!mPreviewDataManager.isLoadFinished()) {
151             mPreviewDataManager.addListener(new PreviewDataManager.PreviewDataListener() {
152                 @Override
153                 public void onPreviewDataLoadFinished() {
154                     mPreviewDataManager.removeListener(this);
155                     updatePreviewDataForChannels();
156                 }
157 
158                 @Override
159                 public void onPreviewDataUpdateFinished() { }
160             });
161             return;
162         }
163         updatePreviewDataForChannels();
164     }
165 
166     /** Called when {@link ChannelPreviewUpdateService} is stopped. */
onStopJob()167     void onStopJob() {
168         if (DEBUG) Log.d(TAG, "onStopJob");
169         mJobService = null;
170         mJobParams = null;
171     }
172 
updatePreviewDataForChannels()173     private void updatePreviewDataForChannels() {
174         new AsyncTask<Void, Void, Set<Program>>() {
175             @Override
176             protected Set<Program> doInBackground(Void... params) {
177                 Set<Program> programs = new HashSet<>();
178                 List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
179                 for (Channel channel : channels) {
180                     if (channel.isPhysicalTunerChannel()) {
181                         final Program program = Utils.getCurrentProgram(mContext, channel.getId());
182                         if (program != null
183                                 && isChannelRecommendationApplicable(channel, program)) {
184                             programs.add(program);
185                             if (programs.size() >= RECOMMENDATION_COUNT) {
186                                 break;
187                             }
188                         }
189                     }
190                 }
191                 return programs;
192             }
193 
194             private boolean isChannelRecommendationApplicable(Channel channel, Program program) {
195                 final long programDurationMs =
196                         program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis();
197                 if (programDurationMs <= 0) {
198                     return false;
199                 }
200                 if (TextUtils.isEmpty(program.getPosterArtUri())) {
201                     return false;
202                 }
203                 if (mParentalControlSettings.isParentalControlsEnabled()
204                         && (channel.isLocked()
205                                 || mParentalControlSettings.isRatingBlocked(
206                                         program.getContentRatings()))) {
207                     return false;
208                 }
209                 long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis();
210                 final int programProgress =
211                         (programDurationMs <= 0)
212                                 ? -1
213                                 : 100 - (int) (programLeftTimsMs * 100 / programDurationMs);
214 
215                 // We recommend those programs that meet the condition only.
216                 return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS
217                         || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS;
218             }
219 
220             @Override
221             protected void onPostExecute(Set<Program> programs) {
222                 updatePreviewDataForChannelsInternal(programs);
223             }
224         }.execute();
225     }
226 
updatePreviewDataForChannelsInternal(Set<Program> programs)227     private void updatePreviewDataForChannelsInternal(Set<Program> programs) {
228         long defaultPreviewChannelId = mPreviewDataManager.getPreviewChannelId(
229                 PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL);
230         if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
231             // Only create if there is enough programs
232             if (programs.size() > MIN_COUNT_TO_ADD_ROW) {
233                 mPreviewDataManager.createDefaultPreviewChannel(
234                         new PreviewDataManager.OnPreviewChannelCreationResultListener() {
235                             @Override
236                             public void onPreviewChannelCreationResult(
237                                     long createdPreviewChannelId) {
238                                 if (createdPreviewChannelId
239                                         != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
240                                     TvContractCompat.requestChannelBrowsable(
241                                             mContext, createdPreviewChannelId);
242                                     updatePreviewProgramsForPreviewChannel(
243                                             createdPreviewChannelId,
244                                             generatePreviewProgramContentsFromPrograms(
245                                                     createdPreviewChannelId, programs));
246                                 }
247                             }
248                         });
249             }
250         } else {
251             updatePreviewProgramsForPreviewChannel(defaultPreviewChannelId,
252                     generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs));
253         }
254     }
255 
generatePreviewProgramContentsFromPrograms( long previewChannelId, Set<Program> programs)256     private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms(
257             long previewChannelId, Set<Program> programs) {
258         Set<PreviewProgramContent> result = new HashSet<>();
259         for (Program program : programs) {
260             PreviewProgramContent previewProgramContent =
261                     PreviewProgramContent.createFromProgram(mContext, previewChannelId, program);
262             if (previewProgramContent != null) {
263                 result.add(previewProgramContent);
264             }
265         }
266         return result;
267     }
268 
updatePreviewProgramsForPreviewChannel(long previewChannelId, Set<PreviewProgramContent> previewProgramContents)269     private void updatePreviewProgramsForPreviewChannel(long previewChannelId,
270             Set<PreviewProgramContent> previewProgramContents) {
271         PreviewDataManager.PreviewDataListener previewDataListener
272                 = new PreviewDataManager.PreviewDataListener() {
273             @Override
274             public void onPreviewDataLoadFinished() { }
275 
276             @Override
277             public void onPreviewDataUpdateFinished() {
278                 mPreviewDataManager.removeListener(this);
279                 if (mJobService != null && mJobParams != null) {
280                     if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService");
281                     mJobService.jobFinished(mJobParams, false);
282                     mJobService = null;
283                     mJobParams = null;
284                 } else {
285                     if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService");
286                 }
287             }
288         };
289         mPreviewDataManager.updatePreviewProgramsForChannel(
290                 previewChannelId, previewProgramContents, previewDataListener);
291     }
292 
293     /**
294      * Job to execute the update of preview programs.
295      */
296     public static class ChannelPreviewUpdateService extends JobService {
297         private ChannelPreviewUpdater mChannelPreviewUpdater;
298 
299         @Override
onCreate()300         public void onCreate() {
301             TvApplication.setCurrentRunningProcess(this, true);
302             if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate");
303             mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this);
304         }
305 
306         @Override
onStartJob(JobParameters params)307         public boolean onStartJob(JobParameters params) {
308             mChannelPreviewUpdater.onStartJob(this, params);
309             return true;
310         }
311 
312         @Override
onStopJob(JobParameters params)313         public boolean onStopJob(JobParameters params) {
314             mChannelPreviewUpdater.onStopJob();
315             return false;
316         }
317 
318         @Override
onDestroy()319         public void onDestroy() {
320             if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy");
321         }
322     }
323 }
324