• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.recommendation;
18 
19 import static android.support.test.InstrumentationRegistry.getContext;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 
24 import android.support.test.filters.SmallTest;
25 import android.test.MoreAsserts;
26 
27 import com.android.tv.data.Channel;
28 import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper;
29 import com.android.tv.testing.Utils;
30 
31 import org.junit.Before;
32 import org.junit.Test;
33 
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.concurrent.TimeUnit;
42 
43 @SmallTest
44 public class RecommenderTest {
45     private static final int DEFAULT_NUMBER_OF_CHANNELS = 5;
46     private static final long DEFAULT_WATCH_START_TIME_MS =
47             System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
48     private static final long DEFAULT_WATCH_END_TIME_MS =
49             System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
50     private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1);
51 
52     private final Comparator<Channel> CHANNEL_SORT_KEY_COMPARATOR = new Comparator<Channel>() {
53         @Override
54         public int compare(Channel lhs, Channel rhs) {
55             return mRecommender.getChannelSortKey(lhs.getId())
56                     .compareTo(mRecommender.getChannelSortKey(rhs.getId()));
57         }
58     };
59     private final Runnable START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS = new Runnable() {
60         @Override
61         public void run() {
62             // Add 4 channels in ChannelRecordMap for testing. Store the added channels to
63             // mChannels_1 ~ mChannels_4. They are sorted by channel id in increasing order.
64             mChannel_1 = mChannelRecordSortedMap.addChannel();
65             mChannel_2 = mChannelRecordSortedMap.addChannel();
66             mChannel_3 = mChannelRecordSortedMap.addChannel();
67             mChannel_4 = mChannelRecordSortedMap.addChannel();
68         }
69     };
70 
71     private RecommendationDataManager mDataManager;
72     private Recommender mRecommender;
73     private FakeEvaluator mEvaluator;
74     private ChannelRecordSortedMapHelper mChannelRecordSortedMap;
75     private boolean mOnRecommenderReady;
76     private boolean mOnRecommendationChanged;
77     private Channel mChannel_1;
78     private Channel mChannel_2;
79     private Channel mChannel_3;
80     private Channel mChannel_4;
81 
82     @Before
setUp()83     public void setUp() {
84         mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext());
85         mDataManager = RecommendationUtils
86                 .createMockRecommendationDataManager(mChannelRecordSortedMap);
87         mChannelRecordSortedMap.resetRandom(Utils.createTestRandom());
88     }
89 
90     @Test
testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore()91     public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() {
92         createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
93 
94         // Recommender doesn't recommend any channels because all channels are not recommended.
95         assertEquals(0, mRecommender.recommendChannels().size());
96         assertEquals(0, mRecommender.recommendChannels(-5).size());
97         assertEquals(0, mRecommender.recommendChannels(0).size());
98         assertEquals(0, mRecommender.recommendChannels(3).size());
99         assertEquals(0, mRecommender.recommendChannels(4).size());
100         assertEquals(0, mRecommender.recommendChannels(5).size());
101     }
102 
103     @Test
testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore()104     public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() {
105         createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
106 
107         // Recommender recommends every channel because it recommends not-recommended channels too.
108         assertEquals(4, mRecommender.recommendChannels().size());
109         assertEquals(0, mRecommender.recommendChannels(-5).size());
110         assertEquals(0, mRecommender.recommendChannels(0).size());
111         assertEquals(3, mRecommender.recommendChannels(3).size());
112         assertEquals(4, mRecommender.recommendChannels(4).size());
113         assertEquals(4, mRecommender.recommendChannels(5).size());
114     }
115 
116     @Test
testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore()117     public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore() {
118         createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
119 
120         setChannelScores_scoreIncreasesAsChannelIdIncreases();
121 
122         // recommendChannels must be sorted by score in decreasing order.
123         // (i.e. sorted by channel ID in decreasing order in this case)
124         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(),
125                 mChannel_4, mChannel_3, mChannel_2, mChannel_1);
126         assertEquals(0, mRecommender.recommendChannels(-5).size());
127         assertEquals(0, mRecommender.recommendChannels(0).size());
128         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3),
129                 mChannel_4, mChannel_3, mChannel_2);
130         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4),
131                 mChannel_4, mChannel_3, mChannel_2, mChannel_1);
132         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5),
133                 mChannel_4, mChannel_3, mChannel_2, mChannel_1);
134     }
135 
136     @Test
testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore()137     public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore() {
138         createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
139 
140         setChannelScores_scoreIncreasesAsChannelIdIncreases();
141 
142         // recommendChannels must be sorted by score in decreasing order.
143         // (i.e. sorted by channel ID in decreasing order in this case)
144         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(),
145                 mChannel_4, mChannel_3, mChannel_2, mChannel_1);
146         assertEquals(0, mRecommender.recommendChannels(-5).size());
147         assertEquals(0, mRecommender.recommendChannels(0).size());
148         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3),
149                 mChannel_4, mChannel_3, mChannel_2);
150         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4),
151                 mChannel_4, mChannel_3, mChannel_2, mChannel_1);
152         MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5),
153                 mChannel_4, mChannel_3, mChannel_2, mChannel_1);
154     }
155 
156     @Test
testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore()157     public void testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore() {
158         createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
159 
160         mEvaluator.setChannelScore(mChannel_1.getId(), 1.0);
161         mEvaluator.setChannelScore(mChannel_2.getId(), 1.0);
162 
163         // Only two channels are recommended because recommender doesn't recommend other channels.
164         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(),
165                 mChannel_1, mChannel_2);
166         assertEquals(0, mRecommender.recommendChannels(-5).size());
167         assertEquals(0, mRecommender.recommendChannels(0).size());
168         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3),
169                 mChannel_1, mChannel_2);
170         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4),
171                 mChannel_1, mChannel_2);
172         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5),
173                 mChannel_1, mChannel_2);
174     }
175 
176     @Test
testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore()177     public void testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore() {
178         createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
179 
180         mEvaluator.setChannelScore(mChannel_1.getId(), 1.0);
181         mEvaluator.setChannelScore(mChannel_2.getId(), 1.0);
182 
183         assertEquals(4, mRecommender.recommendChannels().size());
184         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels().subList(0, 2),
185                 mChannel_1, mChannel_2);
186 
187         assertEquals(0, mRecommender.recommendChannels(-5).size());
188         assertEquals(0, mRecommender.recommendChannels(0).size());
189 
190         assertEquals(3, mRecommender.recommendChannels(3).size());
191         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3).subList(0, 2),
192                 mChannel_1, mChannel_2);
193 
194         assertEquals(4, mRecommender.recommendChannels(4).size());
195         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4).subList(0, 2),
196                 mChannel_1, mChannel_2);
197 
198         assertEquals(4, mRecommender.recommendChannels(5).size());
199         MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5).subList(0, 2),
200                 mChannel_1, mChannel_2);
201     }
202 
203     @Test
testGetChannelSortKey_recommendAllChannels()204     public void testGetChannelSortKey_recommendAllChannels() {
205         createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
206 
207         setChannelScores_scoreIncreasesAsChannelIdIncreases();
208 
209         List<Channel> expectedChannelList = mRecommender.recommendChannels();
210         List<Channel> channelList = Arrays.asList(mChannel_1, mChannel_2, mChannel_3, mChannel_4);
211         Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR);
212 
213         // Recommended channel list and channel list sorted by sort key must be the same.
214         MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray());
215         assertSortKeyNotInvalid(channelList);
216     }
217 
218     @Test
testGetChannelSortKey_recommendFewChannels()219     public void testGetChannelSortKey_recommendFewChannels() {
220         // Test with recommending 3 channels.
221         createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
222 
223         setChannelScores_scoreIncreasesAsChannelIdIncreases();
224 
225         List<Channel> expectedChannelList = mRecommender.recommendChannels(3);
226         // A channel which is not recommended by the recommender has to get an invalid sort key.
227         assertEquals(Recommender.INVALID_CHANNEL_SORT_KEY,
228                 mRecommender.getChannelSortKey(mChannel_1.getId()));
229 
230         List<Channel> channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4);
231         Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR);
232 
233         MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray());
234         assertSortKeyNotInvalid(channelList);
235     }
236 
237     @Test
testListener_onRecommendationChanged()238     public void testListener_onRecommendationChanged() {
239         createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
240         // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel
241         // doesn't have a watch log, nothing is recommended and recommendation isn't changed.
242         assertFalse(mOnRecommendationChanged);
243 
244         // Set lastRecommendationUpdatedTimeUtcMs to check recommendation changed because,
245         // recommender has a minimum recommendation update period.
246         mRecommender.setLastRecommendationUpdatedTimeUtcMs(
247                 System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10));
248         long latestWatchEndTimeMs = DEFAULT_WATCH_START_TIME_MS;
249         for (long channelId : mChannelRecordSortedMap.keySet()) {
250             mEvaluator.setChannelScore(channelId, 1.0);
251             // Add a log to recalculate the recommendation score.
252             assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, latestWatchEndTimeMs,
253                     TimeUnit.MINUTES.toMillis(10)));
254             latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10);
255         }
256 
257         // onRecommendationChanged must be called, because recommend channels are not empty,
258         // by setting score to each channel.
259         assertTrue(mOnRecommendationChanged);
260     }
261 
262     @Test
testListener_onRecommenderReady()263     public void testListener_onRecommenderReady() {
264         createRecommender(true, new Runnable() {
265             @Override
266             public void run() {
267                 mChannelRecordSortedMap.addChannels(DEFAULT_NUMBER_OF_CHANNELS);
268                 mChannelRecordSortedMap.addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS,
269                         DEFAULT_WATCH_END_TIME_MS, DEFAULT_MAX_WATCH_DURATION_MS);
270             }
271         });
272 
273         // After loading channels and watch logs are finished, recommender must be available to use.
274         assertTrue(mOnRecommenderReady);
275     }
276 
assertSortKeyNotInvalid(List<Channel> channelList)277     private void assertSortKeyNotInvalid(List<Channel> channelList) {
278         for (Channel channel : channelList) {
279             MoreAsserts.assertNotEqual(Recommender.INVALID_CHANNEL_SORT_KEY,
280                     mRecommender.getChannelSortKey(channel.getId()));
281         }
282     }
283 
createRecommender(boolean includeRecommendedOnly, Runnable startDataManagerRunnable)284     private void createRecommender(boolean includeRecommendedOnly,
285             Runnable startDataManagerRunnable) {
286         mRecommender = new Recommender(new Recommender.Listener() {
287             @Override
288             public void onRecommenderReady() {
289                 mOnRecommenderReady = true;
290             }
291             @Override
292             public void onRecommendationChanged() {
293                 mOnRecommendationChanged = true;
294             }
295         }, includeRecommendedOnly, mDataManager);
296 
297         mEvaluator = new FakeEvaluator();
298         mRecommender.registerEvaluator(mEvaluator);
299         mChannelRecordSortedMap.setRecommender(mRecommender);
300 
301         // When mRecommender is instantiated, its dataManager will be started, and load channels
302         // and watch history data if it is not started.
303         if (startDataManagerRunnable != null) {
304             startDataManagerRunnable.run();
305             mRecommender.onChannelRecordChanged();
306         }
307         // After loading channels and watch history data are finished,
308         // RecommendationDataManager calls listener.onChannelRecordLoaded()
309         // which will be mRecommender.onChannelRecordLoaded().
310         mRecommender.onChannelRecordLoaded();
311     }
312 
getChannelIdListSorted()313     private List<Long> getChannelIdListSorted() {
314         return new ArrayList<>(mChannelRecordSortedMap.keySet());
315     }
316 
setChannelScores_scoreIncreasesAsChannelIdIncreases()317     private void setChannelScores_scoreIncreasesAsChannelIdIncreases() {
318         List<Long> channelIdList = getChannelIdListSorted();
319         double score = Math.pow(0.5, channelIdList.size());
320         for (long channelId : channelIdList) {
321             // Channel with smaller id has smaller score than channel with higher id.
322             mEvaluator.setChannelScore(channelId, score);
323             score *= 2.0;
324         }
325     }
326 
327     private class FakeEvaluator extends Recommender.Evaluator {
328         private final Map<Long, Double> mChannelScore = new HashMap<>();
329 
330         @Override
evaluateChannel(long channelId)331         public double evaluateChannel(long channelId) {
332             if (getRecommender().getChannelRecord(channelId) == null) {
333                 return NOT_RECOMMENDED;
334             }
335             Double score = mChannelScore.get(channelId);
336             return score == null ? NOT_RECOMMENDED : score;
337         }
338 
setChannelScore(long channelId, double score)339         public void setChannelScore(long channelId, double score) {
340             mChannelScore.put(channelId, score);
341         }
342     }
343 }
344