Home | History | Annotate | Download | only in recommendation
      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