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 
21 import com.android.tv.data.Channel;
22 import com.android.tv.testing.Utils;
23 
24 import org.mockito.Matchers;
25 import org.mockito.Mockito;
26 import org.mockito.invocation.InvocationOnMock;
27 import org.mockito.stubbing.Answer;
28 
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.List;
32 import java.util.Random;
33 import java.util.TreeMap;
34 import java.util.concurrent.TimeUnit;
35 
36 public 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      */
createMockRecommendationDataManager( final ChannelRecordSortedMapHelper channelRecordSortedMap)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 
ChannelRecordSortedMapHelper(Context context)73         public ChannelRecordSortedMapHelper(Context context) {
74             mContext = context;
75         }
76 
setRecommender(Recommender recommender)77         public void setRecommender(Recommender recommender) {
78             mRecommender = recommender;
79         }
80 
resetRandom(Random random)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          */
addChannels(int numberOfChannels)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          */
addChannel()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          */
addRandomWatchLogs(long watchStartTimeMs, long watchEndTimeMs, long maxWatchDurationMs)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          */
addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs)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