1 /*
2  * Copyright (C) 2019 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 package com.android.car.bugreport;
17 
18 import android.content.Context;
19 import android.os.UserManager;
20 import android.util.Log;
21 
22 import com.google.common.base.Preconditions;
23 
24 import java.io.File;
25 import java.io.IOException;
26 import java.nio.file.Files;
27 
28 /**
29  * File utilities.
30  *
31  * Thread safety and file operations: All file operations should happen on the same worker
32  * thread for thread safety. This is provided by running both bugreport service and file upload
33  * service on a asynctask. Asynctasks are by default executed on the same worker thread in serial.
34  *
35  * There is one exception to the rule above:
36  * Voice recorder works on main thread, however this is not a thread safety problem because:
37  * i. voice recorder always works before starting to collect rest of the bugreport
38  * ii. a bug report cannot be moved to upload (pending) directory before it is completely
39  * collected.
40  */
41 public class FileUtils {
42     private static final String TAG = FileUtils.class.getSimpleName();
43     private static final String PREFIX = "bugreport-";
44     /** A directory under the system user; contains bugreport zip files and audio files. */
45     private static final String PENDING_DIR = "bug_reports_pending";
46     // Temporary directory under the current user, used for zipping files.
47     private static final String TEMP_DIR = "bug_reports_temp";
48 
49     private static final String FS = "@";
50 
getPendingDir(Context context)51     static File getPendingDir(Context context) {
52         Preconditions.checkArgument(context.getSystemService(UserManager.class).isSystemUser(),
53                 "Must be called from the system user.");
54         File dir = new File(context.getDataDir(), PENDING_DIR);
55         dir.mkdirs();
56         return dir;
57     }
58 
59     /**
60      * Returns path to the directory for storing bug report files before they are zipped into a
61      * single file.
62      */
getTempDir(Context context, String timestamp)63     static File getTempDir(Context context, String timestamp) {
64         Preconditions.checkArgument(!context.getSystemService(UserManager.class).isSystemUser(),
65                 "Must be called from the current user.");
66         File dir = new File(context.getDataDir(), TEMP_DIR + "/" + timestamp);
67         dir.mkdirs();
68         return dir;
69     }
70 
71     /**
72      * Constructs a bugreport zip file name.
73      *
74      * <p>Add lookup code to the filename to allow matching audio file and bugreport file in USB.
75      */
getZipFileName(MetaBugReport bug)76     static String getZipFileName(MetaBugReport bug) {
77         String lookupCode = extractLookupCode(bug);
78         return PREFIX + bug.getUserName() + FS + bug.getTimestamp() + "-" + lookupCode + ".zip";
79     }
80 
81     /**
82      * Constructs a audio message file name.
83      *
84      * <p>Add lookup code to the filename to allow matching audio file and bugreport file in USB.
85      *
86      * @param timestamp - current timestamp, when audio was created.
87      * @param bug       - a bug report.
88      * @param extension - an extension of audio file.
89      */
getAudioFileName(String timestamp, MetaBugReport bug, String extension)90     static String getAudioFileName(String timestamp, MetaBugReport bug, String extension) {
91         String lookupCode = extractLookupCode(bug);
92         return (PREFIX + bug.getUserName() + FS + timestamp
93                 + "-" + lookupCode + "-message." + extension);
94     }
95 
extractLookupCode(MetaBugReport bug)96     public static String extractLookupCode(MetaBugReport bug) {
97         Preconditions.checkArgument(bug.getTitle().startsWith("["),
98                 "Invalid bugreport title, doesn't contain lookup code. ");
99         return bug.getTitle().substring(1, BugReportActivity.LOOKUP_STRING_LENGTH + 1);
100     }
101 
102     /**
103      * Returns a {@link File} object pointing to a path in a temp directory under current users
104      * {@link Context#getDataDir}.
105      *
106      * @param context   - an application context.
107      * @param timestamp - generates file for this timestamp
108      * @param suffix    - a filename suffix.
109      * @return A file.
110      */
getFileWithSuffix(Context context, String timestamp, String suffix)111     static File getFileWithSuffix(Context context, String timestamp, String suffix) {
112         return new File(getTempDir(context, timestamp), timestamp + suffix);
113     }
114 
115     /**
116      * Returns a {@link File} object pointing to a path in a temp directory under current users
117      * {@link Context#getDataDir}.
118      *
119      * @param context   - an application context.
120      * @param timestamp - generates file for this timestamp.
121      * @param name      - a filename
122      * @return A file.
123      */
getFile(Context context, String timestamp, String name)124     static File getFile(Context context, String timestamp, String name) {
125         return new File(getTempDir(context, timestamp), name);
126     }
127 
128     /**
129      * Deletes a directory and its contents recursively
130      *
131      * @param directory to delete
132      */
deleteDirectory(File directory)133     static void deleteDirectory(File directory) {
134         File[] files = directory.listFiles();
135         if (files != null) {
136             for (File file : files) {
137                 if (!file.isDirectory()) {
138                     file.delete();
139                 } else {
140                     deleteDirectory(file);
141                 }
142             }
143         }
144         directory.delete();
145     }
146 
147     /**
148      * Deletes a file quietly by ignoring exceptions.
149      *
150      * @param file The file to delete.
151      */
deleteFileQuietly(File file)152     public static void deleteFileQuietly(File file) {
153         try {
154             Files.delete(file.toPath());
155         } catch (IOException | SecurityException e) {
156             Log.w(TAG, "Failed to delete " + file + ". Ignoring the error.", e);
157         }
158     }
159 }
160