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.usbtuner.exoplayer.cache;
18
19import android.media.MediaFormat;
20import android.os.ConditionVariable;
21import android.os.HandlerThread;
22import android.support.annotation.VisibleForTesting;
23import android.support.annotation.NonNull;
24import android.support.annotation.Nullable;
25import android.util.ArrayMap;
26import android.util.ArraySet;
27import android.util.Log;
28import android.util.Pair;
29
30import com.google.android.exoplayer.SampleHolder;
31
32import java.io.File;
33import java.io.FileNotFoundException;
34import java.io.IOException;
35import java.text.SimpleDateFormat;
36import java.util.ArrayList;
37import java.util.Date;
38import java.util.List;
39import java.util.Locale;
40import java.util.Map;
41import java.util.Set;
42import java.util.SortedMap;
43import java.util.TreeMap;
44
45/**
46 * Manages {@link SampleCache} objects.
47 * <p>
48 * The cache manager can be disabled, while running, if the write throughput to the associated
49 * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}".
50 * This leads to restarting playback flow.
51 */
52public class CacheManager {
53    private static final String TAG = "CacheManager";
54    private static final boolean DEBUG = false;
55
56    // Constants for the disk write speed checking
57    private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK =
58            10L * 1024 * 1024;  // Checks for every 10M disk write
59    private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024;
60    private static final int MAXIMUM_SPEED_CHECK_COUNT = 5;  // Checks only 5 times
61    private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3;  // 3 Megabytes per second
62
63    private final SampleCache.SampleCacheFactory mSampleCacheFactory;
64    private final Map<String, SortedMap<Long, SampleCache>> mCacheMap = new ArrayMap<>();
65    private final Map<String, EvictListener> mEvictListeners = new ArrayMap<>();
66    private final StorageManager mStorageManager;
67    private final HandlerThread mIoHandlerThread = new HandlerThread(TAG);
68    private long mCacheSize = 0;
69    private final CacheSet mPendingDelete = new CacheSet();
70    private final CacheListener mCacheListener = new CacheListener() {
71        @Override
72        public void onWrite(SampleCache cache) {
73            mCacheSize += cache.getSize();
74        }
75
76        @Override
77        public void onDelete(SampleCache cache) {
78            mPendingDelete.remove(cache);
79            mCacheSize -= cache.getSize();
80        }
81    };
82
83    private volatile boolean mClosed = false;
84    private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
85    private long mTotalWriteSize;
86    private long mTotalWriteTimeNs;
87    private volatile int mSpeedCheckCount;
88    private boolean mDisabled = false;
89
90    public interface CacheListener {
91        void onWrite(SampleCache cache);
92        void onDelete(SampleCache cache);
93    }
94
95    public interface EvictListener {
96        void onCacheEvicted(String id, long createdTimeMs);
97    }
98
99    /**
100     * Handles I/O
101     * between CacheManager and {@link com.android.usbtuner.exoplayer.SampleExtractor}.
102     */
103    public interface SampleBuffer {
104
105        /**
106         * Initializes SampleBuffer.
107         * @param Ids track identifiers for storage read/write.
108         * @param mediaFormats meta-data for each track, this will be saved to storage in recording.
109         * @throws IOException
110         */
111        void init(@NonNull List<String> Ids, @Nullable List<MediaFormat> mediaFormats)
112                throws IOException;
113
114        /**
115         * Selects the track {@code index} for reading sample data.
116         */
117        void selectTrack(int index);
118
119        /**
120         * Deselects the track at {@code index},
121         * so that no more samples will be read from the track.
122         */
123        void deselectTrack(int index);
124
125        /**
126         * Writes sample to storage.
127         *
128         * @param index track index
129         * @param sample sample to write at storage
130         * @param conditionVariable notifies the completion of writing sample.
131         * @throws IOException
132         */
133        void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
134                throws IOException;
135
136        /**
137         * Checks whether storage write speed is slow.
138         */
139        boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs);
140
141        /**
142         * Handles when write speed is slow.
143         */
144        void handleWriteSpeedSlow();
145
146        /**
147         * Sets the flag when EoS was met.
148         */
149        void setEos();
150
151        /**
152         * Reads the next sample in the track at index {@code track} into {@code sampleHolder},
153         * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ}
154         * if it is available.
155         * If the next sample is not available,
156         * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}.
157         */
158        int readSample(int index, SampleHolder outSample);
159
160        /**
161         * Seeks to the specified time in microseconds.
162         */
163        void seekTo(long positionUs);
164
165        /**
166         * Returns an estimate of the position up to which data is buffered.
167         */
168        long getBufferedPositionUs();
169
170        /**
171         * Returns whether there is buffered data.
172         */
173        boolean continueBuffering(long positionUs);
174
175        /**
176         * Cleans up and releases everything.
177         */
178        void release();
179    }
180
181    /**
182     * Storage configuration and policy manager for {@link CacheManager}
183     */
184    public interface StorageManager {
185
186        /**
187         * Provides eligible storage directory for {@link CacheManager}.
188         *
189         * @return a directory to save cache chunks and meta files
190         */
191        File getCacheDir();
192
193        /**
194         * Cleans up storage.
195         */
196        void clearStorage();
197
198        /**
199         * Informs whether the storage is used for persistent use. (eg. dvr recording/play)
200         *
201         * @return {@code true} if stored files are persistent
202         */
203        boolean isPersistent();
204
205        /**
206         * Informs whether the storage usage exceeds pre-determined size.
207         *
208         * @param cacheSize the current total usage of Storage in bytes.
209         * @param pendingDelete the current storage usage which will be deleted in near future by
210         *                      bytes
211         * @return {@code true} if it reached pre-determined max size
212         */
213        boolean reachedStorageMax(long cacheSize, long pendingDelete);
214
215        /**
216         * Informs whether the storage has enough remained space.
217         *
218         * @param pendingDelete the current storage usage which will be deleted in near future by
219         *                      bytes
220         * @return {@code true} if it has enough space
221         */
222        boolean hasEnoughBuffer(long pendingDelete);
223
224        /**
225         * Reads track name & {@link MediaFormat} from storage.
226         *
227         * @param isAudio {@code true} if it is for audio track
228         * @return {@link Pair} of track name & {@link MediaFormat}
229         * @throws {@link java.io.IOException}
230         */
231        Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
232
233        /**
234         * Reads sample indexes for each written sample from storage.
235         *
236         * @param trackId track name
237         * @return
238         * @throws {@link java.io.IOException}
239         */
240        ArrayList<Long> readIndexFile(String trackId) throws IOException;
241
242        /**
243         * Writes track information to storage.
244         *
245         * @param trackId track name
246         * @param format {@link android.media.MediaFormat} of the track
247         * @param isAudio {@code true} if it is for audio track
248         * @throws {@link java.io.IOException}
249         */
250        void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
251                throws IOException;
252
253        /**
254         * Writes index file to storage.
255         *
256         * @param trackName track name
257         * @param index {@link SampleCache} container
258         * @throws {@link java.io.IOException}
259         */
260        void writeIndexFile(String trackName, SortedMap<Long, SampleCache> index)
261                throws IOException;
262    }
263
264    private static class CacheSet {
265        private final Set<SampleCache> mCaches = new ArraySet<>();
266
267        public synchronized void add(SampleCache cache) {
268            mCaches.add(cache);
269        }
270
271        public synchronized void remove(SampleCache cache) {
272            mCaches.remove(cache);
273        }
274
275        public synchronized long getSize() {
276            long size = 0;
277            for (SampleCache cache : mCaches) {
278                size += cache.getSize();
279            }
280            return size;
281        }
282    }
283
284    public CacheManager(StorageManager storageManager) {
285        this(storageManager, new SampleCache.SampleCacheFactory());
286    }
287
288    public CacheManager(StorageManager storageManager,
289            SampleCache.SampleCacheFactory sampleCacheFactory) {
290        mStorageManager = storageManager;
291        mSampleCacheFactory = sampleCacheFactory;
292        clearCache(true);
293        mIoHandlerThread.start();
294    }
295
296    public void registerEvictListener(String id, EvictListener evictListener) {
297        mEvictListeners.put(id, evictListener);
298    }
299
300    public void unregisterEvictListener(String id) {
301        mEvictListeners.remove(id);
302    }
303
304    private void clearCache(boolean deleteFiles) {
305        mCacheMap.clear();
306        if (deleteFiles) {
307            mStorageManager.clearStorage();
308        }
309        mCacheSize = 0;
310    }
311
312    private static String getFileName(String id, long positionUs) {
313        return String.format(Locale.ENGLISH, "%s_%016x.cache", id, positionUs);
314    }
315
316    /**
317     * Creates a new {@link SampleCache} for caching samples.
318     *
319     * @param id the name of the track
320     * @param positionUs starting position of the {@link SampleCache} in micro seconds.
321     * @param samplePool {@link SamplePool} for the fast creation of samples.
322     * @return returns the created {@link SampleCache}.
323     * @throws {@link java.io.IOException}
324     */
325    public SampleCache createNewWriteFile(String id, long positionUs, SamplePool samplePool)
326            throws IOException {
327        if (!maybeEvictCache()) {
328            throw new IOException("Not enough storage space");
329        }
330        SortedMap<Long, SampleCache> map = mCacheMap.get(id);
331        if (map == null) {
332            map = new TreeMap<>();
333            mCacheMap.put(id, map);
334        }
335        File file = new File(mStorageManager.getCacheDir(), getFileName(id, positionUs));
336        SampleCache sampleCache = mSampleCacheFactory.createSampleCache(samplePool, file,
337                positionUs, mCacheListener, mIoHandlerThread.getLooper());
338        map.put(positionUs, sampleCache);
339        return sampleCache;
340    }
341
342    /**
343     * Loads a track using {@link CacheManager.StorageManager}.
344     *
345     * @param trackId the name of the track.
346     * @param samplePool {@link SamplePool} for the fast creation of samples.
347     * @throws {@link java.io.IOException}
348     */
349    public void loadTrackFormStorage(String trackId, SamplePool samplePool) throws IOException {
350        ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
351
352        // TODO: notify the end position
353        SortedMap<Long, SampleCache> map = mCacheMap.get(trackId);
354        if (map == null) {
355            map = new TreeMap<>();
356            mCacheMap.put(trackId, map);
357        }
358        SampleCache cache = null;
359        for (long positionUs: keyPositions) {
360            cache = mSampleCacheFactory.createSampleCacheFromFile(samplePool,
361                    mStorageManager.getCacheDir(), getFileName(trackId, positionUs), positionUs,
362                    mCacheListener, mIoHandlerThread.getLooper(), cache);
363            map.put(positionUs, cache);
364        }
365    }
366
367    /**
368     * Finds a {@link SampleCache} for the specified track name and the position.
369     *
370     * @param id the name of the track.
371     * @param positionUs the position.
372     * @return returns the found {@link SampleCache}.
373     */
374    public SampleCache getReadFile(String id, long positionUs) {
375        SortedMap<Long, SampleCache> map = mCacheMap.get(id);
376        if (map == null) {
377            return null;
378        }
379        SampleCache sampleCache;
380        SortedMap<Long, SampleCache> headMap = map.headMap(positionUs + 1);
381        if (!headMap.isEmpty()) {
382            sampleCache = headMap.get(headMap.lastKey());
383        } else {
384            sampleCache = map.get(map.firstKey());
385        }
386        return sampleCache;
387    }
388
389    private boolean maybeEvictCache() {
390        long pendingDelete = mPendingDelete.getSize();
391        while (mStorageManager.reachedStorageMax(mCacheSize, pendingDelete)
392                || !mStorageManager.hasEnoughBuffer(pendingDelete)) {
393            if (mStorageManager.isPersistent()) {
394                // Since cache is persistent, we cannot evict caches.
395                return false;
396            }
397            SortedMap<Long, SampleCache> earliestCacheMap = null;
398            SampleCache earliestCache = null;
399            String earliestCacheId = null;
400            for (Map.Entry<String, SortedMap<Long, SampleCache>> entry : mCacheMap.entrySet()) {
401                SortedMap<Long, SampleCache> map = entry.getValue();
402                if (map.isEmpty()) {
403                    continue;
404                }
405                SampleCache cache = map.get(map.firstKey());
406                if (earliestCache == null
407                        || cache.getCreatedTimeMs() < earliestCache.getCreatedTimeMs()) {
408                    earliestCacheMap = map;
409                    earliestCache = cache;
410                    earliestCacheId = entry.getKey();
411                }
412            }
413            if (earliestCache == null) {
414                break;
415            }
416            mPendingDelete.add(earliestCache);
417            earliestCache.delete();
418            earliestCacheMap.remove(earliestCache.getStartPositionUs());
419            if (DEBUG) {
420                Log.d(TAG, String.format("cacheSize = %d; pendingDelete = %b; "
421                                + "earliestCache size = %d; %s@%d (%s)",
422                        mCacheSize, pendingDelete, earliestCache.getSize(), earliestCacheId,
423                        earliestCache.getStartPositionUs(),
424                        new SimpleDateFormat().format(new Date(earliestCache.getCreatedTimeMs()))));
425            }
426            EvictListener listener = mEvictListeners.get(earliestCacheId);
427            if (listener != null) {
428                listener.onCacheEvicted(earliestCacheId, earliestCache.getCreatedTimeMs());
429            }
430            pendingDelete = mPendingDelete.getSize();
431        }
432        return true;
433    }
434
435    /**
436     * Reads track information which includes {@link MediaFormat}.
437     *
438     * @return returns all track information which is found by {@link CacheManager.StorageManager}.
439     * @throws {@link java.io.IOException}
440     */
441    public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
442        ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
443        try {
444            trackInfos.add(mStorageManager.readTrackInfoFile(false));
445        } catch (FileNotFoundException e) {
446            // There can be a single track only recording. (eg. audio-only, video-only)
447            // So the exception should not stop the read.
448        }
449        try {
450            trackInfos.add(mStorageManager.readTrackInfoFile(true));
451        } catch (FileNotFoundException e) {
452            // See above catch block.
453        }
454        return trackInfos;
455    }
456
457    /**
458     * Writes track information and index information for all tracks.
459     *
460     * @param audio audio information.
461     * @param video video information.
462     */
463    public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video) {
464        try {
465            if (audio != null) {
466                mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
467                SortedMap<Long, SampleCache> map = mCacheMap.get(audio.first);
468                if (map == null) {
469                    throw new IOException("Audio track index missing");
470                }
471                mStorageManager.writeIndexFile(audio.first, map);
472            }
473            if (video != null) {
474                mStorageManager.writeTrackInfoFile(video.first, video.second, false);
475                SortedMap<Long, SampleCache> map = mCacheMap.get(video.first);
476                if (map == null) {
477                    throw new IOException("Video track index missing");
478                }
479                mStorageManager.writeIndexFile(video.first, map);
480            }
481        } catch (IOException e) {
482            // TODO: throw exception and notify this failure properly.
483        }
484    }
485
486    /**
487     * Marks it is closed and it is not used anymore.
488     */
489    public void close() {
490        // Clean-up may happen after this is called.
491        mClosed = true;
492    }
493
494    /**
495     * Cleans up the specified track.
496     *
497     * @param trackId the name of the track.
498     */
499    public void clearTrack(String trackId) {
500        SortedMap<Long, SampleCache> map = mCacheMap.get(trackId);
501        if (map == null) {
502            Log.w(TAG, "Cache with specified ID (" + trackId + ") not found");
503            return;
504        }
505        for (SampleCache cache : map.values()) {
506            cache.clear();
507            cache.close();
508            if (!mStorageManager.isPersistent()) {
509                cache.delete();
510            }
511        }
512        mCacheMap.remove(trackId);
513        if (mCacheMap.isEmpty() && mClosed) {
514            mIoHandlerThread.quitSafely();
515            clearCache(!mStorageManager.isPersistent());
516        }
517    }
518
519    private void resetWriteStat() {
520        mTotalWriteSize = 0;
521        mTotalWriteTimeNs = 0;
522    }
523
524    /**
525     * Adds a disk write sample size to calculate the average disk write bandwidth.
526     */
527    public void addWriteStat(long size, long timeNs) {
528        if (size >= mMinSampleSizeForSpeedCheck) {
529            mTotalWriteSize += size;
530            mTotalWriteTimeNs += timeNs;
531        }
532    }
533
534    /**
535     * Returns if the average disk write bandwidth is slower than
536     * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}.
537     */
538    public boolean isWriteSlow() {
539        if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) {
540            return false;
541        }
542
543        // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers
544        // by temporary system overloading during the playback.
545        if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) {
546            return false;
547        }
548        mSpeedCheckCount++;
549        float megabytePerSecond = getWriteBandwidth();
550        resetWriteStat();
551        if (DEBUG) {
552            Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
553        }
554        return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS;
555    }
556
557    /**
558     * Returns the disk write speed in megabytes per second.
559     */
560    private float getWriteBandwidth() {
561        if (mTotalWriteTimeNs == 0) {
562            return -1;
563        }
564        return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs);
565    }
566
567    /**
568     * Marks {@link CacheManger} object disabled to prevent it from the future use.
569     */
570    public void disable() {
571        mDisabled = true;
572    }
573
574    /**
575     * Returns if {@link CacheManger} object is disabled.
576     */
577    public boolean isDisabled() {
578        return mDisabled;
579    }
580
581    /**
582     * Returns if {@link CacheManager} has checked the write speed, which is suitable for Trickplay.
583     */
584    @VisibleForTesting
585    public boolean hasSpeedCheckDone() {
586        return mSpeedCheckCount > 0;
587    }
588
589    /**
590     * Sets minimum sample size for write speed check.
591     * @param sampleSize minimum sample size for write speed check.
592     */
593    @VisibleForTesting
594    public void setMinimumSampleSizeForSpeedCheck(int sampleSize) {
595        mMinSampleSizeForSpeedCheck = sampleSize;
596    }
597}
598