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