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.util.Pair;
21
22import java.io.DataInputStream;
23import java.io.DataOutputStream;
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileOutputStream;
27import java.io.IOException;
28import java.nio.ByteBuffer;
29import java.nio.charset.StandardCharsets;
30import java.util.ArrayList;
31import java.util.SortedMap;
32
33/**
34 * Manages DVR storage.
35 */
36public class DvrStorageManager implements CacheManager.StorageManager {
37
38    // TODO: make serializable classes and use protobuf after internal data structure is finalized.
39    private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
40            "com.google.android.videos.pixelWidthHeightRatio";
41    private static final String META_FILE_SUFFIX = ".meta";
42    private static final String IDX_FILE_SUFFIX = ".idx";
43
44    // Size of minimum reserved storage buffer which will be used to save meta files
45    // and index files after actual recording finished.
46    private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024;
47    private static final int NO_VALUE = -1;
48    private static final long NO_VALUE_LONG = -1L;
49
50    private final File mCacheDir;
51
52    // {@code true} when this is for recording, {@code false} when this is for replaying.
53    private final boolean mIsRecording;
54
55    public DvrStorageManager(File file, boolean isRecording) {
56        mCacheDir = file;
57        mCacheDir.mkdirs();
58        mIsRecording = isRecording;
59    }
60
61    @Override
62    public void clearStorage() {
63        if (mIsRecording) {
64            for (File file : mCacheDir.listFiles()) {
65                file.delete();
66            }
67        }
68    }
69
70    @Override
71    public File getCacheDir() {
72        return mCacheDir;
73    }
74
75    @Override
76    public boolean isPersistent() {
77        return true;
78    }
79
80    @Override
81    public boolean reachedStorageMax(long cacheSize, long pendingDelete) {
82        return false;
83    }
84
85    @Override
86    public boolean hasEnoughBuffer(long pendingDelete) {
87        return !mIsRecording || mCacheDir.getUsableSpace() >= MIN_BUFFER_BYTES;
88    }
89
90    private void readFormatInt(DataInputStream in, MediaFormat format, String key)
91            throws IOException {
92        int val = in.readInt();
93        if (val != NO_VALUE) {
94            format.setInteger(key, val);
95        }
96    }
97
98    private void readFormatLong(DataInputStream in, MediaFormat format, String key)
99            throws IOException {
100        long val = in.readLong();
101        if (val != NO_VALUE_LONG) {
102            format.setLong(key, val);
103        }
104    }
105
106    private void readFormatFloat(DataInputStream in, MediaFormat format, String key)
107            throws IOException {
108        float val = in.readFloat();
109        if (val != NO_VALUE) {
110            format.setFloat(key, val);
111        }
112    }
113
114    private String readString(DataInputStream in) throws IOException {
115        int len = in.readInt();
116        if (len <= 0) {
117            return null;
118        }
119        byte [] strBytes = new byte[len];
120        in.readFully(strBytes);
121        return new String(strBytes, StandardCharsets.UTF_8);
122    }
123
124    private void readFormatString(DataInputStream in, MediaFormat format, String key)
125            throws IOException {
126        String str = readString(in);
127        if (str != null) {
128            format.setString(key, str);
129        }
130    }
131
132    private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
133        int len = in.readInt();
134        if (len <= 0) {
135            return null;
136        }
137        byte [] bytes = new byte[len];
138        in.readFully(bytes);
139        ByteBuffer buffer = ByteBuffer.allocate(len);
140        buffer.put(bytes);
141        buffer.flip();
142
143        return buffer;
144    }
145
146    private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key)
147            throws IOException {
148        ByteBuffer buffer = readByteBuffer(in);
149        if (buffer != null) {
150            format.setByteBuffer(key, buffer);
151        }
152    }
153
154    @Override
155    public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException {
156        File file = new File(getCacheDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
157        try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
158            String name = readString(in);
159            MediaFormat format = new MediaFormat();
160            readFormatString(in, format, MediaFormat.KEY_MIME);
161            readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
162            readFormatInt(in, format, MediaFormat.KEY_WIDTH);
163            readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
164            readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
165            readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
166            readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
167            for (int i = 0; i < 3; ++i) {
168                readFormatByteBuffer(in, format, "csd-" + i);
169            }
170            readFormatLong(in, format, MediaFormat.KEY_DURATION);
171            return new Pair<>(name, format);
172        }
173    }
174
175    @Override
176    public ArrayList<Long> readIndexFile(String trackId) throws IOException {
177        ArrayList<Long> indices = new ArrayList<>();
178        File file = new File(getCacheDir(), trackId + IDX_FILE_SUFFIX);
179        try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
180            long count = in.readLong();
181            for (long i = 0; i < count; ++i) {
182                indices.add(in.readLong());
183            }
184            return indices;
185        }
186    }
187
188    private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
189            throws IOException {
190        if (format.containsKey(key)) {
191            out.writeInt(format.getInteger(key));
192        } else {
193            out.writeInt(NO_VALUE);
194        }
195    }
196
197    private void writeFormatLong(DataOutputStream out, MediaFormat format, String key)
198            throws IOException {
199        if (format.containsKey(key)) {
200            out.writeLong(format.getLong(key));
201        } else {
202            out.writeLong(NO_VALUE_LONG);
203        }
204    }
205
206    private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key)
207            throws IOException {
208        if (format.containsKey(key)) {
209            out.writeFloat(format.getFloat(key));
210        } else {
211            out.writeFloat(NO_VALUE);
212        }
213    }
214
215    private void writeString(DataOutputStream out, String str) throws IOException {
216        byte [] data = str.getBytes(StandardCharsets.UTF_8);
217        out.writeInt(data.length);
218        if (data.length > 0) {
219            out.write(data);
220        }
221    }
222
223    private void writeFormatString(DataOutputStream out, MediaFormat format, String key)
224            throws IOException {
225        if (format.containsKey(key)) {
226            writeString(out, format.getString(key));
227        } else {
228            out.writeInt(0);
229        }
230    }
231
232    private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException {
233        byte [] data = new byte[buffer.limit()];
234        buffer.get(data);
235        buffer.flip();
236        out.writeInt(data.length);
237        if (data.length > 0) {
238            out.write(data);
239        } else {
240            out.writeInt(0);
241        }
242    }
243
244    private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key)
245            throws IOException {
246        if (format.containsKey(key)) {
247            writeByteBuffer(out, format.getByteBuffer(key));
248        } else {
249            out.writeInt(0);
250        }
251    }
252
253    @Override
254    public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
255            throws IOException {
256        File file = new File(getCacheDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
257        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
258            writeString(out, trackId);
259            writeFormatString(out, format, MediaFormat.KEY_MIME);
260            writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE);
261            writeFormatInt(out, format, MediaFormat.KEY_WIDTH);
262            writeFormatInt(out, format, MediaFormat.KEY_HEIGHT);
263            writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT);
264            writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE);
265            writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
266            for (int i = 0; i < 3; ++i) {
267                writeFormatByteBuffer(out, format, "csd-" + i);
268            }
269            writeFormatLong(out, format, MediaFormat.KEY_DURATION);
270        }
271    }
272
273    @Override
274    public void writeIndexFile(String trackName, SortedMap<Long, SampleCache> index)
275            throws IOException {
276        File indexFile  = new File(getCacheDir(), trackName + IDX_FILE_SUFFIX);
277        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
278            out.writeLong(index.size());
279            for (Long key : index.keySet()) {
280                out.writeLong(key);
281            }
282        }
283    }
284}
285