• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.support.annotation.Nullable;
20 import android.support.annotation.VisibleForTesting;
21 import android.text.TextUtils;
22 import com.android.tv.data.Program;
23 import java.text.BreakIterator;
24 import java.util.ArrayList;
25 import java.util.Calendar;
26 import java.util.List;
27 import java.util.concurrent.TimeUnit;
28 
29 public class RoutineWatchEvaluator extends Recommender.Evaluator {
30     // TODO: test and refine constant values in WatchedProgramRecommender in order to
31     // improve the performance of this recommender.
32     private static final double REQUIRED_MIN_SCORE = 0.15;
33     @VisibleForTesting static final double MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK = 0.7;
34     private static final double TITLE_MATCH_WEIGHT = 0.5;
35     private static final double TIME_MATCH_WEIGHT = 1 - TITLE_MATCH_WEIGHT;
36     private static final long DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(14);
37     private static final long MAX_DIFF_MS_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(56);
38 
39     @Override
evaluateChannel(long channelId)40     public double evaluateChannel(long channelId) {
41         ChannelRecord cr = getRecommender().getChannelRecord(channelId);
42         if (cr == null) {
43             return NOT_RECOMMENDED;
44         }
45 
46         Program currentProgram = cr.getCurrentProgram();
47         if (currentProgram == null) {
48             return NOT_RECOMMENDED;
49         }
50 
51         WatchedProgram[] watchHistory = cr.getWatchHistory();
52         if (watchHistory.length < 1) {
53             return NOT_RECOMMENDED;
54         }
55 
56         Program watchedProgram = watchHistory[watchHistory.length - 1].getProgram();
57         long startTimeDiffMsWithCurrentProgram =
58                 currentProgram.getStartTimeUtcMillis() - watchedProgram.getStartTimeUtcMillis();
59         if (startTimeDiffMsWithCurrentProgram >= MAX_DIFF_MS_FOR_OLD_PROGRAM) {
60             return NOT_RECOMMENDED;
61         }
62 
63         double maxScore = NOT_RECOMMENDED;
64         long watchedDurationMs = watchHistory[watchHistory.length - 1].getWatchedDurationMs();
65         for (int i = watchHistory.length - 2; i >= 0; --i) {
66             if (watchedProgram.getStartTimeUtcMillis()
67                     == watchHistory[i].getProgram().getStartTimeUtcMillis()) {
68                 watchedDurationMs += watchHistory[i].getWatchedDurationMs();
69             } else {
70                 double score =
71                         calculateRoutineWatchScore(
72                                 currentProgram, watchedProgram, watchedDurationMs);
73                 if (score >= REQUIRED_MIN_SCORE && score > maxScore) {
74                     maxScore = score;
75                 }
76                 watchedProgram = watchHistory[i].getProgram();
77                 watchedDurationMs = watchHistory[i].getWatchedDurationMs();
78                 startTimeDiffMsWithCurrentProgram =
79                         currentProgram.getStartTimeUtcMillis()
80                                 - watchedProgram.getStartTimeUtcMillis();
81                 if (startTimeDiffMsWithCurrentProgram >= MAX_DIFF_MS_FOR_OLD_PROGRAM) {
82                     return maxScore;
83                 }
84             }
85         }
86         double score =
87                 calculateRoutineWatchScore(currentProgram, watchedProgram, watchedDurationMs);
88         if (score >= REQUIRED_MIN_SCORE && score > maxScore) {
89             maxScore = score;
90         }
91         return maxScore;
92     }
93 
calculateRoutineWatchScore( Program currentProgram, Program watchedProgram, long watchedDurationMs)94     private static double calculateRoutineWatchScore(
95             Program currentProgram, Program watchedProgram, long watchedDurationMs) {
96         double timeMatchScore = calculateTimeMatchScore(currentProgram, watchedProgram);
97         double titleMatchScore =
98                 calculateTitleMatchScore(currentProgram.getTitle(), watchedProgram.getTitle());
99         double watchDurationScore = calculateWatchDurationScore(watchedProgram, watchedDurationMs);
100         long diffMs =
101                 currentProgram.getStartTimeUtcMillis() - watchedProgram.getStartTimeUtcMillis();
102         double multiplierForOldProgram =
103                 (diffMs < MAX_DIFF_MS_FOR_OLD_PROGRAM)
104                         ? 1.0
105                                 - (double) Math.max(diffMs - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM, 0)
106                                         / (MAX_DIFF_MS_FOR_OLD_PROGRAM
107                                                 - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM)
108                         : 0.0;
109         return (titleMatchScore * TITLE_MATCH_WEIGHT + timeMatchScore * TIME_MATCH_WEIGHT)
110                 * watchDurationScore
111                 * multiplierForOldProgram;
112     }
113 
114     @VisibleForTesting
calculateTitleMatchScore(@ullable String title1, @Nullable String title2)115     static double calculateTitleMatchScore(@Nullable String title1, @Nullable String title2) {
116         if (TextUtils.isEmpty(title1) || TextUtils.isEmpty(title2)) {
117             return 0;
118         }
119         List<String> wordList1 = splitTextToWords(title1);
120         List<String> wordList2 = splitTextToWords(title2);
121         if (wordList1.isEmpty() || wordList2.isEmpty()) {
122             return 0;
123         }
124         int maxMatchedWordSeqLen = calculateMaximumMatchedWordSequenceLength(wordList1, wordList2);
125 
126         // F-measure score
127         double precision = (double) maxMatchedWordSeqLen / wordList1.size();
128         double recall = (double) maxMatchedWordSeqLen / wordList2.size();
129         return 2.0 * precision * recall / (precision + recall);
130     }
131 
132     @VisibleForTesting
calculateMaximumMatchedWordSequenceLength( List<String> toSearchWords, List<String> toMatchWords)133     static int calculateMaximumMatchedWordSequenceLength(
134             List<String> toSearchWords, List<String> toMatchWords) {
135         int[] matchedWordSeqLen = new int[toMatchWords.size()];
136         int maxMatchedWordSeqLen = 0;
137         for (String word : toSearchWords) {
138             for (int j = toMatchWords.size() - 1; j >= 0; --j) {
139                 if (word.equals(toMatchWords.get(j))) {
140                     matchedWordSeqLen[j] = j > 0 ? matchedWordSeqLen[j - 1] + 1 : 1;
141                 } else {
142                     maxMatchedWordSeqLen = Math.max(maxMatchedWordSeqLen, matchedWordSeqLen[j]);
143                     matchedWordSeqLen[j] = 0;
144                 }
145             }
146         }
147         for (int len : matchedWordSeqLen) {
148             maxMatchedWordSeqLen = Math.max(maxMatchedWordSeqLen, len);
149         }
150 
151         return maxMatchedWordSeqLen;
152     }
153 
calculateTimeMatchScore(Program p1, Program p2)154     private static double calculateTimeMatchScore(Program p1, Program p2) {
155         ProgramTime t1 = ProgramTime.createFromProgram(p1);
156         ProgramTime t2 = ProgramTime.createFromProgram(p2);
157 
158         double dupTimeScore = calculateOverlappedIntervalScore(t1, t2);
159 
160         // F-measure score
161         double precision = dupTimeScore / (t1.endTimeOfDayInSec - t1.startTimeOfDayInSec);
162         double recall = dupTimeScore / (t2.endTimeOfDayInSec - t2.startTimeOfDayInSec);
163         return 2.0 * precision * recall / (precision + recall);
164     }
165 
166     @VisibleForTesting
calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2)167     static double calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2) {
168         if (t1.dayChanged && !t2.dayChanged) {
169             // Swap two values.
170             return calculateOverlappedIntervalScore(t2, t1);
171         }
172 
173         boolean sameDay = false;
174         // Handle cases like (00:00 - 02:00) - (01:00 - 03:00) or (22:00 - 25:00) - (23:00 - 26:00).
175         double score =
176                 Math.max(
177                         0,
178                         Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec)
179                                 - Math.max(t1.startTimeOfDayInSec, t2.startTimeOfDayInSec));
180         if (score > 0) {
181             sameDay = (t1.weekDay == t2.weekDay);
182         } else if (t1.dayChanged != t2.dayChanged) {
183             // To handle cases like t1 : (00:00 - 01:00) and t2 : (23:00 - 25:00).
184             score =
185                     Math.max(
186                             0,
187                             Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec - 24 * 60 * 60)
188                                     - t1.startTimeOfDayInSec);
189             // Same day if next day of t2's start day equals to t1's start day. (1 <= weekDay <= 7)
190             sameDay = (t1.weekDay == ((t2.weekDay % 7) + 1));
191         }
192 
193         if (!sameDay) {
194             score *= MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK;
195         }
196         return score;
197     }
198 
calculateWatchDurationScore(Program program, long durationMs)199     private static double calculateWatchDurationScore(Program program, long durationMs) {
200         return (double) durationMs
201                 / (program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis());
202     }
203 
204     @VisibleForTesting
getTimeOfDayInSec(Calendar time)205     static int getTimeOfDayInSec(Calendar time) {
206         return time.get(Calendar.HOUR_OF_DAY) * 60 * 60
207                 + time.get(Calendar.MINUTE) * 60
208                 + time.get(Calendar.SECOND);
209     }
210 
211     @VisibleForTesting
splitTextToWords(String text)212     static List<String> splitTextToWords(String text) {
213         List<String> wordList = new ArrayList<>();
214         BreakIterator boundary = BreakIterator.getWordInstance();
215         boundary.setText(text);
216         int start = boundary.first();
217         for (int end = boundary.next();
218                 end != BreakIterator.DONE;
219                 start = end, end = boundary.next()) {
220             String word = text.substring(start, end);
221             if (Character.isLetterOrDigit(word.charAt(0))) {
222                 wordList.add(word);
223             }
224         }
225         return wordList;
226     }
227 
228     @VisibleForTesting
229     static class ProgramTime {
230         final int startTimeOfDayInSec;
231         final int endTimeOfDayInSec;
232         final int weekDay;
233         final boolean dayChanged;
234 
createFromProgram(Program p)235         public static ProgramTime createFromProgram(Program p) {
236             Calendar time = Calendar.getInstance();
237 
238             time.setTimeInMillis(p.getStartTimeUtcMillis());
239             int weekDay = time.get(Calendar.DAY_OF_WEEK);
240             int startTimeOfDayInSec = getTimeOfDayInSec(time);
241 
242             time.setTimeInMillis(p.getEndTimeUtcMillis());
243             boolean dayChanged = (weekDay != time.get(Calendar.DAY_OF_WEEK));
244             // Set maximum program duration time to 12 hours.
245             int endTimeOfDayInSec =
246                     startTimeOfDayInSec
247                             + (int)
248                                             Math.min(
249                                                     p.getEndTimeUtcMillis()
250                                                             - p.getStartTimeUtcMillis(),
251                                                     TimeUnit.HOURS.toMillis(12))
252                                     / 1000;
253 
254             return new ProgramTime(startTimeOfDayInSec, endTimeOfDayInSec, weekDay, dayChanged);
255         }
256 
ProgramTime( int startTimeOfDayInSec, int endTimeOfDayInSec, int weekDay, boolean dayChanged)257         private ProgramTime(
258                 int startTimeOfDayInSec, int endTimeOfDayInSec, int weekDay, boolean dayChanged) {
259             this.startTimeOfDayInSec = startTimeOfDayInSec;
260             this.endTimeOfDayInSec = endTimeOfDayInSec;
261             this.weekDay = weekDay;
262             this.dayChanged = dayChanged;
263         }
264     }
265 }
266