aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/recommendation
diff options
context:
space:
mode:
authorSungsoo Lim <sungsoo@google.com>2014-05-29 11:38:37 +0900
committerSungsoo Lim <sungsoo@google.com>2014-06-18 15:38:31 +0900
commit3b06c249c2a5fe37b1561b01d105ecd94833e8b4 (patch)
tree9df48e2d6679f87b514ce19a9e21817edd2ae154 /src/com/android/tv/recommendation
parent5170e59d6a735300b4d0e2c1d178c8c36fd4fa96 (diff)
downloadTV-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.java59
-rw-r--r--src/com/android/tv/recommendation/WatchedProgramRecommender.java189
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;
+ }
+}