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.ArgumentMatchers; 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) throws Throwable { 64 long channelId = (long) invocation.getArguments()[0]; 65 return channelRecordSortedMap.get(channelId); 66 } 67 }) 68 .when(dataManager) 69 .getChannelRecord(ArgumentMatchers.anyLong()); 70 return dataManager; 71 } 72 73 public static class ChannelRecordSortedMapHelper extends TreeMap<Long, ChannelRecord> { 74 private final Context mContext; 75 private Recommender mRecommender; 76 private Random mRandom = Utils.createTestRandom(); 77 ChannelRecordSortedMapHelper(Context context)78 public ChannelRecordSortedMapHelper(Context context) { 79 mContext = context; 80 } 81 setRecommender(Recommender recommender)82 public void setRecommender(Recommender recommender) { 83 mRecommender = recommender; 84 } 85 resetRandom(Random random)86 public void resetRandom(Random random) { 87 mRandom = random; 88 } 89 90 /** 91 * Add new {@code numberOfChannels} channels by adding channel record to {@code 92 * channelRecordMap} with no history. This action corresponds to loading channels in the 93 * RecommendationDataManger. 94 */ addChannels(int numberOfChannels)95 public void addChannels(int numberOfChannels) { 96 for (int i = 0; i < numberOfChannels; ++i) { 97 addChannel(); 98 } 99 } 100 101 /** 102 * Add new one channel by adding channel record to {@code channelRecordMap} with no history. 103 * This action corresponds to loading one channel in the RecommendationDataManger. 104 * 105 * @return The new channel was made by this method. 106 */ addChannel()107 public Channel addChannel() { 108 long channelId = size(); 109 ChannelImpl channel = new ChannelImpl.Builder().setId(channelId).build(); 110 ChannelRecord channelRecord = new ChannelRecord(mContext, channel, false); 111 put(channelId, channelRecord); 112 return channel; 113 } 114 115 /** 116 * Add the watch logs which its durationTime is under {@code maxWatchDurationMs}. Add until 117 * latest watch end time becomes bigger than {@code watchEndTimeMs}, starting from {@code 118 * watchStartTimeMs}. 119 * 120 * @return true if adding watch log success, otherwise false. 121 */ addRandomWatchLogs( long watchStartTimeMs, long watchEndTimeMs, long maxWatchDurationMs)122 public boolean addRandomWatchLogs( 123 long watchStartTimeMs, long watchEndTimeMs, long maxWatchDurationMs) { 124 long latestWatchEndTimeMs = watchStartTimeMs; 125 long previousChannelId = INVALID_CHANNEL_ID; 126 List<Long> channelIdList = new ArrayList<>(keySet()); 127 while (latestWatchEndTimeMs < watchEndTimeMs) { 128 long channelId = channelIdList.get(mRandom.nextInt(channelIdList.size())); 129 if (previousChannelId == channelId) { 130 // Time hopping with random minutes. 131 latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1); 132 } 133 long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1L; 134 if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) { 135 return false; 136 } 137 latestWatchEndTimeMs += watchedDurationMs; 138 previousChannelId = channelId; 139 } 140 return true; 141 } 142 143 /** 144 * Add new watch log to channel that id is {@code ChannelId}. Add watch log starts from 145 * {@code watchStartTimeMs} with duration {@code durationTimeMs}. If adding is finished, 146 * notify the recommender that there's a new watch log. 147 * 148 * @return true if adding watch log success, otherwise false. 149 */ addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs)150 public boolean addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) { 151 ChannelRecord channelRecord = get(channelId); 152 if (channelRecord == null 153 || watchStartTimeMs + durationTimeMs > System.currentTimeMillis()) { 154 return false; 155 } 156 157 channelRecord.logWatchHistory( 158 new WatchedProgram(null, watchStartTimeMs, watchStartTimeMs + durationTimeMs)); 159 if (mRecommender != null) { 160 mRecommender.onNewWatchLog(channelRecord); 161 } 162 return true; 163 } 164 } 165 } 166