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 
17 package com.android.tv.tuner.source;
18 
19 import android.content.Context;
20 import android.util.Log;
21 import com.android.tv.tuner.data.TunerChannel;
22 import java.io.File;
23 import java.io.FileNotFoundException;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.util.HashSet;
27 import java.util.Set;
28 
29 /** Stores TS files to the disk for debugging. */
30 public class TsStreamWriter {
31     private static final String TAG = "TsStreamWriter";
32     private static final boolean DEBUG = false;
33 
34     private static final long TIME_LIMIT_MS = 10000; // 10s
35     private static final int NO_INSTANCE_ID = 0;
36     private static final int MAX_GET_ID_RETRY_COUNT = 5;
37     private static final int MAX_INSTANCE_ID = 10000;
38     private static final String SEPARATOR = "_";
39 
40     private FileOutputStream mFileOutputStream;
41     private long mFileStartTimeMs;
42     private String mFileName = null;
43     private final String mDirectoryPath;
44     private final File mDirectory;
45     private final int mInstanceId;
46     private TunerChannel mChannel;
47 
TsStreamWriter(Context context)48     public TsStreamWriter(Context context) {
49         File externalFilesDir = context.getExternalFilesDir(null);
50         if (externalFilesDir == null || !externalFilesDir.isDirectory()) {
51             mDirectoryPath = null;
52             mDirectory = null;
53             mInstanceId = NO_INSTANCE_ID;
54             if (DEBUG) {
55                 Log.w(TAG, "Fail to get external files dir!");
56             }
57         } else {
58             mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream";
59             mDirectory = new File(mDirectoryPath);
60             if (!mDirectory.exists()) {
61                 boolean madeDir = mDirectory.mkdir();
62                 if (!madeDir) {
63                     Log.w(TAG, "Error. Fail to create folder!");
64                 }
65             }
66             mInstanceId = generateInstanceId();
67         }
68     }
69 
70     /**
71      * Sets the current channel.
72      *
73      * @param channel curren channel of the stream
74      */
setChannel(TunerChannel channel)75     public void setChannel(TunerChannel channel) {
76         mChannel = channel;
77     }
78 
79     /** Opens a file to store TS data. */
openFile()80     public void openFile() {
81         if (mChannel == null || mDirectoryPath == null) {
82             return;
83         }
84         mFileStartTimeMs = System.currentTimeMillis();
85         mFileName =
86                 mChannel.getDisplayNumber()
87                         + SEPARATOR
88                         + mFileStartTimeMs
89                         + SEPARATOR
90                         + mInstanceId
91                         + ".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      */
closeFile(boolean calledWhenStopStream)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      */
writeToFile(byte[] buffer, int bytesWritten)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 {@code
145      *     false} if the most recent file should not be deleted
146      */
deleteOutdatedFiles(boolean deleteAll)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()
161                     && getFileId(file) == mInstanceId
162                     && (deleteAll || !mFileName.equals(file.getName()))) {
163                 boolean deleted = file.delete();
164                 if (DEBUG && !deleted) {
165                     Log.w(TAG, "Failed to delete " + file.getName());
166                 }
167             }
168         }
169     }
170 
171     /**
172      * Generates a unique instance ID.
173      *
174      * @return a unique instance ID
175      */
generateInstanceId()176     private int generateInstanceId() {
177         if (mDirectory == null) {
178             return NO_INSTANCE_ID;
179         }
180         Set<Integer> idSet = getExistingIds();
181         if (idSet == null) {
182             return NO_INSTANCE_ID;
183         }
184         for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) {
185             // Range [1, MAX_INSTANCE_ID]
186             int id = (int) Math.floor(Math.random() * MAX_INSTANCE_ID) + 1;
187             if (!idSet.contains(id)) {
188                 return id;
189             }
190         }
191         return NO_INSTANCE_ID;
192     }
193 
194     /**
195      * Gets all existing instance IDs.
196      *
197      * @return a set of all existing instance IDs
198      */
getExistingIds()199     private Set<Integer> getExistingIds() {
200         if (mDirectory == null || !mDirectory.isDirectory()) {
201             return null;
202         }
203 
204         Set<Integer> idSet = new HashSet<>();
205         for (File file : mDirectory.listFiles()) {
206             int id = getFileId(file);
207             if (id != NO_INSTANCE_ID) {
208                 idSet.add(id);
209             }
210         }
211         return idSet;
212     }
213 
214     /**
215      * Gets the instance ID of a given file.
216      *
217      * @param file the file whose TsStreamWriter ID is returned
218      * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available
219      */
getFileId(File file)220     private static int getFileId(File file) {
221         if (file == null || !file.isFile()) {
222             return NO_INSTANCE_ID;
223         }
224         String fileName = file.getName();
225         int lastSeparator = fileName.lastIndexOf(SEPARATOR);
226         if (!fileName.endsWith(".ts") || lastSeparator == -1) {
227             return NO_INSTANCE_ID;
228         }
229         try {
230             return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3));
231         } catch (NumberFormatException e) {
232             if (DEBUG) {
233                 Log.e(TAG, fileName + " is not a valid file name.");
234             }
235         }
236         return NO_INSTANCE_ID;
237     }
238 }
239