diff options
Diffstat (limited to 'src/com/android/tv/recommendation/TvRecommendation.java')
-rw-r--r-- | src/com/android/tv/recommendation/TvRecommendation.java | 481 |
1 files changed, 0 insertions, 481 deletions
diff --git a/src/com/android/tv/recommendation/TvRecommendation.java b/src/com/android/tv/recommendation/TvRecommendation.java deleted file mode 100644 index 6483eb0d..00000000 --- a/src/com/android/tv/recommendation/TvRecommendation.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * 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.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.UriMatcher; -import android.database.ContentObserver; -import android.database.Cursor; -import android.graphics.Bitmap; -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 { - private static final String TAG = "TvRecommendation"; - - private static final String PATH_INPUT = "input"; - - private static final UriMatcher sUriMatcher; - private static final int MATCH_CHANNEL = 1; - private static final int MATCH_CHANNEL_ID = 2; - private static final int MATCH_WATCHED_PROGRAM_ID = 3; - private static final int MATCH_INPUT_ID_CHANNEL = 4; - - static { - sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL); - sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); - sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); - sUriMatcher.addURI(TvContract.AUTHORITY, "input/*/channel", MATCH_INPUT_ID_CHANNEL); - } - - private final List<TvRecommenderWrapper> mTvRecommenders; - // 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. - * - * @param context The context to register {@link ContentObserver}s for - * {@link android.media.tv.TvContract.Channels} and - * {@link android.media.tv.TvContract.WatchedPrograms}. - * @param handler The handler to run {@link android.database.ContentObserver#onChange(boolean)} - * on, or null if none. - * @param includeRecommendedOnly true to include only recommended results, or false. - */ - public TvRecommendation(Context context, Handler handler, boolean includeRecommendedOnly) { - mContext = context; - mChannelRecordMap = new ConcurrentHashMap<Long, ChannelRecord>(); - mHandler = handler; - mContentObserver = createContentObserver(); - mIncludeRecommendedOnly = includeRecommendedOnly; - mTvRecommenders = new ArrayList<TvRecommenderWrapper>(); - registerContentObservers(); - buildChannelRecordMap(); - } - - private ContentObserver createContentObserver() { - return new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange, Uri uri) { - int match = sUriMatcher.match(uri); - if (match == MATCH_CHANNEL) { - Map<Long, ChannelRecord> channelRecordMap = - new ConcurrentHashMap<Long, ChannelRecord>(); - - Cursor c = null; - try { - c = mContext.getContentResolver().query(uri, null, null, null, null); - if (c != null) { - int channelIdIndex = c.getColumnIndex(Channels._ID); - long channelId; - while (c.moveToNext()) { - channelId = c.getLong(channelIdIndex); - ChannelRecord oldChannelRecord = mChannelRecordMap.get(channelId); - ChannelRecord newChannelRecord = - new ChannelRecord(mContext, Channel.fromCursor(c)); - newChannelRecord.mLastWatchedTimeMs = (oldChannelRecord == null) - ? 0 : oldChannelRecord.mLastWatchedTimeMs; - channelRecordMap.put(channelId, newChannelRecord); - } - } - } finally { - if (c != null) { - c.close(); - } - } - mChannelRecordMap = channelRecordMap; - } else if (match == MATCH_CHANNEL_ID) { - long channelId = ContentUris.parseId(uri); - Cursor cursor = null; - try { - cursor = mContext.getContentResolver().query( - uri, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - ChannelRecord oldChannelRecord = mChannelRecordMap.get(channelId); - ChannelRecord newChannelRecord = - new ChannelRecord(mContext, Channel.fromCursor(cursor)); - newChannelRecord.mLastWatchedTimeMs = (oldChannelRecord == null) - ? 0 : oldChannelRecord.mLastWatchedTimeMs; - mChannelRecordMap.put(channelId, newChannelRecord); - } else { - mChannelRecordMap.remove(channelId); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } else if (match == MATCH_INPUT_ID_CHANNEL) { - String inputId = TvContract.getInputId(uri); - - Set<Long> channelIdSet = new HashSet<Long>(); - for (ChannelRecord cr : mChannelRecordMap.values()) { - if (inputId.equals(cr.mChannel.getInputId())) { - channelIdSet.add(cr.mChannel.getId()); - } - } - - Uri inputUri = TvContract.buildChannelsUriForInput(inputId, false); - Cursor c = null; - try { - c = mContext.getContentResolver().query(inputUri, null, null, null, null); - if (c != null) { - int channelIdIndex = c.getColumnIndex(Channels._ID); - long channelId; - while (c.moveToNext()) { - channelId = c.getLong(channelIdIndex); - ChannelRecord oldChannelRecord = mChannelRecordMap.get(channelId); - ChannelRecord newChannelRecord = - new ChannelRecord(mContext, Channel.fromCursor(c)); - newChannelRecord.mLastWatchedTimeMs = (oldChannelRecord == null) - ? 0 : oldChannelRecord.mLastWatchedTimeMs; - mChannelRecordMap.put(channelId, newChannelRecord); - channelIdSet.remove(channelId); - } - } - } finally { - if (c != null) { - c.close(); - } - } - - for (Long channelId : channelIdSet) { - mChannelRecordMap.remove(channelId); - } - } else if (match == MATCH_WATCHED_PROGRAM_ID) { - Cursor cursor = null; - try { - cursor = mContext.getContentResolver().query( - uri, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - ChannelRecord channelRecord = - updateChannelRecordFromWatchedProgramCursor(cursor); - for (TvRecommenderWrapper recommender : mTvRecommenders) { - recommender.onNewWatchLog(channelRecord); - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - } - }; - } - - public void release() { - unregisterContentObservers(); - mChannelRecordMap.clear(); - } - - public void registerTvRecommender(TvRecommender recommender) { - registerTvRecommender(recommender, - TvRecommenderWrapper.DEFAULT_BASE_SCORE, TvRecommenderWrapper.DEFAULT_WEIGHT); - } - - public void registerTvRecommender(TvRecommender recommender, double baseScore, double weight) { - mTvRecommenders.add(new TvRecommenderWrapper(recommender, baseScore, weight)); - } - - /** - * Get the channel list of recommendation up to {@code n} or the number of channels. - * - * @param size The number of channels that might be recommended. - * @return Top {@code size} channels recommended. If {@code size} is bigger than the number of - * channels, the number of results could be less than {@code size}. - */ - // TODO: consider to change the return type from ChannelRecord[] to Channel[] - public ChannelRecord[] getRecommendedChannelList(int size) { - ArrayList<ChannelRecord> results = new ArrayList<ChannelRecord>(); - for (ChannelRecord cr : mChannelRecordMap.values()) { - double maxScore = TvRecommender.NOT_RECOMMENDED; - for (TvRecommenderWrapper recommender : mTvRecommenders) { - double score = recommender.calculateScaledScore(cr); - if (score > maxScore) { - maxScore = score; - } - } - cr.mScore = maxScore; - if (!mIncludeRecommendedOnly || cr.mScore != TvRecommender.NOT_RECOMMENDED) { - results.add(cr); - } - } - ChannelRecord[] allChannelRecords = results.toArray(new ChannelRecord[0]); - if (size > allChannelRecords.length) { - size = allChannelRecords.length; - } - Arrays.sort(allChannelRecords); - return Arrays.copyOfRange(allChannelRecords, 0, size); - } - - public ChannelRecord[] getRecommendedChannelList() { - return getRecommendedChannelList(mChannelRecordMap.size()); - } - - private void registerContentObservers() { - mContext.getContentResolver().registerContentObserver( - TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver); - mContext.getContentResolver().registerContentObserver( - TvContract.Channels.CONTENT_URI, true, mContentObserver); - mContext.getContentResolver().registerContentObserver( - new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(TvContract.AUTHORITY).appendPath(PATH_INPUT).build(), - true, mContentObserver); - } - - private void unregisterContentObservers() { - mContext.getContentResolver().unregisterContentObserver(mContentObserver); - } - - private void buildChannelRecordMap() { - // register channels into channel map. - Cursor cursor = null; - try { - cursor = mContext.getContentResolver().query( - TvContract.Channels.CONTENT_URI, null, null, null, null); - if (cursor != null) { - int indexId = cursor.getColumnIndex(TvContract.Channels._ID); - while (cursor.moveToNext()) { - ChannelRecord cr = new ChannelRecord(mContext, Channel.fromCursor(cursor)); - mChannelRecordMap.put(cursor.getLong(indexId), cr); - } - } - } finally { - if (cursor != null) { - cursor.close(); - cursor = null; - } - } - - // update last watched time for channels. - try { - cursor = mContext.getContentResolver().query( - TvContract.WatchedPrograms.CONTENT_URI, null, null, null, null); - if (cursor != null) { - while (cursor.moveToNext()) { - updateChannelRecordFromWatchedProgramCursor(cursor); - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - 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( - TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); - final int indexWatchChannelId = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_CHANNEL_ID); - - long watchEndTimeMs = cursor.getLong(indexWatchEndTime); - long watchDurationMs = watchEndTimeMs - cursor.getLong(indexWatchStartTime); - ChannelRecord channelRecord = null; - if (watchEndTimeMs != 0l) { - channelRecord = mChannelRecordMap.get( - cursor.getLong(indexWatchChannelId)); - if (channelRecord != null && channelRecord.mLastWatchedTimeMs < watchEndTimeMs) { - channelRecord.mLastWatchedTimeMs = watchEndTimeMs; - channelRecord.mLastWatchDurationMs = watchDurationMs; - Program program = createProgramFromWatchedProgramCursor(cursor); - if (program != null) { - channelRecord.logWatchHistory(program); - } - } - } - return channelRecord; - } - - 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; - - public ChannelRecord(Context context, Channel channel) { - mChannel = channel; - mChannelUri = ContentUris.withAppendedId(TvContract.Channels.CONTENT_URI, - channel.getId()); - mWatchHistory = new ArrayDeque<Program>(); - mLastWatchedTimeMs = 0l; - mLastWatchDurationMs = 0; - mChannel.loadLogo(context, this); - } - - public Channel getChannel() { - return mChannel; - } - - public Uri getChannelUri() { - return mChannelUri; - } - - public long getLastWatchedTimeMs() { - return mLastWatchedTimeMs; - } - - public long getLastWatchDurationMs() { - return mLastWatchDurationMs; - } - - public double getRecommendationScore() { - 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. - return (mScore == another.mScore) ? 0 : (mScore > another.mScore) ? -1 : 1; - } - - @Override - public void onLoadLogoFinished(Channel channel, Bitmap logo) { - // do nothing - } - } - - public static abstract class TvRecommender { - public static final double NOT_RECOMMENDED = -1.0; - - /** - * This will be called when a new watch log comes into WatchedPrograms table. - */ - protected void onNewWatchLog(ChannelRecord channelRecord) { - } - - /** - * The implementation should return the calculated score for the given channel record. - * The return value should be in the range of [0.0, 1.0] or NOT_RECOMMENDED for denoting - * that it gives up to calculate the score for the channel. - * - * @param cr The channel record which will be evaluated by this recommender. - * @return The recommendation score - */ - protected abstract double calculateScore(final ChannelRecord cr); - } - - private static class TvRecommenderWrapper { - private static final double DEFAULT_BASE_SCORE = 0.0; - private static final double DEFAULT_WEIGHT = 1.0; - - private final TvRecommender mRecommender; - // The minimum score of the TvRecommender unless it gives up to provide the score. - private final double mBaseScore; - // The weight of the recommender. The return-value of getScore() will be multiplied by - // this value. - private final double mWeight; - - public TvRecommenderWrapper(TvRecommender recommender, double baseScore, double weight) { - mRecommender = recommender; - mBaseScore = baseScore; - mWeight = weight; - } - - /** - * This returns the scaled score for the given channel record based on the returned value - * of calculateScore(). - * - * @param channelRecord The channel record which will be evaluated by the recommender. - * @return Returns the scaled score (mBaseScore + score * mWeight) when calculateScore() is - * in the range of [0.0, 1.0]. If calculateScore() returns NOT_RECOMMENDED or any negative - * numbers, it returns NOT_RECOMMENDED. If calculateScore() returns more than 1.0, it - * returns (mBaseScore + mWeight). - */ - public double calculateScaledScore(final ChannelRecord channelRecord) { - double score = mRecommender.calculateScore(channelRecord); - if (score < 0.0) { - if (score != TvRecommender.NOT_RECOMMENDED) { - Log.w(TAG, "Unexpected scroe (" + score + ") from the recommender" - + mRecommender); - } - // If the recommender gives up to calculate the score, return 0.0 - return TvRecommender.NOT_RECOMMENDED; - } else if (score > 1.0) { - Log.w(TAG, "Unexpected scroe (" + score + ") from the recommender" - + mRecommender); - score = 1.0; - } - return mBaseScore + score * mWeight; - } - - public void onNewWatchLog(ChannelRecord channelRecord) { - mRecommender.onNewWatchLog(channelRecord); - } - } -} |