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