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.content.Context; 20 import com.android.tv.data.ChannelImpl; 21 import com.android.tv.data.api.Channel; 22 import com.android.tv.testing.utils.Utils; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.List; 26 import java.util.Random; 27 import java.util.TreeMap; 28 import java.util.concurrent.TimeUnit; 29 import org.mockito.Matchers; 30 import org.mockito.Mockito; 31 import org.mockito.invocation.InvocationOnMock; 32 import org.mockito.stubbing.Answer; 33 34 public class RecommendationUtils { 35 private static final long INVALID_CHANNEL_ID = -1; 36 37 /** Create a mock RecommendationDataManager backed by a {@link ChannelRecordSortedMapHelper}. */ createMockRecommendationDataManager( final ChannelRecordSortedMapHelper channelRecordSortedMap)38 public static RecommendationDataManager createMockRecommendationDataManager( 39 final ChannelRecordSortedMapHelper channelRecordSortedMap) { 40 RecommendationDataManager dataManager = Mockito.mock(RecommendationDataManager.class); 41 Mockito.doAnswer( 42 new Answer<Integer>() { 43 @Override 44 public Integer answer(InvocationOnMock invocation) throws Throwable { 45 return channelRecordSortedMap.size(); 46 } 47 }) 48 .when(dataManager) 49 .getChannelRecordCount(); 50 Mockito.doAnswer( 51 new Answer<Collection<ChannelRecord>>() { 52 @Override 53 public Collection<ChannelRecord> answer(InvocationOnMock invocation) 54 throws Throwable { 55 return channelRecordSortedMap.values(); 56 } 57 }) 58 .when(dataManager) 59 .getChannelRecords(); 60 Mockito.doAnswer( 61 new Answer<ChannelRecord>() { 62 @Override 63 public ChannelRecord answer(InvocationOnMock invocation) 64 throws Throwable { 65 long channelId = (long) invocation.getArguments()[0]; 66 return channelRecordSortedMap.get(channelId); 67 } 68 }) 69 .when(dataManager) 70 .getChannelRecord(Matchers.anyLong()); 71 return dataManager; 72 } 73 74 public static class ChannelRecordSortedMapHelper extends TreeMap<Long, ChannelRecord> { 75 private final Context mContext; 76 private Recommender mRecommender; 77 private Random mRandom = Utils.createTestRandom(); 78 ChannelRecordSortedMapHelper(Context context)79 public ChannelRecordSortedMapHelper(Context context) { 80 mContext = context; 81 } 82 setRecommender(Recommender recommender)83 public void setRecommender(Recommender recommender) { 84 mRecommender = recommender; 85 } 86 resetRandom(Random random)87 public void resetRandom(Random random) { 88 mRandom = random; 89 } 90 91 /** 92 * Add new {@code numberOfChannels} channels by adding channel record to {@code 93 * channelRecordMap} with no history. This action corresponds to loading channels in the 94 * RecommendationDataManger. 95 */ addChannels(int numberOfChannels)96 public void addChannels(int numberOfChannels) { 97 for (int i = 0; i < numberOfChannels; ++i) { 98 addChannel(); 99 } 100 } 101 102 /** 103 * Add new one channel by adding channel record to {@code channelRecordMap} with no history. 104 * This action corresponds to loading one channel in the RecommendationDataManger. 105 * 106 * @return The new channel was made by this method. 107 */ addChannel()108 public Channel addChannel() { 109 long channelId = size(); 110 ChannelImpl channel = new ChannelImpl.Builder().setId(channelId).build(); 111 ChannelRecord channelRecord = new ChannelRecord(mContext, channel, false); 112 put(channelId, channelRecord); 113 return channel; 114 } 115 116 /** 117 * Add the watch logs which its durationTime is under {@code maxWatchDurationMs}. Add until 118 * latest watch end time becomes bigger than {@code watchEndTimeMs}, starting from {@code 119 * watchStartTimeMs}. 120 * 121 * @return true if adding watch log success, otherwise false. 122 */ addRandomWatchLogs( long watchStartTimeMs, long watchEndTimeMs, long maxWatchDurationMs)123 public boolean addRandomWatchLogs( 124 long watchStartTimeMs, long watchEndTimeMs, long maxWatchDurationMs) { 125 long latestWatchEndTimeMs = watchStartTimeMs; 126 long previousChannelId = INVALID_CHANNEL_ID; 127 List<Long> channelIdList = new ArrayList<>(keySet()); 128 while (latestWatchEndTimeMs < watchEndTimeMs) { 129 long channelId = channelIdList.get(mRandom.nextInt(channelIdList.size())); 130 if (previousChannelId == channelId) { 131 // Time hopping with random minutes. 132 latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1); 133 } 134 long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1; 135 if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) { 136 return false; 137 } 138 latestWatchEndTimeMs += watchedDurationMs; 139 previousChannelId = channelId; 140 } 141 return true; 142 } 143 144 /** 145 * Add new watch log to channel that id is {@code ChannelId}. Add watch log starts from 146 * {@code watchStartTimeMs} with duration {@code durationTimeMs}. If adding is finished, 147 * notify the recommender that there's a new watch log. 148 * 149 * @return true if adding watch log success, otherwise false. 150 */ addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs)151 public boolean addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) { 152 ChannelRecord channelRecord = get(channelId); 153 if (channelRecord == null 154 || watchStartTimeMs + durationTimeMs > System.currentTimeMillis()) { 155 return false; 156 } 157 158 channelRecord.logWatchHistory( 159 new WatchedProgram(null, watchStartTimeMs, watchStartTimeMs + durationTimeMs)); 160 if (mRecommender != null) { 161 mRecommender.onNewWatchLog(channelRecord); 162 } 163 return true; 164 } 165 } 166 } 167