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