1/*
2 * Copyright (C) 2016 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.tuner.source;
18
19import android.content.Context;
20import android.util.Log;
21import com.android.tv.tuner.data.TunerChannel;
22
23import java.io.File;
24import java.io.FileNotFoundException;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.util.HashSet;
28import java.util.Set;
29
30/**
31 * Stores TS files to the disk for debugging.
32 */
33public class TsStreamWriter {
34    private static final String TAG = "TsStreamWriter";
35    private static final boolean DEBUG = false;
36
37    private static final long TIME_LIMIT_MS = 10000; // 10s
38    private static final int NO_INSTANCE_ID = 0;
39    private static final int MAX_GET_ID_RETRY_COUNT = 5;
40    private static final int MAX_INSTANCE_ID = 10000;
41    private static final String SEPARATOR = "_";
42
43    private FileOutputStream mFileOutputStream;
44    private long mFileStartTimeMs;
45    private String mFileName = null;
46    private final String mDirectoryPath;
47    private final File mDirectory;
48    private final int mInstanceId;
49    private TunerChannel mChannel;
50
51    public TsStreamWriter(Context context) {
52        File externalFilesDir = context.getExternalFilesDir(null);
53        if (externalFilesDir == null || !externalFilesDir.isDirectory()) {
54            mDirectoryPath = null;
55            mDirectory = null;
56            mInstanceId = NO_INSTANCE_ID;
57            if (DEBUG) {
58                Log.w(TAG, "Fail to get external files dir!");
59            }
60        } else {
61            mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream";
62            mDirectory = new File(mDirectoryPath);
63            if (!mDirectory.exists()) {
64                boolean madeDir = mDirectory.mkdir();
65                if (!madeDir) {
66                    Log.w(TAG, "Error. Fail to create folder!");
67                }
68            }
69            mInstanceId = generateInstanceId();
70        }
71    }
72
73    /**
74     * Sets the current channel.
75     *
76     * @param channel curren channel of the stream
77     */
78    public void setChannel(TunerChannel channel) {
79        mChannel = channel;
80    }
81
82    /**
83     * Opens a file to store TS data.
84     */
85    public void openFile() {
86        if (mChannel == null || mDirectoryPath == null) {
87            return;
88        }
89        mFileStartTimeMs = System.currentTimeMillis();
90        mFileName = mChannel.getDisplayNumber() + SEPARATOR + mFileStartTimeMs + SEPARATOR
91                + mInstanceId + ".ts";
92        String filePath = mDirectoryPath + "/" + mFileName;
93        try {
94            mFileOutputStream = new FileOutputStream(filePath, false);
95        } catch (FileNotFoundException e) {
96            Log.w(TAG, "Cannot open file: " + filePath, e);
97        }
98    }
99
100    /**
101     * Closes the file and stops storing TS data.
102     *
103     * @param calledWhenStopStream {@code true} if this method is called when the stream is stopped
104     *                             {@code false} otherwise
105     */
106    public void closeFile(boolean calledWhenStopStream) {
107        if (mFileOutputStream == null) {
108            return;
109        }
110        try {
111            mFileOutputStream.close();
112            deleteOutdatedFiles(calledWhenStopStream);
113            mFileName = null;
114            mFileOutputStream = null;
115        } catch (IOException e) {
116            Log.w(TAG, "Error on closing file.", e);
117        }
118    }
119
120    /**
121     * Writes the data to the file.
122     *
123     * @param buffer the data to be written
124     * @param bytesWritten number of bytes written
125     */
126    public void writeToFile(byte[] buffer, int bytesWritten) {
127        if (mFileOutputStream == null) {
128            return;
129        }
130        if (System.currentTimeMillis() - mFileStartTimeMs > TIME_LIMIT_MS) {
131            closeFile(false);
132            openFile();
133        }
134        try {
135            mFileOutputStream.write(buffer, 0, bytesWritten);
136        } catch (IOException e) {
137            Log.w(TAG, "Error on writing TS stream.", e);
138        }
139    }
140
141    /**
142     * Deletes outdated files to save storage.
143     *
144     * @param deleteAll {@code true} if all the files with the relative ID should be deleted
145     *                  {@code false} if the most recent file should not be deleted
146     */
147    private void deleteOutdatedFiles(boolean deleteAll) {
148        if (mFileName == null) {
149            return;
150        }
151        if (mDirectory == null || !mDirectory.isDirectory()) {
152            Log.e(TAG, "Error. The folder doesn't exist!");
153            return;
154        }
155        if (mFileName == null) {
156            Log.e(TAG, "Error. The current file name is null!");
157            return;
158        }
159        for (File file : mDirectory.listFiles()) {
160            if (file.isFile() && getFileId(file) == mInstanceId
161                    && (deleteAll || !mFileName.equals(file.getName()))) {
162                boolean deleted = file.delete();
163                if (DEBUG && !deleted) {
164                    Log.w(TAG, "Failed to delete " + file.getName());
165                }
166            }
167        }
168    }
169
170    /**
171     * Generates a unique instance ID.
172     *
173     * @return a unique instance ID
174     */
175    private int generateInstanceId() {
176        if (mDirectory == null) {
177            return NO_INSTANCE_ID;
178        }
179        Set<Integer> idSet = getExistingIds();
180        if (idSet == null) {
181            return  NO_INSTANCE_ID;
182        }
183        for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) {
184            // Range [1, MAX_INSTANCE_ID]
185            int id = (int)Math.floor(Math.random() * MAX_INSTANCE_ID) + 1;
186            if (!idSet.contains(id)) {
187                return id;
188            }
189        }
190        return NO_INSTANCE_ID;
191    }
192
193    /**
194     * Gets all existing instance IDs.
195     *
196     * @return a set of all existing instance IDs
197     */
198    private Set<Integer> getExistingIds() {
199        if (mDirectory == null || !mDirectory.isDirectory()) {
200            return null;
201        }
202
203        Set<Integer> idSet = new HashSet<>();
204        for (File file : mDirectory.listFiles()) {
205            int id = getFileId(file);
206            if(id != NO_INSTANCE_ID) {
207                idSet.add(id);
208            }
209        }
210        return idSet;
211    }
212
213    /**
214     * Gets the instance ID of a given file.
215     *
216     * @param file the file whose TsStreamWriter ID is returned
217     * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available
218     */
219    private static int getFileId(File file) {
220        if (file == null || !file.isFile()) {
221            return NO_INSTANCE_ID;
222        }
223        String fileName = file.getName();
224        int lastSeparator = fileName.lastIndexOf(SEPARATOR);
225        if (!fileName.endsWith(".ts") || lastSeparator == -1) {
226            return NO_INSTANCE_ID;
227        }
228        try {
229            return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3));
230        } catch (NumberFormatException e) {
231            if (DEBUG) {
232                Log.e(TAG, fileName + " is not a valid file name.");
233            }
234        }
235        return NO_INSTANCE_ID;
236    }
237}
238