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