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