1816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko/* 2816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Copyright (C) 2015 The Android Open Source Project 3816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 4816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Licensed under the Apache License, Version 2.0 (the "License"); 5816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * you may not use this file except in compliance with the License. 6816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * You may obtain a copy of the License at 7816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 8816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * http://www.apache.org/licenses/LICENSE-2.0 9816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 10816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Unless required by applicable law or agreed to in writing, software 11816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * distributed under the License is distributed on an "AS IS" BASIS, 12816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * See the License for the specific language governing permissions and 14816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * limitations under the License. 15816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 16816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 17816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkopackage com.android.tv.recommendation; 18816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 19816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport android.content.Context; 20816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport android.support.annotation.VisibleForTesting; 21816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport android.util.Log; 22816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport android.util.Pair; 23816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 24816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport com.android.tv.data.Channel; 25816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 26816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.ArrayList; 27816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Collection; 28816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Collections; 29816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Comparator; 30816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.HashMap; 31816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.List; 32816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Map; 33816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.concurrent.TimeUnit; 34816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 35816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkopublic class Recommender implements RecommendationDataManager.Listener { 36816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private static final String TAG = "Recommender"; 37816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 38816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @VisibleForTesting 39816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko static final String INVALID_CHANNEL_SORT_KEY = "INVALID"; 40816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private static final long MINIMUM_RECOMMENDATION_UPDATE_PERIOD = TimeUnit.MINUTES.toMillis(5); 41816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private static final Comparator<Pair<Channel, Double>> mChannelScoreComparator = 42816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko new Comparator<Pair<Channel, Double>>() { 43816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @Override 44816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public int compare(Pair<Channel, Double> lhs, Pair<Channel, Double> rhs) { 45816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko // Sort the scores with descending order. 46816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return rhs.second.compareTo(lhs.second); 47816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 48816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko }; 49816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 50816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final List<EvaluatorWrapper> mEvaluators = new ArrayList<>(); 51816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final boolean mIncludeRecommendedOnly; 52816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final Listener mListener; 53816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 54816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final Map<Long, String> mChannelSortKey = new HashMap<>(); 55816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final RecommendationDataManager mDataManager; 56816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private List<Channel> mPreviousRecommendedChannels = new ArrayList<>(); 57816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private long mLastRecommendationUpdatedTimeUtcMillis; 58816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private boolean mChannelRecordLoaded; 59816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 60816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 61816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Create a recommender object. 62816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 63816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param includeRecommendedOnly true to include only recommended results, or false. 64816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 65816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public Recommender(Context context, Listener listener, boolean includeRecommendedOnly) { 66816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mListener = listener; 67816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mIncludeRecommendedOnly = includeRecommendedOnly; 68816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mDataManager = RecommendationDataManager.acquireManager(context, this); 69816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 70816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 71816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @VisibleForTesting 72816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko Recommender(Listener listener, boolean includeRecommendedOnly, 73816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko RecommendationDataManager dataManager) { 74816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mListener = listener; 75816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mIncludeRecommendedOnly = includeRecommendedOnly; 76816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mDataManager = dataManager; 77816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 78816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 79816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public boolean isReady() { 80816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return mChannelRecordLoaded; 81816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 82816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 83816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void release() { 84816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mDataManager.release(this); 85816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 86816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 87816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void registerEvaluator(Evaluator evaluator) { 88816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko registerEvaluator(evaluator, 89816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko EvaluatorWrapper.DEFAULT_BASE_SCORE, EvaluatorWrapper.DEFAULT_WEIGHT); 90816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 91816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 92816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 93816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Register the evaluator used in recommendation. 94816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 95816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * The range of evaluated scores by this evaluator will be between {@code baseScore} and 96816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * {@code baseScore} + {@code weight} (inclusive). 97816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 98816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param evaluator The evaluator to register inside this recommender. 99816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param baseScore Base(Minimum) score of the score evaluated by {@code evaluator}. 100816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param weight Weight value to rearrange the score evaluated by {@code evaluator}. 101816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 102816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void registerEvaluator(Evaluator evaluator, double baseScore, double weight) { 103816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mEvaluators.add(new EvaluatorWrapper(this, evaluator, baseScore, weight)); 104816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 105816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 106816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public List<Channel> recommendChannels() { 107816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return recommendChannels(mDataManager.getChannelRecordCount()); 108816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 109816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 110816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 111816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Return the channel list of recommendation up to {@code n} or the number of channels. 112816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * During the evaluation, this method updates the channel sort key of recommended channels. 113816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 114816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param size The number of channels that might be recommended. 115816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @return Top {@code size} channels recommended sorted by score in descending order. If 116816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * {@code size} is bigger than the number of channels, the number of results could 117816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * be less than {@code size}. 118816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 119816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public List<Channel> recommendChannels(int size) { 120816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko List<Pair<Channel, Double>> records = new ArrayList<>(); 121816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko Collection<ChannelRecord> channelRecordList = mDataManager.getChannelRecords(); 122816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko for (ChannelRecord cr : channelRecordList) { 123816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko double maxScore = Evaluator.NOT_RECOMMENDED; 124816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko for (EvaluatorWrapper evaluator : mEvaluators) { 125816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko double score = evaluator.getScaledEvaluatorScore(cr.getChannel().getId()); 126816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (score > maxScore) { 127816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko maxScore = score; 128816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 129816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 130816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (!mIncludeRecommendedOnly || maxScore != Evaluator.NOT_RECOMMENDED) { 131816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko records.add(new Pair<>(cr.getChannel(), maxScore)); 132816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 133816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 134816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (size > records.size()) { 135816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko size = records.size(); 136816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 137816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko Collections.sort(records, mChannelScoreComparator); 138816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 139816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko List<Channel> results = new ArrayList<>(); 140816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 141816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mChannelSortKey.clear(); 142816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko String sortKeyFormat = "%0" + String.valueOf(size).length() + "d"; 143816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko for (int i = 0; i < size; ++i) { 144816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko // Channel with smaller sort key has higher priority. 145816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mChannelSortKey.put(records.get(i).first.getId(), String.format(sortKeyFormat, i)); 146816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko results.add(records.get(i).first); 147816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 1482e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko return results; 149816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 150816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 151816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 152816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Returns the {@link Channel} object for a given channel ID from the channel pool that this 153816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * recommendation engine has. 154816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 155816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param channelId The channel ID to retrieve the {@link Channel} object for. 156816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @return the {@link Channel} object for the given channel ID, {@code null} if such a channel 157816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * is not found. 158816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 159816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public Channel getChannel(long channelId) { 160816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko ChannelRecord record = mDataManager.getChannelRecord(channelId); 161816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return record == null ? null : record.getChannel(); 162816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 163816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 164816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 165816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Returns the {@link ChannelRecord} object for a given channel ID. 166816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 167816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param channelId The channel ID to receive the {@link ChannelRecord} object for. 168816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @return the {@link ChannelRecord} object for the given channel ID. 169816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 170816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public ChannelRecord getChannelRecord(long channelId) { 171816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return mDataManager.getChannelRecord(channelId); 172816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 173816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 174816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 175816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Returns the sort key of a given channel Id. Sort key is determined in 176816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * {@link #recommendChannels()} and getChannelSortKey must be called after that. 177816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 178816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * If getChannelSortKey was called before evaluating the channels or trying to get sort key 179816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * of non-recommended channel, it returns {@link #INVALID_CHANNEL_SORT_KEY}. 180816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 181816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public String getChannelSortKey(long channelId) { 182816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko String key = mChannelSortKey.get(channelId); 183816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return key == null ? INVALID_CHANNEL_SORT_KEY : key; 184816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 185816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 186816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @Override 187816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void onChannelRecordLoaded() { 188816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mChannelRecordLoaded = true; 189816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mListener.onRecommenderReady(); 190816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko List<ChannelRecord> channels = new ArrayList<>(mDataManager.getChannelRecords()); 191816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko for (EvaluatorWrapper evaluator : mEvaluators) { 192816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko evaluator.onChannelListChanged(Collections.unmodifiableList(channels)); 193816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 194816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 195816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 196816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @Override 197816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void onNewWatchLog(ChannelRecord channelRecord) { 198816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko for (EvaluatorWrapper evaluator : mEvaluators) { 199816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko evaluator.onNewWatchLog(channelRecord); 200816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 201816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko checkRecommendationChanged(); 202816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 203816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 204816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @Override 205816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void onChannelRecordChanged() { 206816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (mChannelRecordLoaded) { 207816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko List<ChannelRecord> channels = new ArrayList<>(mDataManager.getChannelRecords()); 208816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko for (EvaluatorWrapper evaluator : mEvaluators) { 209816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko evaluator.onChannelListChanged(Collections.unmodifiableList(channels)); 210816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 211816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 212816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko checkRecommendationChanged(); 213816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 214816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 215816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private void checkRecommendationChanged() { 216816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko long currentTimeUtcMillis = System.currentTimeMillis(); 217816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (currentTimeUtcMillis - mLastRecommendationUpdatedTimeUtcMillis 218816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko < MINIMUM_RECOMMENDATION_UPDATE_PERIOD) { 219816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return; 220816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 221816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mLastRecommendationUpdatedTimeUtcMillis = currentTimeUtcMillis; 222816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko List<Channel> recommendedChannels = recommendChannels(); 223816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (!recommendedChannels.equals(mPreviousRecommendedChannels)) { 224816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mPreviousRecommendedChannels = recommendedChannels; 225816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mListener.onRecommendationChanged(); 226816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 227816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 228816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 229816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko @VisibleForTesting 230816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko void setLastRecommendationUpdatedTimeUtcMs(long newUpdatedTimeMs) { 231816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mLastRecommendationUpdatedTimeUtcMillis = newUpdatedTimeMs; 232816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 233816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 234816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public static abstract class Evaluator { 235816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public static final double NOT_RECOMMENDED = -1.0; 236816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private Recommender mRecommender; 237816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 238816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko protected Evaluator() {} 239816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 240816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko protected void onChannelRecordListChanged(List<ChannelRecord> channelRecords) { 241816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 242816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 243816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 244816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * This will be called when a new watch log comes into WatchedPrograms table. 245816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 246816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param channelRecord The channel record corresponds to the new watch log. 247816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 248816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko protected void onNewWatchLog(ChannelRecord channelRecord) { 249816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 250816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 251816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 252816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * The implementation should return the recommendation score for the given channel ID. 253816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * The return value should be in the range of [0.0, 1.0] or NOT_RECOMMENDED for denoting 254816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * that it gives up to calculate the score for the channel. 255816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 256816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param channelId The channel ID which will be evaluated by this recommender. 257816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @return The recommendation score 258816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 259816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko protected abstract double evaluateChannel(final long channelId); 260816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 261816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko protected void setRecommender(Recommender recommender) { 262816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mRecommender = recommender; 263816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 264816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 265816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko protected Recommender getRecommender() { 266816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return mRecommender; 267816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 268816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 269816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 270816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private static class EvaluatorWrapper { 271816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private static final double DEFAULT_BASE_SCORE = 0.0; 272816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private static final double DEFAULT_WEIGHT = 1.0; 273816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 274816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final Evaluator mEvaluator; 275816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko // The minimum score of the Recommender unless it gives up to provide the score. 276816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final double mBaseScore; 277816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko // The weight of the recommender. The return-value of getScore() will be multiplied by 278816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko // this value. 279816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private final double mWeight; 280816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 281816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public EvaluatorWrapper(Recommender recommender, Evaluator evaluator, 282816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko double baseScore, double weight) { 283816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mEvaluator = evaluator; 284816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko evaluator.setRecommender(recommender); 285816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mBaseScore = baseScore; 286816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mWeight = weight; 287816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 288816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 289816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 290816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * This returns the scaled score for the given channel ID based on the returned value 291816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * of evaluateChannel(). 292816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * 293816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @param channelId The channel ID which will be evaluated by the recommender. 294816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * @return Returns the scaled score (mBaseScore + score * mWeight) when evaluateChannel() is 295816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * in the range of [0.0, 1.0]. If evaluateChannel() returns NOT_RECOMMENDED or any 296816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * negative numbers, it returns NOT_RECOMMENDED. If calculateScore() returns more 297816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * than 1.0, it returns (mBaseScore + mWeight). 298816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 299816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko private double getScaledEvaluatorScore(long channelId) { 300816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko double score = mEvaluator.evaluateChannel(channelId); 301816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (score < 0.0) { 302816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko if (score != Evaluator.NOT_RECOMMENDED) { 303816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko Log.w(TAG, "Unexpected score (" + score + ") from the recommender" 304816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko + mEvaluator); 305816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 306816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko // If the recommender gives up to calculate the score, return 0.0 307816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return Evaluator.NOT_RECOMMENDED; 308816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } else if (score > 1.0) { 309816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko Log.w(TAG, "Unexpected score (" + score + ") from the recommender" 310816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko + mEvaluator); 311816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko score = 1.0; 312816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 313816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko return mBaseScore + score * mWeight; 314816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 315816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 316816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void onNewWatchLog(ChannelRecord channelRecord) { 317816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mEvaluator.onNewWatchLog(channelRecord); 318816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 319816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 320816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public void onChannelListChanged(List<ChannelRecord> channelRecords) { 321816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko mEvaluator.onChannelRecordListChanged(channelRecords); 322816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 323816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 324816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 325816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko public interface Listener { 326816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 327816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Called after channel record map is loaded. 328816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 329816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko void onRecommenderReady(); 330816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko 331816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko /** 332816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Called when the recommendation changes. 333816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */ 334816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko void onRecommendationChanged(); 335816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko } 336816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko} 337