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.os.ConditionVariable;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.Message;
23import android.util.Log;
24
25import com.google.android.exoplayer.SampleHolder;
26
27import java.io.File;
28import java.io.FileNotFoundException;
29import java.io.IOException;
30import java.io.RandomAccessFile;
31import java.nio.channels.FileChannel;
32import java.util.concurrent.ConcurrentLinkedQueue;
33
34/**
35 * {@link SampleCache} stores samples into file and make them available for read.
36 * This class is thread-safe.
37 */
38public class SampleCache {
39    private static final String TAG = "SampleCache";
40    private static final boolean DEBUG = false;
41
42    private final long mCreatedTimeMs;
43    private final long mStartPositionUs;
44    private long mEndPositionUs = 0;
45    private SampleCache mNextCache = null;
46    private final CacheState mCacheState = new CacheState();
47    private final Handler mIoHandler;
48
49    public static class SampleCacheFactory {
50        public SampleCache createSampleCache(SamplePool samplePool, File file,
51                long startPositionUs, CacheManager.CacheListener cacheListener,
52                Looper looper) throws IOException {
53            return new SampleCache(samplePool, file, startPositionUs, System.currentTimeMillis(),
54                    cacheListener, looper);
55        }
56
57        public SampleCache createSampleCacheFromFile(SamplePool samplePool, File cacheDir,
58                String filename, long startPositionUs, CacheManager.CacheListener cacheListener,
59                Looper looper, SampleCache prev) throws IOException {
60            File file = new File(cacheDir, filename);
61            SampleCache cache =
62                    new SampleCache(samplePool, file, startPositionUs, cacheListener, looper);
63            if (prev != null) {
64                prev.setNext(cache);
65            }
66            return cache;
67        }
68    }
69
70    private static class CacheState {
71        private static final int NUM_SAMPLES = 3;
72
73        private volatile boolean mCanReadMore = true;
74        private final ConcurrentLinkedQueue<SampleHolder> mSamples = new ConcurrentLinkedQueue<>();
75        private volatile long mSize = 0;
76
77        public SampleHolder pollSample() {
78            return mSamples.poll();
79        }
80
81        public void offerSample(SampleHolder sample) {
82            mSamples.offer(sample);
83        }
84
85        public void setCanReadMore(boolean canReadMore) {
86            mCanReadMore = canReadMore;
87        }
88
89        public boolean canReadMore() {
90            return mCanReadMore || !mSamples.isEmpty();
91        }
92
93        public boolean hasEnoughSamples() {
94            return mSamples.size() > NUM_SAMPLES;
95        }
96
97        public void setSize(long size) {
98            mSize = size;
99        }
100
101        public long getSize() {
102            return mSize;
103        }
104    }
105
106    private class IoHandlerCallback implements Handler.Callback {
107        public static final int MSG_WRITE = 1;
108        public static final int MSG_READ = 2;
109        public static final int MSG_CLOSE = 3;
110        public static final int MSG_DELETE = 4;
111        public static final int MSG_FINISH_WRITE = 5;
112        public static final int MSG_RESET_READ = 6;
113        public static final int MSG_CLEAR = 7;
114        public static final int MSG_OPEN = 8;
115
116        private static final int DELAY_MS = 10;
117        private static final int SAMPLE_HEADER_LENGTH = 16;
118
119        private final File mFile;
120        private final CacheManager.CacheListener mCacheListener;
121        private final SamplePool mSamplePool;
122        private final CacheState mCacheState;
123        private RandomAccessFile mRaf = null;
124        private long mWriteOffset = 0;
125        private long mReadOffset = 0;
126        private boolean mWriteFinished = false;
127        private boolean mDeleteAtEof = false;
128        private boolean mDeleted = false;
129
130        public IoHandlerCallback(File file, CacheManager.CacheListener cacheListener,
131                SamplePool samplePool, CacheState cacheState, boolean fromFile) throws IOException {
132            mFile = file;
133            mCacheListener = cacheListener;
134            mSamplePool = samplePool;
135            mCacheState = cacheState;
136            if (fromFile) {
137                loadFromFile();
138            }
139        }
140
141        private void loadFromFile() throws IOException {
142            // "r" is enough
143            try (RandomAccessFile raf = new RandomAccessFile(mFile, "r")) {
144                mWriteFinished = true;
145                mWriteOffset = raf.length();
146                mCacheState.setSize(mWriteOffset);
147                mCacheListener.onWrite(SampleCache.this);
148            }
149        }
150
151        @Override
152        public boolean handleMessage(Message msg) {
153            if (mDeleted) {
154                if (DEBUG) {
155                    Log.d(TAG, "Ignore access to a deleted cache.");
156                }
157                return true;
158            }
159            try {
160                switch (msg.what) {
161                    case MSG_WRITE:
162                        handleWrite(msg);
163                        return true;
164                    case MSG_READ:
165                        handleRead(msg);
166                        return true;
167                    case MSG_CLOSE:
168                        handleClose();
169                        return true;
170                    case MSG_DELETE:
171                        handleDelete();
172                        return true;
173                    case MSG_FINISH_WRITE:
174                        handleFinishWrite();
175                        return true;
176                    case MSG_RESET_READ:
177                        handleResetRead();
178                        return true;
179                    case MSG_CLEAR:
180                        handleClear(msg);
181                        return true;
182                    case MSG_OPEN:
183                        handleOpen();
184                        return true;
185                    default:
186                        return false;
187                }
188            } catch (IOException e) {
189                Log.e(TAG, "Error while handling file operation", e);
190                return true;
191            }
192        }
193
194        private void handleWrite(Message msg) throws IOException {
195            SampleHolder sample = (SampleHolder) ((Object[])msg.obj)[0];
196            ConditionVariable conditionVariable = (ConditionVariable) ((Object[])msg.obj)[1];
197            try {
198                mRaf.seek(mWriteOffset);
199                mRaf.writeInt(sample.size);
200                mRaf.writeInt(sample.flags);
201                mRaf.writeLong(sample.timeUs);
202                sample.data.position(0).limit(sample.size);
203                mRaf.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data);
204                mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH;
205                mCacheState.setSize(mWriteOffset);
206            } finally {
207                conditionVariable.open();
208            }
209        }
210
211        private void handleRead(Message msg) throws IOException {
212            msg.getTarget().removeMessages(MSG_READ);
213            if (mCacheState.hasEnoughSamples()) {
214                // If cache has enough samples, try again few moments later hoping that mCacheState
215                // needs a sample by then.
216                msg.getTarget().sendEmptyMessageDelayed(MSG_READ, DELAY_MS);
217            } else if (mReadOffset >= mWriteOffset) {
218                if (mWriteFinished) {
219                    if (mRaf != null) {
220                        mRaf.close();
221                        mRaf = null;
222                    }
223                    mCacheState.setCanReadMore(false);
224                    maybeDelete();
225                } else {
226                    // Read reached write but write is not finished yet --- wait a few moments to
227                    // see if another sample is written.
228                    msg.getTarget().sendEmptyMessageDelayed(MSG_READ, DELAY_MS);
229                }
230            } else {
231                if (mRaf == null) {
232                    try {
233                        mRaf = new RandomAccessFile(mFile, "r");
234                    } catch (FileNotFoundException e) {
235                        // Cache can be deleted by installd service.
236                        Log.e(TAG, "Failed opening a random access file.", e);
237                        mDeleted = true;
238                        mCacheListener.onDelete(SampleCache.this);
239                        return;
240                    }
241                }
242                mRaf.seek(mReadOffset);
243                int size = mRaf.readInt();
244                SampleHolder sample = mSamplePool.acquireSample(size);
245                sample.size = size;
246                sample.flags = mRaf.readInt();
247                sample.timeUs = mRaf.readLong();
248                sample.clearData();
249                sample.data.put(mRaf.getChannel().map(FileChannel.MapMode.READ_ONLY,
250                        mReadOffset + SAMPLE_HEADER_LENGTH, sample.size));
251                mReadOffset += sample.size + SAMPLE_HEADER_LENGTH;
252                mCacheState.offerSample(sample);
253                msg.getTarget().sendEmptyMessage(MSG_READ);
254            }
255        }
256
257        private void handleClose() throws IOException {
258            if (mWriteFinished) {
259                if (mRaf != null) {
260                    mRaf.close();
261                    mRaf = null;
262                }
263                mReadOffset = mWriteOffset;
264                mCacheState.setCanReadMore(false);
265                maybeDelete();
266            }
267        }
268
269        private void handleDelete() throws IOException {
270            mDeleteAtEof = true;
271            maybeDelete();
272        }
273
274        private void maybeDelete() throws IOException {
275            if (!mDeleteAtEof || mCacheState.canReadMore()) {
276                return;
277            }
278            if (mRaf != null) {
279                mRaf.close();
280                mRaf = null;
281            }
282            mFile.delete();
283            mDeleted = true;
284            mCacheListener.onDelete(SampleCache.this);
285        }
286
287        private void handleFinishWrite() throws IOException {
288            mCacheListener.onWrite(SampleCache.this);
289            mWriteFinished = true;
290            mRaf.close();
291            mRaf = null;
292        }
293
294        private void handleResetRead() {
295            mReadOffset = 0;
296        }
297
298        private void handleClear(Message msg) {
299            msg.getTarget().removeMessages(MSG_READ);
300            SampleHolder sample;
301            while ((sample = mCacheState.pollSample()) != null) {
302                mSamplePool.releaseSample(sample);
303            }
304        }
305
306        private void handleOpen() {
307            try {
308                mRaf = new RandomAccessFile(mFile, "rw");
309            } catch (FileNotFoundException e) {
310                Log.e(TAG, "Failed opening a random access file.", e);
311            }
312        }
313    }
314
315    protected SampleCache(SamplePool samplePool, File file, long startPositionUs,
316            long createdTimeMs, CacheManager.CacheListener cacheListener, Looper looper)
317            throws IOException {
318            mEndPositionUs = mStartPositionUs = startPositionUs;
319            mCreatedTimeMs = createdTimeMs;
320            mIoHandler = new Handler(looper,
321                    new IoHandlerCallback(file, cacheListener, samplePool, mCacheState, false));
322            mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_OPEN);
323    }
324
325    // Constructor of SampleCache which is backed by the given existing file.
326    protected SampleCache(SamplePool samplePool, File file, long startPositionUs,
327            CacheManager.CacheListener cacheListener, Looper looper) throws IOException {
328        mCreatedTimeMs = mEndPositionUs = mStartPositionUs = startPositionUs;
329        IoHandlerCallback handlerCallback =
330                new IoHandlerCallback(file, cacheListener, samplePool, mCacheState, true);
331        mIoHandler = new Handler(looper, handlerCallback);
332    }
333
334    public void resetRead() {
335        mCacheState.setCanReadMore(true);
336        mIoHandler.sendMessageAtFrontOfQueue(
337                mIoHandler.obtainMessage(IoHandlerCallback.MSG_RESET_READ));
338    }
339
340    private void setNext(SampleCache next) {
341        mNextCache = next;
342    }
343
344    public void finishWrite(SampleCache next) {
345        setNext(next);
346        mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_FINISH_WRITE);
347    }
348
349    public long getStartPositionUs() {
350        return mStartPositionUs;
351    }
352
353    public SampleCache getNext() {
354        return mNextCache;
355    }
356
357    public void writeSample(SampleHolder sample, ConditionVariable conditionVariable) {
358        if (mNextCache != null) {
359            throw new IllegalStateException(
360                    "Called writeSample() even though write is already finished");
361        }
362        mEndPositionUs = sample.timeUs;
363        conditionVariable.close();
364        mIoHandler.obtainMessage(IoHandlerCallback.MSG_WRITE,
365                new Object[] { sample, conditionVariable }).sendToTarget();
366    }
367
368    public long getEndPositionUs() {
369        return mEndPositionUs;
370    }
371
372    public long getCreatedTimeMs() {
373        return mCreatedTimeMs;
374    }
375
376    public boolean canReadMore() {
377        return mCacheState.canReadMore();
378    }
379
380    public SampleHolder maybeReadSample() {
381        SampleHolder sample = mCacheState.pollSample();
382        mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_READ);
383        return sample;
384    }
385
386    public void close() {
387        mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_CLOSE);
388    }
389
390    public void delete() {
391        mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_DELETE);
392    }
393
394    public void clear() {
395        mIoHandler.sendMessageAtFrontOfQueue(mIoHandler.obtainMessage(IoHandlerCallback.MSG_CLEAR));
396    }
397
398    public long getSize() {
399        return mCacheState.getSize();
400    }
401}
402