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