diff options
Diffstat (limited to 'src/com/android/tv')
-rw-r--r-- | src/com/android/tv/data/Channel.java | 91 | ||||
-rw-r--r-- | src/com/android/tv/recommendation/TvRecommendation.java | 251 |
2 files changed, 334 insertions, 8 deletions
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 69908a52..d09c69be 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -17,6 +17,7 @@ package com.android.tv.data; import android.content.ContentValues; +import android.database.Cursor; import android.provider.TvContract; /** @@ -29,14 +30,88 @@ public final class Channel { private long mId; private String mServiceName; - private String mType; + private int mType; private int mOriginalNetworkId; private int mTransportStreamId; private String mDisplayNumber; private String mDisplayName; private String mDescription; private boolean mIsBrowsable; - private String mData; + private byte[] mData; + + public static Channel fromCursor(Cursor cursor) { + Channel channel = new Channel(); + int index = cursor.getColumnIndex(TvContract.Channels._ID); + if (index >= 0) { + channel.mId = cursor.getLong(index); + } else { + channel.mId = INVALID_ID; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_SERVICE_NAME); + if (index >= 0) { + channel.mServiceName = cursor.getString(index); + } else { + channel.mServiceName = "serviceName"; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_TYPE); + if (index >= 0) { + channel.mType = cursor.getInt(index); + } else { + channel.mType = 0; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID); + if (index >= 0) { + channel.mTransportStreamId = cursor.getInt(index); + } else { + channel.mTransportStreamId = 0; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID); + if (index >= 0) { + channel.mOriginalNetworkId = cursor.getInt(index); + } else { + channel.mOriginalNetworkId = 0; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NUMBER); + if (index >= 0) { + channel.mDisplayNumber = cursor.getString(index); + } else { + channel.mDisplayNumber = "0"; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NAME); + if (index >= 0) { + channel.mDisplayName = cursor.getString(index); + } else { + channel.mDisplayName = "name"; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DESCRIPTION); + if (index >= 0) { + channel.mDescription = cursor.getString(index); + } else { + channel.mDescription = "description"; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_BROWSABLE); + if (index >= 0) { + channel.mIsBrowsable = cursor.getInt(index) == 1; + } else { + channel.mIsBrowsable = true; + } + + index = cursor.getColumnIndex(TvContract.Channels.COLUMN_DATA); + if (index >= 0) { + channel.mData = cursor.getBlob(index); + } else { + channel.mData = null; + } + return channel; + } private Channel() { // Do nothing. @@ -50,7 +125,7 @@ public final class Channel { return mServiceName; } - public String getType() { + public int getType() { return mType; } @@ -86,7 +161,7 @@ public final class Channel { mIsBrowsable = browsable; } - public String getData() { + public byte[] getData() { return mData; } @@ -145,14 +220,14 @@ public final class Channel { // Fill initial data. mChannel.mId = INVALID_ID; mChannel.mServiceName = "serviceName"; - mChannel.mType = "type"; + mChannel.mType = 0; mChannel.mTransportStreamId = 0; mChannel.mOriginalNetworkId = 0; mChannel.mDisplayNumber = "0"; mChannel.mDisplayName = "name"; mChannel.mDescription = "description"; mChannel.mIsBrowsable = true; - mChannel.mData = ""; + mChannel.mData = null; } public Builder(Channel other) { @@ -170,7 +245,7 @@ public final class Channel { return this; } - public Builder setType(String type) { + public Builder setType(int type) { mChannel.mType = type; return this; } @@ -205,7 +280,7 @@ public final class Channel { return this; } - public Builder setData(String data) { + public Builder setData(byte[] data) { mChannel.mData = data; return this; } diff --git a/src/com/android/tv/recommendation/TvRecommendation.java b/src/com/android/tv/recommendation/TvRecommendation.java new file mode 100644 index 00000000..a37d28a5 --- /dev/null +++ b/src/com/android/tv/recommendation/TvRecommendation.java @@ -0,0 +1,251 @@ +/* + * 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.ContentUris; +import android.content.Context; +import android.content.UriMatcher; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.provider.TvContract; + +import com.android.tv.data.Channel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class TvRecommendation { + private static final long MIN_WATCH_DURATION_MS = 5 * 60 * 1000; // 5 minutes + + private static final UriMatcher sUriMatcher; + private static final int MATCH_CHANNEL_ID = 1; + private static final int MATCH_WATCHED_PROGRAM_ID = 2; + + static { + sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); + sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); + } + + private final Map<Long, ChannelRecord> mChannelRecordMap; + // TODO: Consider to define each observer rather than the list or observers. + private final List<ContentObserver> mContentObservers; + private final Handler mHandler; + private final Context mContext; + + /** + * Create a TV recommendation object. + * + * @param context The context to register {@link ContentObserver}s for + * {@link android.provider.TvContract.Channels} and + * {@link android.provider.TvContract.WatchedPrograms}. + * @param handler The handler to run {@link android.database.ContentObserver#onChange(boolean)} + * on, or null if none. + */ + public TvRecommendation(Context context, Handler handler) { + mContext = context; + mChannelRecordMap = new ConcurrentHashMap<Long, ChannelRecord>(); + mContentObservers = new ArrayList<ContentObserver>(); + mHandler = handler; + registerContentObservers(); + buildChannelRecordMap(); + } + + public void release() { + unregisterContentObservers(); + mChannelRecordMap.clear(); + } + + /** + * Get the channel list of recommendation up to {@code n} or the number of channels. + * + * @param n The number of channels will be recommended. + * @return Top {@code n} channels recommended. If {@code n} is bigger than the number of + * channels, the number of results could be less than {@code n}. + */ + public ChannelRecord[] getRecommendedChannelList(int n) { + if (n > mChannelRecordMap.size()) { + n = mChannelRecordMap.size(); + } + ChannelRecord[] allChannelRecords = + mChannelRecordMap.values().toArray(new ChannelRecord[0]); + Arrays.sort(allChannelRecords, new Comparator<ChannelRecord>() { + @Override + public int compare(ChannelRecord c1, ChannelRecord c2) { + long diff = c1.getLastWatchedTimeMs() - c2.getLastWatchedTimeMs(); + return (diff == 0l) ? 0 : (diff < 0) ? 1 : -1; + } + }); + return Arrays.copyOfRange(allChannelRecords, 0, n); + } + + private void registerContentObservers() { + ContentObserver observer = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (sUriMatcher.match(uri) == 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); + if (cursor != null && cursor.moveToFirst()) { + updateLastWatchedTimeFromWatchedProgramCursor(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } + }; + mContentObservers.add(observer); + mContext.getContentResolver().registerContentObserver( + TvContract.WatchedPrograms.CONTENT_URI, true, observer); + + observer = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (sUriMatcher.match(uri) == 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(Channel.fromCursor(cursor)); + newChannelRecord.setLastWatchedTime(oldChannelRecord == null + ? 0 : oldChannelRecord.getLastWatchedTimeMs()); + mChannelRecordMap.put(channelId, newChannelRecord); + } else { + mChannelRecordMap.remove(channelId); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } + }; + mContentObservers.add(observer); + mContext.getContentResolver().registerContentObserver( + TvContract.Channels.CONTENT_URI, true, observer); + } + + private void unregisterContentObservers() { + for (ContentObserver observer : mContentObservers) { + mContext.getContentResolver().unregisterContentObserver(observer); + } + mContentObservers.clear(); + } + + 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()) { + mChannelRecordMap.put(cursor.getLong(indexId), + new ChannelRecord(Channel.fromCursor(cursor))); + } + } + } finally { + if (cursor != null) { + cursor.close(); + cursor = null; + } + } + + // 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); + if (cursor != null) { + while (cursor.moveToNext()) { + updateLastWatchedTimeFromWatchedProgramCursor(cursor); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private void updateLastWatchedTimeFromWatchedProgramCursor(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); + if (watchEndTimeMs != 0l && watchDurationMs > MIN_WATCH_DURATION_MS) { + ChannelRecord channelRecord = mChannelRecordMap.get( + cursor.getLong(indexWatchChannelId)); + if (channelRecord != null && channelRecord.getLastWatchedTimeMs() < watchEndTimeMs) { + channelRecord.setLastWatchedTime(watchEndTimeMs); + } + } + } + + public static class ChannelRecord { + private final Channel mChannel; + private long mLastWatchedTimeMs; + + public ChannelRecord(Channel channel) { + mChannel = channel; + mLastWatchedTimeMs = 0l; + } + + public Channel getChannel() { + return mChannel; + } + + public long getLastWatchedTimeMs() { + return mLastWatchedTimeMs; + } + + public void setLastWatchedTime(long timeMs) { + mLastWatchedTimeMs = timeMs; + } + } +} |