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