• 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.text.TextUtils;
29 import android.util.Log;
30 import androidx.tvprovider.media.tv.TvContractCompat;
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                 try {
173                     List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
174                     for (Channel channel : channels) {
175                         if (channel.isPhysicalTunerChannel()) {
176                             final Program program =
177                                     Utils.getCurrentProgram(mContext, channel.getId());
178                             if (program != null
179                                     && isChannelRecommendationApplicable(channel, program)) {
180                                 programs.add(program);
181                                 if (programs.size() >= RECOMMENDATION_COUNT) {
182                                     break;
183                                 }
184                             }
185                         }
186                     }
187                 } catch (Exception e) {
188                     Log.w(TAG, "Can't update preview data", e);
189                 }
190                 return programs;
191             }
192 
193             private boolean isChannelRecommendationApplicable(Channel channel, Program program) {
194                 final long programDurationMs =
195                         program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis();
196                 if (programDurationMs <= 0) {
197                     return false;
198                 }
199                 if (TextUtils.isEmpty(program.getPosterArtUri())) {
200                     return false;
201                 }
202                 if (mParentalControlSettings.isParentalControlsEnabled()
203                         && (channel.isLocked()
204                                 || mParentalControlSettings.isRatingBlocked(
205                                         program.getContentRatings()))) {
206                     return false;
207                 }
208                 long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis();
209                 final int programProgress =
210                         (programDurationMs <= 0)
211                                 ? -1
212                                 : 100 - (int) (programLeftTimsMs * 100 / programDurationMs);
213 
214                 // We recommend those programs that meet the condition only.
215                 return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS
216                         || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS;
217             }
218 
219             @Override
220             protected void onPostExecute(Set<Program> programs) {
221                 updatePreviewDataForChannelsInternal(programs);
222             }
223         }.execute();
224     }
225 
updatePreviewDataForChannelsInternal(Set<Program> programs)226     private void updatePreviewDataForChannelsInternal(Set<Program> programs) {
227         long defaultPreviewChannelId =
228                 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             } else if (mJobService != null && mJobParams != null) {
250                 if (DEBUG) {
251                     Log.d(
252                             TAG,
253                             "Preview channel not created because there is only "
254                                     + programs.size()
255                                     + " programs");
256                 }
257                 mJobService.jobFinished(mJobParams, false);
258                 mJobService = null;
259                 mJobParams = null;
260             }
261         } else {
262             updatePreviewProgramsForPreviewChannel(
263                     defaultPreviewChannelId,
264                     generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs));
265         }
266     }
267 
generatePreviewProgramContentsFromPrograms( long previewChannelId, Set<Program> programs)268     private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms(
269             long previewChannelId, Set<Program> programs) {
270         Set<PreviewProgramContent> result = new HashSet<>();
271         for (Program program : programs) {
272             PreviewProgramContent previewProgramContent =
273                     PreviewProgramContent.createFromProgram(mContext, previewChannelId, program);
274             if (previewProgramContent != null) {
275                 result.add(previewProgramContent);
276             }
277         }
278         return result;
279     }
280 
updatePreviewProgramsForPreviewChannel( long previewChannelId, Set<PreviewProgramContent> previewProgramContents)281     private void updatePreviewProgramsForPreviewChannel(
282             long previewChannelId, Set<PreviewProgramContent> previewProgramContents) {
283         PreviewDataManager.PreviewDataListener previewDataListener =
284                 new PreviewDataManager.PreviewDataListener() {
285                     @Override
286                     public void onPreviewDataLoadFinished() {}
287 
288                     @Override
289                     public void onPreviewDataUpdateFinished() {
290                         mPreviewDataManager.removeListener(this);
291                         if (mJobService != null && mJobParams != null) {
292                             if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService");
293                             mJobService.jobFinished(mJobParams, false);
294                             mJobService = null;
295                             mJobParams = null;
296                         } else {
297                             if (DEBUG)
298                                 Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService");
299                         }
300                     }
301                 };
302         mPreviewDataManager.updatePreviewProgramsForChannel(
303                 previewChannelId, previewProgramContents, previewDataListener);
304     }
305 
306     /** Job to execute the update of preview programs. */
307     public static class ChannelPreviewUpdateService extends JobService {
308         private ChannelPreviewUpdater mChannelPreviewUpdater;
309 
310         @Override
onCreate()311         public void onCreate() {
312             Starter.start(this);
313             if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate");
314             mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this);
315         }
316 
317         @Override
onStartJob(JobParameters params)318         public boolean onStartJob(JobParameters params) {
319             mChannelPreviewUpdater.onStartJob(this, params);
320             return true;
321         }
322 
323         @Override
onStopJob(JobParameters params)324         public boolean onStopJob(JobParameters params) {
325             mChannelPreviewUpdater.onStopJob();
326             return false;
327         }
328 
329         @Override
onDestroy()330         public void onDestroy() {
331             if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy");
332         }
333     }
334 }
335