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