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 
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.HashSet;
28 import java.util.Set;
29 
30 /**
31  * Stores TS files to the disk for debugging.
32  */
33 public 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 
TsStreamWriter(Context context)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      */
setChannel(TunerChannel channel)78     public void setChannel(TunerChannel channel) {
79         mChannel = channel;
80     }
81 
82     /**
83      * Opens a file to store TS data.
84      */
openFile()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      */
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
145      *                  {@code 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() && 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      */
generateInstanceId()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      */
getExistingIds()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      */
getFileId(File file)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