Home | History | Annotate | Download | only in recommendation
      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      */
     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 
    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      */
    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. */
    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      */
    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. */
    167     void onStopJob() {
    168         if (DEBUG) Log.d(TAG, "onStopJob");
    169         mJobService = null;
    170         mJobParams = null;
    171     }
    172 
    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 
    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 
    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 
    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
    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
    307         public boolean onStartJob(JobParameters params) {
    308             mChannelPreviewUpdater.onStartJob(this, params);
    309             return true;
    310         }
    311 
    312         @Override
    313         public boolean onStopJob(JobParameters params) {
    314             mChannelPreviewUpdater.onStopJob();
    315             return false;
    316         }
    317 
    318         @Override
    319         public void onDestroy() {
    320             if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy");
    321         }
    322     }
    323 }
    324