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