diff options
author | Sungsoo Lim <sungsoo@google.com> | 2014-05-29 11:38:37 +0900 |
---|---|---|
committer | Sungsoo Lim <sungsoo@google.com> | 2014-06-18 15:38:31 +0900 |
commit | 3b06c249c2a5fe37b1561b01d105ecd94833e8b4 (patch) | |
tree | 9df48e2d6679f87b514ce19a9e21817edd2ae154 /src/com/android/tv/recommendation | |
parent | 5170e59d6a735300b4d0e2c1d178c8c36fd4fa96 (diff) | |
download | TV-3b06c249c2a5fe37b1561b01d105ecd94833e8b4.tar.gz |
Implemet a heuristic recommender
Based on the watch history, WatchProgramRecommender will recommend channels
that you watched before on the same time in one or several weeks ago.
Bug: 15709478
Change-Id: I7d2f50f17114713c29fc68751dba7246a72d8412
Diffstat (limited to 'src/com/android/tv/recommendation')
-rw-r--r-- | src/com/android/tv/recommendation/TvRecommendation.java | 59 | ||||
-rw-r--r-- | src/com/android/tv/recommendation/WatchedProgramRecommender.java | 189 |
2 files changed, 234 insertions, 14 deletions
diff --git a/src/com/android/tv/recommendation/TvRecommendation.java b/src/com/android/tv/recommendation/TvRecommendation.java index ea7cb717..b1d682aa 100644 --- a/src/com/android/tv/recommendation/TvRecommendation.java +++ b/src/com/android/tv/recommendation/TvRecommendation.java @@ -28,16 +28,20 @@ import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.net.Uri; import android.os.Handler; +import android.text.TextUtils; import android.util.Log; import com.android.tv.data.Channel; +import com.android.tv.data.Program; import java.util.ArrayList; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; public class TvRecommendation { @@ -61,12 +65,12 @@ public class TvRecommendation { } private final List<TvRecommenderWrapper> mTvRecommenders; - private Map<Long, ChannelRecord> mChannelRecordMap; // TODO: Consider to define each observer rather than the list or observers. private final Handler mHandler; private final ContentObserver mContentObserver; private final Context mContext; private final boolean mIncludeRecommendedOnly; + private Map<Long, ChannelRecord> mChannelRecordMap; /** * Create a TV recommendation object. @@ -181,15 +185,10 @@ public class TvRecommendation { mChannelRecordMap.remove(channelId); } } else if (match == MATCH_WATCHED_PROGRAM_ID) { - String[] projection = { - TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, - TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, - TvContract.WatchedPrograms.COLUMN_CHANNEL_ID }; - Cursor cursor = null; try { cursor = mContext.getContentResolver().query( - uri, projection, null, null, null); + uri, null, null, null, null); if (cursor != null && cursor.moveToFirst()) { ChannelRecord channelRecord = updateChannelRecordFromWatchedProgramCursor(cursor); @@ -292,14 +291,9 @@ public class TvRecommendation { } // update last watched time for channels. - String[] projection = { - TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, - TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, - TvContract.WatchedPrograms.COLUMN_CHANNEL_ID }; - try { cursor = mContext.getContentResolver().query( - TvContract.WatchedPrograms.CONTENT_URI, projection, null, null, null); + TvContract.WatchedPrograms.CONTENT_URI, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { updateChannelRecordFromWatchedProgramCursor(cursor); @@ -312,7 +306,25 @@ public class TvRecommendation { } } - private final ChannelRecord updateChannelRecordFromWatchedProgramCursor(Cursor cursor) { + private Program createProgramFromWatchedProgramCursor(Cursor cursor) { + final int indexWatchChannelId = cursor.getColumnIndex( + TvContract.WatchedPrograms.COLUMN_CHANNEL_ID); + final int indexProgramTitle = cursor.getColumnIndex( + TvContract.WatchedPrograms.COLUMN_TITLE); + final int indexProgramStartTime = cursor.getColumnIndex( + TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); + final int indexProgramEndTime = cursor.getColumnIndex( + TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); + String title = cursor.getString(indexProgramTitle); + return TextUtils.isEmpty(title) ? null : new Program.Builder() + .setChannelId(cursor.getLong(indexWatchChannelId)) + .setTitle(title) + .setStartTimeUtcMillis(cursor.getLong(indexProgramStartTime)) + .setStartTimeUtcMillis(cursor.getLong(indexProgramEndTime)) + .build(); + } + + private ChannelRecord updateChannelRecordFromWatchedProgramCursor(Cursor cursor) { final int indexWatchStartTime = cursor.getColumnIndex( TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); final int indexWatchEndTime = cursor.getColumnIndex( @@ -329,6 +341,10 @@ public class TvRecommendation { if (channelRecord != null && channelRecord.mLastWatchedTimeMs < watchEndTimeMs) { channelRecord.mLastWatchedTimeMs = watchEndTimeMs; channelRecord.mLastWatchDurationMs = watchDurationMs; + Program program = createProgramFromWatchedProgramCursor(cursor); + if (program != null) { + channelRecord.logWatchHistory(program); + } } } return channelRecord; @@ -336,8 +352,11 @@ public class TvRecommendation { public static class ChannelRecord implements Comparable<ChannelRecord>, Channel.LoadLogoCallback { + // TODO: decide the value for max history size. + private static final int MAX_HISTORY_SIZE = 100; private final Channel mChannel; private final Uri mChannelUri; + private final Queue<Program> mWatchHistory; private long mLastWatchedTimeMs; private long mLastWatchDurationMs; private double mScore; @@ -346,6 +365,7 @@ public class TvRecommendation { mChannel = channel; mChannelUri = ContentUris.withAppendedId(TvContract.Channels.CONTENT_URI, channel.getId()); + mWatchHistory = new ArrayDeque<Program>(); mLastWatchedTimeMs = 0l; mLastWatchDurationMs = 0; mChannel.loadLogo(context, this); @@ -371,6 +391,17 @@ public class TvRecommendation { return mScore; } + public final Program[] getWatchHistory() { + return mWatchHistory.toArray(new Program[0]); + } + + public void logWatchHistory(Program p) { + mWatchHistory.offer(p); + if (mWatchHistory.size() > MAX_HISTORY_SIZE) { + mWatchHistory.poll(); + } + } + @Override public int compareTo(ChannelRecord another) { // Make Array.sort work in descending order. diff --git a/src/com/android/tv/recommendation/WatchedProgramRecommender.java b/src/com/android/tv/recommendation/WatchedProgramRecommender.java new file mode 100644 index 00000000..9cd0343f --- /dev/null +++ b/src/com/android/tv/recommendation/WatchedProgramRecommender.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.text.format.Time; + +import com.android.tv.data.Program; +import com.android.tv.recommendation.TvRecommendation.ChannelRecord; +import com.android.tv.recommendation.TvRecommendation.TvRecommender; +import com.android.tv.util.Utils; + +public class WatchedProgramRecommender extends TvRecommender { + // TODO: test and refine constant values in WatchedProgramRecommender in order to + // improve the performance of this recommender. + private static final double REQUIRED_MIM_SCORE = 0.15; + private static final double MATCH_SCORE_DAY_OF_WEEK_MATCHED= 1.0; + private static final double MATCH_SCORE_DAY_OF_WEEK_HIGH = 0.9; + private static final double MATCH_SCORE_DAY_OF_WEEK_LOW = 0.5; + private static final double MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED = 0.0; + + private final Context mContext; + + public WatchedProgramRecommender(Context context) { + mContext = context; + } + + @Override + public double calculateScore(final ChannelRecord cr) { + double maxScore = NOT_RECOMMENDED; + Program curProgram = Utils.getCurrentProgram(mContext, cr.getChannelUri()); + + if (curProgram != null) { + for (Program program : cr.getWatchHistory()) { + double score = calculateTitleMatchScore(curProgram, program) * 0.8 + + calculateTimeMatchScore(program) * 0.2; + if (score >= REQUIRED_MIM_SCORE && score > maxScore) { + maxScore = score; + } + } + } + return maxScore; + } + + private int calculateTimeOfDay(Time time) { + return time.hour * 60 * 60 + time.minute * 60 + time.second; + } + + private double calculateTitleMatchScore(Program p1, Program p2) { + // TODO: Use more proper algorithm for matching title. + if (p1 == null | p2 == null || p1.getTitle() == null || p2.getTitle() == null) { + return 0.0; + } + return p1.getTitle().equals(p2.getTitle()) ? 1.0 : 0.0; + } + + private double calculateTimeMatchScore(Program program) { + // TODO: need to refine this heuristic method. + Time curTime = new Time(); + curTime.set(System.currentTimeMillis()); + int curTimeOfDay = calculateTimeOfDay(curTime); + int curWeekDay = curTime.weekDay; + + Time startTime = new Time(); + startTime.set(program.getStartTimeUtcMillis()); + int startTimeOfDay = calculateTimeOfDay(startTime); + + Time endTime = new Time(); + endTime.set(program.getEndTimeUtcMillis()); + int endTimeOfDay = calculateTimeOfDay(endTime); + if (startTimeOfDay > endTimeOfDay) { + if (curTimeOfDay < endTimeOfDay) { + curTimeOfDay += 24 * 60 * 60; + curWeekDay = (curWeekDay + 6) % 7; + } + endTimeOfDay += 24 * 60 * 60; + } + + return calculateTimeOfDayMatchScore(curTimeOfDay, startTimeOfDay, endTimeOfDay) * 0.7 + + calculateDayOfWeekMatchScore(curWeekDay, startTime.weekDay) * 0.3; + } + + private double calculateTimeOfDayMatchScore(int curTimeOfDay, int startTimeOfDay, + int endTimeOfDay) { + // TODO: need to refine this heuristic method. + if (curTimeOfDay >= startTimeOfDay && curTimeOfDay < endTimeOfDay) { + return 1.0; + } + double minDiff = Math.min( + Math.abs(startTimeOfDay - curTimeOfDay), + Math.abs(curTimeOfDay - endTimeOfDay)); + if (minDiff < 3600.0) { + return 1.0 - minDiff / 3600.0; + } + return 0.0; + } + + private double calculateDayOfWeekMatchScore(int curWeekDay, int programWeekDay) { + // TODO: need to refine this heuristic method. + if (curWeekDay == programWeekDay) { + return MATCH_SCORE_DAY_OF_WEEK_MATCHED; + } + switch (curWeekDay) { + case Time.MONDAY: { + switch (programWeekDay) { + case Time.TUESDAY: + return MATCH_SCORE_DAY_OF_WEEK_HIGH; + case Time.WEDNESDAY: + case Time.THURSDAY: + case Time.FRIDAY: + return MATCH_SCORE_DAY_OF_WEEK_LOW; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + case Time.TUESDAY: { + switch (programWeekDay) { + case Time.MONDAY: + return MATCH_SCORE_DAY_OF_WEEK_HIGH; + case Time.WEDNESDAY: + case Time.THURSDAY: + case Time.FRIDAY: + return MATCH_SCORE_DAY_OF_WEEK_LOW; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + case Time.WEDNESDAY: { + switch (programWeekDay) { + case Time.THURSDAY: + return MATCH_SCORE_DAY_OF_WEEK_HIGH; + case Time.MONDAY: + case Time.TUESDAY: + case Time.FRIDAY: + return MATCH_SCORE_DAY_OF_WEEK_LOW; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + case Time.THURSDAY: { + switch (programWeekDay) { + case Time.WEDNESDAY: + return MATCH_SCORE_DAY_OF_WEEK_HIGH; + case Time.MONDAY: + case Time.TUESDAY: + case Time.FRIDAY: + return MATCH_SCORE_DAY_OF_WEEK_LOW; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + case Time.FRIDAY: { + switch (programWeekDay) { + case Time.MONDAY: + case Time.TUESDAY: + case Time.WEDNESDAY: + case Time.THURSDAY: + return MATCH_SCORE_DAY_OF_WEEK_LOW; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + case Time.SATURDAY: { + switch (programWeekDay) { + case Time.SUNDAY: + return MATCH_SCORE_DAY_OF_WEEK_HIGH; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + case Time.SUNDAY: { + switch (programWeekDay) { + case Time.SATURDAY: + return MATCH_SCORE_DAY_OF_WEEK_HIGH; + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } + } + return MATCH_SCORE_DAY_OF_WEEK_NOT_MATCHED; + } +} |