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 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 List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels()); 173 for (Channel channel : channels) { 174 if (channel.isPhysicalTunerChannel()) { 175 final Program program = Utils.getCurrentProgram(mContext, channel.getId()); 176 if (program != null 177 && isChannelRecommendationApplicable(channel, program)) { 178 programs.add(program); 179 if (programs.size() >= RECOMMENDATION_COUNT) { 180 break; 181 } 182 } 183 } 184 } 185 return programs; 186 } 187 188 private boolean isChannelRecommendationApplicable(Channel channel, Program program) { 189 final long programDurationMs = 190 program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); 191 if (programDurationMs <= 0) { 192 return false; 193 } 194 if (TextUtils.isEmpty(program.getPosterArtUri())) { 195 return false; 196 } 197 if (mParentalControlSettings.isParentalControlsEnabled() 198 && (channel.isLocked() 199 || mParentalControlSettings.isRatingBlocked( 200 program.getContentRatings()))) { 201 return false; 202 } 203 long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis(); 204 final int programProgress = 205 (programDurationMs <= 0) 206 ? -1 207 : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); 208 209 // We recommend those programs that meet the condition only. 210 return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS 211 || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS; 212 } 213 214 @Override 215 protected void onPostExecute(Set<Program> programs) { 216 updatePreviewDataForChannelsInternal(programs); 217 } 218 }.execute(); 219 } 220 updatePreviewDataForChannelsInternal(Set<Program> programs)221 private void updatePreviewDataForChannelsInternal(Set<Program> programs) { 222 long defaultPreviewChannelId = 223 mPreviewDataManager.getPreviewChannelId( 224 PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL); 225 if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) { 226 // Only create if there is enough programs 227 if (programs.size() > MIN_COUNT_TO_ADD_ROW) { 228 mPreviewDataManager.createDefaultPreviewChannel( 229 new PreviewDataManager.OnPreviewChannelCreationResultListener() { 230 @Override 231 public void onPreviewChannelCreationResult( 232 long createdPreviewChannelId) { 233 if (createdPreviewChannelId 234 != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) { 235 TvContractCompat.requestChannelBrowsable( 236 mContext, createdPreviewChannelId); 237 updatePreviewProgramsForPreviewChannel( 238 createdPreviewChannelId, 239 generatePreviewProgramContentsFromPrograms( 240 createdPreviewChannelId, programs)); 241 } 242 } 243 }); 244 } 245 } else { 246 updatePreviewProgramsForPreviewChannel( 247 defaultPreviewChannelId, 248 generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs)); 249 } 250 } 251 generatePreviewProgramContentsFromPrograms( long previewChannelId, Set<Program> programs)252 private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms( 253 long previewChannelId, Set<Program> programs) { 254 Set<PreviewProgramContent> result = new HashSet<>(); 255 for (Program program : programs) { 256 PreviewProgramContent previewProgramContent = 257 PreviewProgramContent.createFromProgram(mContext, previewChannelId, program); 258 if (previewProgramContent != null) { 259 result.add(previewProgramContent); 260 } 261 } 262 return result; 263 } 264 updatePreviewProgramsForPreviewChannel( long previewChannelId, Set<PreviewProgramContent> previewProgramContents)265 private void updatePreviewProgramsForPreviewChannel( 266 long previewChannelId, Set<PreviewProgramContent> previewProgramContents) { 267 PreviewDataManager.PreviewDataListener previewDataListener = 268 new PreviewDataManager.PreviewDataListener() { 269 @Override 270 public void onPreviewDataLoadFinished() {} 271 272 @Override 273 public void onPreviewDataUpdateFinished() { 274 mPreviewDataManager.removeListener(this); 275 if (mJobService != null && mJobParams != null) { 276 if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService"); 277 mJobService.jobFinished(mJobParams, false); 278 mJobService = null; 279 mJobParams = null; 280 } else { 281 if (DEBUG) 282 Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService"); 283 } 284 } 285 }; 286 mPreviewDataManager.updatePreviewProgramsForChannel( 287 previewChannelId, previewProgramContents, previewDataListener); 288 } 289 290 /** Job to execute the update of preview programs. */ 291 public static class ChannelPreviewUpdateService extends JobService { 292 private ChannelPreviewUpdater mChannelPreviewUpdater; 293 294 @Override onCreate()295 public void onCreate() { 296 Starter.start(this); 297 if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate"); 298 mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this); 299 } 300 301 @Override onStartJob(JobParameters params)302 public boolean onStartJob(JobParameters params) { 303 mChannelPreviewUpdater.onStartJob(this, params); 304 return true; 305 } 306 307 @Override onStopJob(JobParameters params)308 public boolean onStopJob(JobParameters params) { 309 mChannelPreviewUpdater.onStopJob(); 310 return false; 311 } 312 313 @Override onDestroy()314 public void onDestroy() { 315 if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy"); 316 } 317 } 318 } 319