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 Chalko
21816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport com.android.tv.data.Channel;
227d67089aa1e9aa2123c3cd2f386d7019a1544db1Nick Chalkoimport com.android.tv.testing.Utils;
23816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
24816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport org.mockito.Matchers;
25816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport org.mockito.Mockito;
26816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport org.mockito.invocation.InvocationOnMock;
27816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport org.mockito.stubbing.Answer;
28816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
29816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.ArrayList;
30816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Collection;
31816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.List;
32816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Random;
33816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.TreeMap;
34816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.concurrent.TimeUnit;
35816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
36816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkopublic class RecommendationUtils {
37816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final String TAG = "RecommendationUtils";
38816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final long INVALID_CHANNEL_ID = -1;
39816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
40816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    /**
41816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     * Create a mock RecommendationDataManager backed by a {@link ChannelRecordSortedMapHelper}.
42816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     */
43816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    public static RecommendationDataManager createMockRecommendationDataManager(
44816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            final ChannelRecordSortedMapHelper channelRecordSortedMap) {
45816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        RecommendationDataManager dataManager = Mockito.mock(RecommendationDataManager.class);
46816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        Mockito.doAnswer(new Answer<Integer>() {
47816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            @Override
48816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            public Integer answer(InvocationOnMock invocation) throws Throwable {
49816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                return channelRecordSortedMap.size();
50816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
51816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }).when(dataManager).getChannelRecordCount();
52816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        Mockito.doAnswer(new Answer<Collection<ChannelRecord>>() {
53816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            @Override
54816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            public Collection<ChannelRecord> answer(InvocationOnMock invocation) throws Throwable {
55816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                return channelRecordSortedMap.values();
56816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
57816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }).when(dataManager).getChannelRecords();
58816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        Mockito.doAnswer(new Answer<ChannelRecord>() {
59816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            @Override
60816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            public ChannelRecord answer(InvocationOnMock invocation) throws Throwable {
61816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                long channelId = (long) invocation.getArguments()[0];
62816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                return channelRecordSortedMap.get(channelId);
63816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
64816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }).when(dataManager).getChannelRecord(Matchers.anyLong());
65816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return dataManager;
66816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
67816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
68816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    public static class ChannelRecordSortedMapHelper extends TreeMap<Long, ChannelRecord> {
6907b043dc3db83d6d20f0e8513b946830ab00e37bNick Chalko        private final Context mContext;
70816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        private Recommender mRecommender;
717d67089aa1e9aa2123c3cd2f386d7019a1544db1Nick Chalko        private Random mRandom = Utils.createTestRandom();
72816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
73816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public ChannelRecordSortedMapHelper(Context context) {
74816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            mContext = context;
75816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
76816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
77816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public void setRecommender(Recommender recommender) {
78816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            mRecommender = recommender;
79816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
80816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
81816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public void resetRandom(Random random) {
82816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            mRandom = random;
83816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
84816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
85816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        /**
86816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * Add new {@code numberOfChannels} channels by adding channel record to
87816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * {@code channelRecordMap} with no history.
88816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * This action corresponds to loading channels in the RecommendationDataManger.
89816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         */
90816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public void addChannels(int numberOfChannels) {
91816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            for (int i = 0; i < numberOfChannels; ++i) {
92816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                addChannel();
93816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
94816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
95816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
96816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        /**
97816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * Add new one channel by adding channel record to {@code channelRecordMap} with no history.
98816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * This action corresponds to loading one channel in the RecommendationDataManger.
99816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         *
100816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * @return The new channel was made by this method.
101816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         */
102816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public Channel addChannel() {
103816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            long channelId = size();
104816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            Channel channel = new Channel.Builder().setId(channelId).build();
105816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            ChannelRecord channelRecord = new ChannelRecord(mContext, channel, false);
106816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            put(channelId, channelRecord);
107816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return channel;
108816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
109816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
110816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        /**
111816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * Add the watch logs which its durationTime is under {@code maxWatchDurationMs}.
112816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * Add until latest watch end time becomes bigger than {@code watchEndTimeMs},
113816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * starting from {@code watchStartTimeMs}.
114816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         *
115816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * @return true if adding watch log success, otherwise false.
116816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         */
117816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public boolean addRandomWatchLogs(long watchStartTimeMs, long watchEndTimeMs,
118816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                long maxWatchDurationMs) {
119816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            long latestWatchEndTimeMs = watchStartTimeMs;
120816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            long previousChannelId = INVALID_CHANNEL_ID;
121816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            List<Long> channelIdList = new ArrayList<>(keySet());
122816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            while (latestWatchEndTimeMs < watchEndTimeMs) {
123816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                long channelId = channelIdList.get(mRandom.nextInt(channelIdList.size()));
124816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                if (previousChannelId == channelId) {
125816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    // Time hopping with random minutes.
126816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1);
127816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                }
128816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1;
129816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) {
130816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    return false;
131816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                }
132816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                latestWatchEndTimeMs += watchedDurationMs;
133816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                previousChannelId = channelId;
134816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
135816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return true;
136816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
137816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
138816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        /**
139816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * Add new watch log to channel that id is {@code ChannelId}. Add watch log starts from
140816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * {@code watchStartTimeMs} with duration {@code durationTimeMs}. If adding is finished,
141816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * notify the recommender that there's a new watch log.
142816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         *
143816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         * @return true if adding watch log success, otherwise false.
144816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko         */
145816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public boolean addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) {
146816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            ChannelRecord channelRecord = get(channelId);
147816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            if (channelRecord == null ||
148816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    watchStartTimeMs + durationTimeMs > System.currentTimeMillis()) {
149816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                return false;
150816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
151816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
152816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            channelRecord.logWatchHistory(new WatchedProgram(null, watchStartTimeMs,
153816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    watchStartTimeMs + durationTimeMs));
154816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            if (mRecommender != null) {
155816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                mRecommender.onNewWatchLog(channelRecord);
156816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
157816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return true;
158816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
159816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
160816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko}
161