1 /*
<lambda>null2  * Copyright 2023 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 androidx.camera.testing.impl
18 
19 import android.content.ContentResolver
20 import android.content.ContentValues
21 import android.net.Uri
22 import android.os.Environment.DIRECTORY_DOCUMENTS
23 import android.os.Environment.DIRECTORY_MOVIES
24 import android.os.Environment.getExternalStoragePublicDirectory
25 import android.provider.MediaStore
26 import android.util.Log
27 import androidx.camera.core.Logger
28 import androidx.camera.video.FileOutputOptions
29 import androidx.camera.video.MediaStoreOutputOptions
30 import androidx.camera.video.internal.compat.quirk.DeviceQuirks
31 import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite
32 import java.io.File
33 import java.io.FileOutputStream
34 import java.io.OutputStreamWriter
35 
36 private const val TAG = "FileUtil"
37 private const val EXTENSION_MP4 = "mp4"
38 private const val EXTENSION_TEXT = "txt"
39 
40 public object FileUtil {
41 
42     /**
43      * Write the given text to the external storage.
44      *
45      * @param text the text to write to the external storage.
46      * @param fileName the file name to save the text.
47      * @param extension the file extension to save the text, [EXTENSION_TEXT] will be used by
48      *   default.
49      * @return the [FileOutputOptions] instance.
50      */
51     @JvmStatic
52     public fun writeTextToExternalFile(
53         text: String,
54         fileName: String,
55         extension: String = EXTENSION_TEXT
56     ) {
57         val fileNameWithExtension = "$fileName.$extension"
58         val folder = getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)
59         if (!folder.exists() && !folder.mkdirs()) {
60             Logger.e(TAG, "Failed to create directory: $folder")
61         }
62 
63         val file = File(folder, fileNameWithExtension)
64         FileOutputStream(file).use { fos ->
65             OutputStreamWriter(fos).use { writer ->
66                 writer.write(text)
67                 writer.flush()
68                 fos.fd.sync()
69                 writer.close()
70                 fos.close()
71             }
72         }
73         Logger.d(TAG, "Wrote [$text] to: ${file.path}")
74     }
75 
76     /**
77      * Check if the media store is available to save video recordings.
78      *
79      * @return true if the media store can be used, false otherwise.
80      * @see MediaStoreVideoCannotWrite
81      */
82     @JvmStatic
83     public fun canDeviceWriteToMediaStore(): Boolean {
84         return DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
85     }
86 
87     /**
88      * Create a [FileOutputOptions] for video recording with some default values.
89      *
90      * @param fileName the file name of the video recording.
91      * @param extension the file extension of the video recording, [EXTENSION_MP4] will be used by
92      *   default.
93      * @return the [FileOutputOptions] instance.
94      */
95     @JvmStatic
96     public fun generateVideoFileOutputOptions(
97         fileName: String,
98         extension: String = EXTENSION_MP4
99     ): FileOutputOptions {
100         val fileNameWithExtension = "$fileName.$extension"
101         val folder = getExternalStoragePublicDirectory(DIRECTORY_MOVIES)
102         if (!createFolder(folder)) {
103             Logger.e(TAG, "Failed to create directory: $folder")
104         }
105         return FileOutputOptions.Builder(File(folder, fileNameWithExtension)).build()
106     }
107 
108     /**
109      * Create a [MediaStoreOutputOptions] for video recording with some default values.
110      *
111      * @param contentResolver the [ContentResolver] instance.
112      * @param fileName the file name of the video recording.
113      * @return the [MediaStoreOutputOptions] instance.
114      */
115     @JvmStatic
116     public fun generateVideoMediaStoreOptions(
117         contentResolver: ContentResolver,
118         fileName: String
119     ): MediaStoreOutputOptions =
120         MediaStoreOutputOptions.Builder(
121                 contentResolver,
122                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI
123             )
124             .setContentValues(generateVideoContentValues(fileName))
125             .build()
126 
127     /**
128      * Check if the given file name string is valid.
129      *
130      * Currently a file name is invalid if:
131      * - it is `null`.
132      * - its length is zero.
133      * - it contains only whitespace character.
134      *
135      * @param fileName the file name string to be checked.
136      * @return `true` if the given file name is valid, otherwise `false`.
137      */
138     @JvmStatic
139     public fun isFileNameValid(fileName: String?): Boolean {
140         return !fileName.isNullOrBlank()
141     }
142 
143     /**
144      * Creates parent folder for the input file path.
145      *
146      * @param filePath the input file path to create its parent folder.
147      * @return `true` if the parent folder already exists or is created successfully. `false` if the
148      *   existing parent folder path is not a folder or failed to create.
149      */
150     @JvmStatic
151     public fun createParentFolder(filePath: String): Boolean {
152         return createParentFolder(File(filePath))
153     }
154 
155     /**
156      * Creates parent folder for the input file.
157      *
158      * @param file the input file to create its parent folder
159      * @return `true` if the parent folder already exists or is created successfully. `false` if the
160      *   existing parent folder path is not a folder or failed to create.
161      */
162     @JvmStatic
163     public fun createParentFolder(file: File): Boolean =
164         file.parentFile?.let { createFolder(it) } ?: false
165 
166     /**
167      * Creates folder for the input file.
168      *
169      * @param file the input file to create folder
170      * @return `true` if the folder already exists or is created successfully. `false` if the
171      *   existing folder path is not a folder or failed to create.
172      */
173     @JvmStatic
174     public fun createFolder(file: File): Boolean =
175         if (file.exists()) {
176             file.isDirectory
177         } else {
178             file.mkdirs()
179         }
180 
181     /**
182      * Gets the absolute path from a Uri.
183      *
184      * @param resolver the content resolver.
185      * @param contentUri the content uri.
186      * @return the file path of the content uri or null if not found.
187      */
188     @JvmStatic
189     public fun getAbsolutePathFromUri(resolver: ContentResolver, contentUri: Uri): String? {
190         // MediaStore.Video.Media.DATA was deprecated in API level 29.
191         val column = MediaStore.Video.Media.DATA
192         try {
193             resolver.query(contentUri, arrayOf(column), null, null, null)!!.use { cursor ->
194                 val columnIndex = cursor.getColumnIndexOrThrow(column)
195                 cursor.moveToFirst()
196                 return cursor.getString(columnIndex)
197             }
198         } catch (e: RuntimeException) {
199             Log.e(
200                 TAG,
201                 String.format(
202                     "Failed in getting absolute path for Uri %s with Exception %s",
203                     contentUri,
204                     e
205                 ),
206                 e
207             )
208             return null
209         }
210     }
211 
212     private fun generateVideoContentValues(fileName: String) =
213         ContentValues().apply {
214             put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
215             put(MediaStore.Video.Media.TITLE, fileName)
216             put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
217             val currentTimeMs = System.currentTimeMillis()
218             put(MediaStore.Video.Media.DATE_ADDED, currentTimeMs / 1000)
219             put(MediaStore.Video.Media.DATE_TAKEN, currentTimeMs)
220         }
221 }
222