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