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