1 /*
<lambda>null2  * Copyright 2022 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.integration.extensions.utils
18 
19 import android.content.ContentResolver
20 import android.content.ContentValues
21 import android.graphics.ImageFormat
22 import android.media.Image
23 import android.net.Uri
24 import android.os.Build
25 import android.provider.MediaStore
26 import android.util.Log
27 import androidx.camera.core.impl.utils.Exif
28 import androidx.core.net.toUri
29 import java.io.File
30 import java.io.FileInputStream
31 import java.io.FileOutputStream
32 
33 private const val TAG = "FileUtil"
34 
35 /** File util functions */
36 object FileUtil {
37 
38     /**
39      * Saves an [Image] to the specified file path. The format of the input [Image] must be JPEG or
40      * YUV_420_888 format.
41      */
42     @JvmStatic
43     fun saveImage(
44         image: Image,
45         fileNamePrefix: String,
46         fileNameSuffix: String,
47         relativePath: String,
48         contentResolver: ContentResolver,
49         rotationDegrees: Int
50     ): Uri? {
51         require((image.format == ImageFormat.JPEG) or (image.format == ImageFormat.YUV_420_888)) {
52             "Incorrect image format of the input image proxy: ${image.format}"
53         }
54 
55         val fileName =
56             if (fileNameSuffix.isNotEmpty() && fileNameSuffix[0] == '.') {
57                 fileNamePrefix + fileNameSuffix
58             } else {
59                 "$fileNamePrefix.$fileNameSuffix"
60             }
61 
62         // Saves the image to the temp file
63         val tempFileUri =
64             saveImageToTempFile(image, fileNamePrefix, fileNameSuffix, null, rotationDegrees)
65                 ?: return null
66 
67         val contentValues =
68             ContentValues().apply {
69                 put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
70                 put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
71                 put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
72             }
73 
74         // Copies the temp file to the final output path
75         return copyTempFileToOutputLocation(
76             contentResolver,
77             tempFileUri,
78             MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
79             contentValues
80         )
81     }
82 
83     /** Saves an [Image] to a temp file. */
84     @JvmStatic
85     fun saveImageToTempFile(
86         image: Image,
87         prefix: String,
88         suffix: String,
89         cacheDir: File? = null,
90         rotationDegrees: Int = 0
91     ): Uri? {
92         val tempFile = File.createTempFile(prefix, suffix, cacheDir)
93 
94         val byteArray =
95             when (image.format) {
96                 ImageFormat.JPEG -> {
97                     ImageUtil.jpegImageToJpegByteArray(image)
98                 }
99                 ImageFormat.YUV_420_888 -> {
100                     ImageUtil.yuvImageToJpegByteArray(image, 100)
101                 }
102                 else -> {
103                     Log.e(TAG, "Incorrect image format of the input image proxy: ${image.format}")
104                     return null
105                 }
106             }
107 
108         val outputStream = FileOutputStream(tempFile)
109         outputStream.write(byteArray)
110         outputStream.close()
111 
112         // Updates Exif rotation tag info
113         val exif = Exif.createFromFile(tempFile)
114         exif.rotate(rotationDegrees)
115         exif.save()
116 
117         return tempFile.toUri()
118     }
119 
120     /**
121      * Copies temp file to the destination location.
122      *
123      * @return null if the copy process is failed.
124      */
125     @JvmStatic
126     fun copyTempFileToOutputLocation(
127         contentResolver: ContentResolver,
128         tempFileUri: Uri,
129         targetUrl: Uri,
130         contentValues: ContentValues,
131     ): Uri? {
132         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
133             Log.e(
134                 TAG,
135                 "The known devices which support Extensions should be at least" + " Android Q!"
136             )
137             return null
138         }
139 
140         contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
141 
142         val outputUri = contentResolver.insert(targetUrl, contentValues) ?: return null
143 
144         if (copyTempFileByteArrayToOutputLocation(contentResolver, tempFileUri, outputUri)) {
145             contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
146             contentResolver.update(outputUri, contentValues, null, null)
147             return outputUri
148         } else {
149             Log.e(TAG, "Failed to copy the temp file to the output path!")
150         }
151 
152         return null
153     }
154 
155     /**
156      * Copies temp file byte array to output [Uri].
157      *
158      * @return false if the [Uri] is not writable.
159      */
160     @JvmStatic
161     private fun copyTempFileByteArrayToOutputLocation(
162         contentResolver: ContentResolver,
163         tempFileUri: Uri,
164         uri: Uri
165     ): Boolean {
166         contentResolver.openOutputStream(uri).use { outputStream ->
167             if (tempFileUri.path == null || outputStream == null) {
168                 return false
169             }
170 
171             val tempFile = File(tempFileUri.path!!)
172 
173             FileInputStream(tempFile).use { inputStream ->
174                 val buf = ByteArray(1024)
175                 var len: Int
176                 while (inputStream.read(buf).also { len = it } > 0) {
177                     outputStream.write(buf, 0, len)
178                 }
179             }
180         }
181         return true
182     }
183 }
184