| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.recommendation; |
| |
| import android.app.job.JobInfo; |
| import android.app.job.JobParameters; |
| import android.app.job.JobScheduler; |
| import android.app.job.JobService; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.os.AsyncTask; |
| import android.os.Build; |
| import android.support.annotation.RequiresApi; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.tvprovider.media.tv.TvContractCompat; |
| |
| import com.android.tv.Starter; |
| import com.android.tv.TvSingletons; |
| import com.android.tv.data.PreviewDataManager; |
| import com.android.tv.data.PreviewProgramContent; |
| import com.android.tv.data.api.Channel; |
| import com.android.tv.data.api.Program; |
| import com.android.tv.parental.ParentalControlSettings; |
| import com.android.tv.util.Utils; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Class for updating the preview programs for {@link Channel}. */ |
| @RequiresApi(Build.VERSION_CODES.O) |
| public class ChannelPreviewUpdater { |
| private static final String TAG = "ChannelPreviewUpdater"; |
| private static final boolean DEBUG = false; |
| |
| private static final int UPATE_PREVIEW_PROGRAMS_JOB_ID = 1000001; |
| private static final long ROUTINE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10); |
| // The left time of a program should meet the threshold so that it could be recommended. |
| private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = TimeUnit.MINUTES.toMillis(10); |
| private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90% |
| private static final int RECOMMENDATION_COUNT = 6; |
| private static final int MIN_COUNT_TO_ADD_ROW = 4; |
| |
| private static ChannelPreviewUpdater sChannelPreviewUpdater; |
| |
| /** Creates and returns the {@link ChannelPreviewUpdater}. */ |
| public static ChannelPreviewUpdater getInstance(Context context) { |
| if (sChannelPreviewUpdater == null) { |
| sChannelPreviewUpdater = new ChannelPreviewUpdater(context.getApplicationContext()); |
| } |
| return sChannelPreviewUpdater; |
| } |
| |
| private final Context mContext; |
| private final Recommender mRecommender; |
| private final PreviewDataManager mPreviewDataManager; |
| private JobService mJobService; |
| private JobParameters mJobParams; |
| |
| private final ParentalControlSettings mParentalControlSettings; |
| |
| private boolean mNeedUpdateAfterRecommenderReady = false; |
| |
| private Recommender.Listener mRecommenderListener = |
| new Recommender.Listener() { |
| @Override |
| public void onRecommenderReady() { |
| if (mNeedUpdateAfterRecommenderReady) { |
| if (DEBUG) Log.d(TAG, "Recommender is ready"); |
| updatePreviewDataForChannelsImmediately(); |
| mNeedUpdateAfterRecommenderReady = false; |
| } |
| } |
| |
| @Override |
| public void onRecommendationChanged() { |
| updatePreviewDataForChannelsImmediately(); |
| } |
| }; |
| |
| private ChannelPreviewUpdater(Context context) { |
| mContext = context; |
| mRecommender = new Recommender(context, mRecommenderListener, true); |
| mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1); |
| mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5); |
| mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0); |
| TvSingletons tvSingleton = TvSingletons.getSingletons(context); |
| mPreviewDataManager = tvSingleton.getPreviewDataManager(); |
| mParentalControlSettings = |
| tvSingleton.getTvInputManagerHelper().getParentalControlSettings(); |
| } |
| |
| /** Starts the routine service for updating the preview programs. */ |
| public void startRoutineService() { |
| JobScheduler jobScheduler = |
| (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE); |
| if (jobScheduler.getPendingJob(UPATE_PREVIEW_PROGRAMS_JOB_ID) != null) { |
| if (DEBUG) Log.d(TAG, "UPDATE_PREVIEW_JOB already exists"); |
| return; |
| } |
| JobInfo job = |
| new JobInfo.Builder( |
| UPATE_PREVIEW_PROGRAMS_JOB_ID, |
| new ComponentName(mContext, ChannelPreviewUpdateService.class)) |
| .setPeriodic(ROUTINE_INTERVAL_MS) |
| .setPersisted(true) |
| .build(); |
| if (jobScheduler.schedule(job) < 0) { |
| Log.i(TAG, "JobScheduler failed to schedule the job"); |
| } |
| } |
| |
| /** Called when {@link ChannelPreviewUpdateService} is started. */ |
| void onStartJob(JobService service, JobParameters params) { |
| if (DEBUG) Log.d(TAG, "onStartJob"); |
| mJobService = service; |
| mJobParams = params; |
| updatePreviewDataForChannelsImmediately(); |
| } |
| |
| /** Updates the preview programs table. */ |
| public void updatePreviewDataForChannelsImmediately() { |
| if (!mRecommender.isReady()) { |
| mNeedUpdateAfterRecommenderReady = true; |
| return; |
| } |
| |
| if (!mPreviewDataManager.isLoadFinished()) { |
| mPreviewDataManager.addListener( |
| new PreviewDataManager.PreviewDataListener() { |
| @Override |
| public void onPreviewDataLoadFinished() { |
| mPreviewDataManager.removeListener(this); |
| updatePreviewDataForChannels(); |
| } |
| |
| @Override |
| public void onPreviewDataUpdateFinished() {} |
| }); |
| return; |
| } |
| updatePreviewDataForChannels(); |
| } |
| |
| /** Called when {@link ChannelPreviewUpdateService} is stopped. */ |
| void onStopJob() { |
| if (DEBUG) Log.d(TAG, "onStopJob"); |
| mJobService = null; |
| mJobParams = null; |
| } |
| |
| private void updatePreviewDataForChannels() { |
| new AsyncTask<Void, Void, Set<Program>>() { |
| @Override |
| protected Set<Program> doInBackground(Void... params) { |
| Set<Program> programs = new HashSet<>(); |
| try { |
| List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels()); |
| for (Channel channel : channels) { |
| if (channel.isPhysicalTunerChannel()) { |
| final Program program = |
| Utils.getCurrentProgram(mContext, channel.getId()); |
| if (program != null |
| && isChannelRecommendationApplicable(channel, program)) { |
| programs.add(program); |
| if (programs.size() >= RECOMMENDATION_COUNT) { |
| break; |
| } |
| } |
| } |
| } |
| } catch (Exception e) { |
| Log.w(TAG, "Can't update preview data", e); |
| } |
| return programs; |
| } |
| |
| private boolean isChannelRecommendationApplicable(Channel channel, Program program) { |
| final long programDurationMs = |
| program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); |
| if (programDurationMs <= 0) { |
| return false; |
| } |
| if (TextUtils.isEmpty(program.getPosterArtUri())) { |
| return false; |
| } |
| if (mParentalControlSettings.isParentalControlsEnabled() |
| && (channel.isLocked() |
| || mParentalControlSettings.isRatingBlocked( |
| program.getContentRatings()))) { |
| return false; |
| } |
| long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis(); |
| final int programProgress = |
| (programDurationMs <= 0) |
| ? -1 |
| : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); |
| |
| // We recommend those programs that meet the condition only. |
| return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS |
| || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS; |
| } |
| |
| @Override |
| protected void onPostExecute(Set<Program> programs) { |
| updatePreviewDataForChannelsInternal(programs); |
| } |
| }.execute(); |
| } |
| |
| private void updatePreviewDataForChannelsInternal(Set<Program> programs) { |
| long defaultPreviewChannelId = |
| mPreviewDataManager.getPreviewChannelId( |
| PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL); |
| if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) { |
| // Only create if there is enough programs |
| if (programs.size() > MIN_COUNT_TO_ADD_ROW) { |
| mPreviewDataManager.createDefaultPreviewChannel( |
| new PreviewDataManager.OnPreviewChannelCreationResultListener() { |
| @Override |
| public void onPreviewChannelCreationResult( |
| long createdPreviewChannelId) { |
| if (createdPreviewChannelId |
| != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) { |
| TvContractCompat.requestChannelBrowsable( |
| mContext, createdPreviewChannelId); |
| updatePreviewProgramsForPreviewChannel( |
| createdPreviewChannelId, |
| generatePreviewProgramContentsFromPrograms( |
| createdPreviewChannelId, programs)); |
| } |
| } |
| }); |
| } else if (mJobService != null && mJobParams != null) { |
| if (DEBUG) { |
| Log.d( |
| TAG, |
| "Preview channel not created because there is only " |
| + programs.size() |
| + " programs"); |
| } |
| mJobService.jobFinished(mJobParams, false); |
| mJobService = null; |
| mJobParams = null; |
| } |
| } else { |
| updatePreviewProgramsForPreviewChannel( |
| defaultPreviewChannelId, |
| generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs)); |
| } |
| } |
| |
| private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms( |
| long previewChannelId, Set<Program> programs) { |
| Set<PreviewProgramContent> result = new HashSet<>(); |
| for (Program program : programs) { |
| PreviewProgramContent previewProgramContent = |
| PreviewProgramContent.createFromProgram(mContext, previewChannelId, program); |
| if (previewProgramContent != null) { |
| result.add(previewProgramContent); |
| } |
| } |
| return result; |
| } |
| |
| private void updatePreviewProgramsForPreviewChannel( |
| long previewChannelId, Set<PreviewProgramContent> previewProgramContents) { |
| PreviewDataManager.PreviewDataListener previewDataListener = |
| new PreviewDataManager.PreviewDataListener() { |
| @Override |
| public void onPreviewDataLoadFinished() {} |
| |
| @Override |
| public void onPreviewDataUpdateFinished() { |
| mPreviewDataManager.removeListener(this); |
| if (mJobService != null && mJobParams != null) { |
| if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService"); |
| mJobService.jobFinished(mJobParams, false); |
| mJobService = null; |
| mJobParams = null; |
| } else { |
| if (DEBUG) |
| Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService"); |
| } |
| } |
| }; |
| mPreviewDataManager.updatePreviewProgramsForChannel( |
| previewChannelId, previewProgramContents, previewDataListener); |
| } |
| |
| /** Job to execute the update of preview programs. */ |
| public static class ChannelPreviewUpdateService extends JobService { |
| private ChannelPreviewUpdater mChannelPreviewUpdater; |
| |
| @Override |
| public void onCreate() { |
| Starter.start(this); |
| if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate"); |
| mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this); |
| } |
| |
| @Override |
| public boolean onStartJob(JobParameters params) { |
| mChannelPreviewUpdater.onStartJob(this, params); |
| return true; |
| } |
| |
| @Override |
| public boolean onStopJob(JobParameters params) { |
| mChannelPreviewUpdater.onStopJob(); |
| return false; |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy"); |
| } |
| } |
| } |