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