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.test.AndroidTestCase; 20 import android.test.MoreAsserts; 21 import android.test.suitebuilder.annotation.SmallTest; 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 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 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 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 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 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 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 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 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 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 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 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 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 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 298 private List<Long> getChannelIdListSorted() { 299 return new ArrayList<>(mChannelRecordSortedMap.keySet()); 300 } 301 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 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 324 public void setChannelScore(long channelId, double score) { 325 mChannelScore.put(channelId, score); 326 } 327 } 328 } 329