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.usbtuner.exoplayer.cache;
18 
19 import android.os.ConditionVariable;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.util.Log;
24 
25 import com.google.android.exoplayer.SampleHolder;
26 
27 import java.io.File;
28 import java.io.FileNotFoundException;
29 import java.io.IOException;
30 import java.io.RandomAccessFile;
31 import java.nio.channels.FileChannel;
32 import 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  */
38 public 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 {
createSampleCache(SamplePool samplePool, File file, long startPositionUs, CacheManager.CacheListener cacheListener, Looper looper)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 
createSampleCacheFromFile(SamplePool samplePool, File cacheDir, String filename, long startPositionUs, CacheManager.CacheListener cacheListener, Looper looper, SampleCache prev)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 
pollSample()77         public SampleHolder pollSample() {
78             return mSamples.poll();
79         }
80 
offerSample(SampleHolder sample)81         public void offerSample(SampleHolder sample) {
82             mSamples.offer(sample);
83         }
84 
setCanReadMore(boolean canReadMore)85         public void setCanReadMore(boolean canReadMore) {
86             mCanReadMore = canReadMore;
87         }
88 
canReadMore()89         public boolean canReadMore() {
90             return mCanReadMore || !mSamples.isEmpty();
91         }
92 
hasEnoughSamples()93         public boolean hasEnoughSamples() {
94             return mSamples.size() > NUM_SAMPLES;
95         }
96 
setSize(long size)97         public void setSize(long size) {
98             mSize = size;
99         }
100 
getSize()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 
IoHandlerCallback(File file, CacheManager.CacheListener cacheListener, SamplePool samplePool, CacheState cacheState, boolean fromFile)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 
loadFromFile()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
handleMessage(Message msg)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 
handleWrite(Message msg)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 
handleRead(Message msg)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 
handleClose()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 
handleDelete()269         private void handleDelete() throws IOException {
270             mDeleteAtEof = true;
271             maybeDelete();
272         }
273 
maybeDelete()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 
handleFinishWrite()287         private void handleFinishWrite() throws IOException {
288             mCacheListener.onWrite(SampleCache.this);
289             mWriteFinished = true;
290             mRaf.close();
291             mRaf = null;
292         }
293 
handleResetRead()294         private void handleResetRead() {
295             mReadOffset = 0;
296         }
297 
handleClear(Message msg)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 
handleOpen()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 
SampleCache(SamplePool samplePool, File file, long startPositionUs, long createdTimeMs, CacheManager.CacheListener cacheListener, Looper looper)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.
SampleCache(SamplePool samplePool, File file, long startPositionUs, CacheManager.CacheListener cacheListener, Looper looper)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 
resetRead()334     public void resetRead() {
335         mCacheState.setCanReadMore(true);
336         mIoHandler.sendMessageAtFrontOfQueue(
337                 mIoHandler.obtainMessage(IoHandlerCallback.MSG_RESET_READ));
338     }
339 
setNext(SampleCache next)340     private void setNext(SampleCache next) {
341         mNextCache = next;
342     }
343 
finishWrite(SampleCache next)344     public void finishWrite(SampleCache next) {
345         setNext(next);
346         mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_FINISH_WRITE);
347     }
348 
getStartPositionUs()349     public long getStartPositionUs() {
350         return mStartPositionUs;
351     }
352 
getNext()353     public SampleCache getNext() {
354         return mNextCache;
355     }
356 
writeSample(SampleHolder sample, ConditionVariable conditionVariable)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 
getEndPositionUs()368     public long getEndPositionUs() {
369         return mEndPositionUs;
370     }
371 
getCreatedTimeMs()372     public long getCreatedTimeMs() {
373         return mCreatedTimeMs;
374     }
375 
canReadMore()376     public boolean canReadMore() {
377         return mCacheState.canReadMore();
378     }
379 
maybeReadSample()380     public SampleHolder maybeReadSample() {
381         SampleHolder sample = mCacheState.pollSample();
382         mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_READ);
383         return sample;
384     }
385 
close()386     public void close() {
387         mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_CLOSE);
388     }
389 
delete()390     public void delete() {
391         mIoHandler.sendEmptyMessage(IoHandlerCallback.MSG_DELETE);
392     }
393 
clear()394     public void clear() {
395         mIoHandler.sendMessageAtFrontOfQueue(mIoHandler.obtainMessage(IoHandlerCallback.MSG_CLEAR));
396     }
397 
getSize()398     public long getSize() {
399         return mCacheState.getSize();
400     }
401 }
402