OSDN Git Service

プレビュー画像(ライブビュー画像)保存時にXMPを設定して、Googleフォトで360度画像と認識してもらうように修正。
[gokigen/ThetaView.git] / app / src / main / java / jp / osdn / gokigen / thetaview / liveview / storeimage / StoreImage.kt
1 package jp.osdn.gokigen.thetaview.liveview.storeimage
2
3 import android.content.ContentValues
4 import android.database.DatabaseUtils
5 import android.graphics.Bitmap
6 import android.os.Build
7 import android.os.Environment
8 import android.provider.MediaStore
9 import android.util.Log
10 import androidx.fragment.app.FragmentActivity
11 import jp.osdn.gokigen.thetaview.R
12 import jp.osdn.gokigen.thetaview.liveview.image.IImageProvider
13 import jp.osdn.gokigen.thetaview.preference.IPreferencePropertyAccessor
14 import jp.osdn.gokigen.thetaview.preference.PreferenceAccessWrapper
15 import java.io.ByteArrayOutputStream
16
17 import java.io.File
18 import java.io.FileOutputStream
19 import java.text.SimpleDateFormat
20 import java.util.*
21
22 class StoreImage(private val context: FragmentActivity, private val imageProvider: IImageProvider, private val dumpLog : Boolean = false) : IStoreImage
23 {
24
25     override fun doStore(target: Bitmap?)
26     {
27         try
28         {
29             // 保存処理(プログレスダイアログ(「保存中...」)を表示
30
31             val bitmapToStore = target ?: imageProvider.getImage()
32             val isLocalLocation  = PreferenceAccessWrapper(context).getBoolean(IPreferencePropertyAccessor.PREFERENCE_SAVE_LOCAL_LOCATION, IPreferencePropertyAccessor.PREFERENCE_SAVE_LOCAL_LOCATION_DEFAULT_VALUE)
33             if (isLocalLocation)
34             {
35                 saveImageLocal(bitmapToStore)
36             }
37             else
38             {
39                 saveImageExternal(bitmapToStore)
40             }
41
42             // 保存処理(プログレスダイアログ(「保存中...」)を削除
43         }
44         catch (t: Throwable)
45         {
46             t.printStackTrace()
47         }
48     }
49
50 /*
51     private fun storeImageImpl(target: Bitmap)
52     {
53 /*
54         // 保存処理(プログレスダイアログ(「保存中...」)を表示して処理する)
55         val saveDialog = ProgressDialog(context)
56         saveDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER)
57         saveDialog.setMessage(context!!.getString(R.string.data_saving))
58         saveDialog.isIndeterminate = true
59         saveDialog.setCancelable(false)
60         saveDialog.show()
61         val thread = Thread {
62             System.gc()
63             saveImageImpl(target)
64             System.gc()
65             saveDialog.dismiss()
66         }
67         try
68         {
69             thread.start()
70         }
71         catch (t: Throwable)
72         {
73             t.printStackTrace()
74             System.gc()
75         }
76 */
77         try
78         {
79             val isLocalLocation  = PreferenceAccessWrapper(context).getBoolean(IPreferencePropertyAccessor.PREFERENCE_SAVE_LOCAL_LOCATION, IPreferencePropertyAccessor.PREFERENCE_SAVE_LOCAL_LOCATION_DEFAULT_VALUE)
80             if (isLocalLocation)
81             {
82                 saveImageLocal(target)
83             }
84             else
85             {
86                 saveImageExternal(target)
87             }
88         }
89         catch (t: Throwable)
90         {
91             t.printStackTrace()
92         }
93     }
94 */
95     private fun prepareLocalOutputDirectory(): File
96     {
97         val mediaDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
98         mediaDir?.mkdirs()
99         return (if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir)
100     }
101
102     /**
103      * ビットマップイメージをファイルに出力する
104      *
105      * @param targetImage  出力するビットマップイメージ
106      */
107     private fun saveImageLocal(targetImage: Bitmap)
108     {
109         try
110         {
111             val fileName = "L" + SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".jpg"
112             val photoFile = File(prepareLocalOutputDirectory(), fileName)
113             val outputStream = FileOutputStream(photoFile)
114             targetImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
115             outputStream.flush()
116             outputStream.close()
117         }
118         catch (t: Throwable)
119         {
120             t.printStackTrace()
121         }
122     }
123
124     private fun getExternalOutputDirectory(): File
125     {
126         @Suppress("DEPRECATION") val directoryPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).path + "/" + context.getString(R.string.app_location) + "/"
127         val target = File(directoryPath)
128         try
129         {
130             target.mkdirs()
131         }
132         catch (e: Exception)
133         {
134             e.printStackTrace()
135         }
136         if (dumpLog)
137         {
138             Log.v(TAG, "  ----- RECORD Directory PATH : $directoryPath -----")
139         }
140         return (target)
141     }
142
143     /**
144      * ビットマップイメージを外部ストレージのファイルに出力する
145      *
146      * @param targetImage  出力するビットマップイメージ
147      */
148     private fun saveImageExternal(targetImage: Bitmap)
149     {
150         try
151         {
152             if (!isExternalStorageWritable())
153             {
154
155                 saveImageLocal(targetImage)
156                 return
157             }
158
159             val outputDir = getExternalOutputDirectory()
160             val resolver = context.contentResolver
161             val mimeType = "image/jpeg"
162             val now = System.currentTimeMillis()
163             val path = Environment.DIRECTORY_DCIM + File.separator + context.getString(R.string.app_location) // Environment.DIRECTORY_PICTURES  + File.separator + "gokigen" //"DCIM/aira01a/"
164             val fileName = "L" + SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(now) + ".jpg"
165
166             val values = ContentValues()
167             values.put(MediaStore.Images.Media.TITLE, fileName)
168             values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
169             values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
170             val extStorageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
171                 values.put(MediaStore.Images.Media.RELATIVE_PATH, path)
172 /*
173                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
174                     values.put(MediaStore.Images.Media.XMP, xmpValue)
175                 }
176 */
177                 values.put(MediaStore.Images.Media.IS_PENDING, true)
178                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
179             } else {
180                 @Suppress("DEPRECATION")
181                 values.put(MediaStore.Images.Media.DATA, outputDir.absolutePath + File.separator + fileName)
182                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI
183             }
184             val imageUri = resolver.insert(extStorageUri, values)
185             if (imageUri != null)
186             {
187                 //resolver.update(imageUri, values, null, null)
188                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
189                 {
190                     Log.v(TAG, "  ===== StorageOperationWithPermission() : $imageUri =====")
191                     //StorageOperationWithPermission(context).requestAndroidRMediaPermission(imageUri)
192                 }
193
194                 ////////////////////////////////////////////////////////////////
195                 if (dumpLog)
196                 {
197                     val cursor = resolver.query(imageUri, null, null, null, null)
198                     DatabaseUtils.dumpCursor(cursor)
199                     cursor!!.close()
200                 }
201                 ////////////////////////////////////////////////////////////////
202
203                 val outputStream = resolver.openOutputStream(imageUri)
204                 if (outputStream != null)
205                 {
206                     val xmpValue = "http://ns.adobe.com/xap/1.0/"
207                     val xmpValue1 = "<?xpacket begin=\"" + "\" ?> <x:xmpmeta xmlns:x=\"adobe:ns:meta/\" xmptk=\"ThetaThoughtShutter\">" +
208                                     "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"><rdf:Description rdf:about=\"\" xmlns:GPano=\"http://ns.google.com/photos/1.0/panorama/\">" +
209                                     "<GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer><GPano:ProjectionType>equirectangular</GPano:ProjectionType>" +
210                                     "<GPano:FullPanoWidthPixels>${targetImage.width}</GPano:FullPanoWidthPixels><GPano:FullPanoHeightPixels>${targetImage.height}</GPano:FullPanoHeightPixels>" +
211                                     "<GPano:CroppedAreaImageWidthPixels>${targetImage.width}</GPano:CroppedAreaImageWidthPixels><GPano:CroppedAreaImageHeightPixels>${targetImage.height}</GPano:CroppedAreaImageHeightPixels>"+
212                                     "<GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels><GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end=\"r\"?>"
213                     val xmpLength = xmpValue.length + xmpValue1.length + 3
214                     val byteArrayStream = ByteArrayOutputStream()
215                     targetImage.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayStream)
216                     val jpegImage = byteArrayStream.toByteArray()
217
218                     outputStream.write(jpegImage, 0, 20)
219                     outputStream.write(0xff)
220                     outputStream.write(0xe1)
221                     outputStream.write((xmpLength and 0xff00) shr 8)
222                     outputStream.write((xmpLength and 0x00ff))
223                     outputStream.write(xmpValue.toByteArray())
224                     outputStream.write(0x00)
225                     outputStream.write(xmpValue1.toByteArray())
226                     outputStream.write(jpegImage, 20, (jpegImage.size -20))
227
228                     //targetImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
229                     outputStream.flush()
230                     outputStream.close()
231                 }
232                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
233                 {
234                     values.put(MediaStore.Images.Media.IS_PENDING, false)
235                     resolver.update(imageUri, values, null, null)
236
237                 }
238             }
239         }
240         catch (t: Throwable)
241         {
242             t.printStackTrace()
243         }
244     }
245
246     private fun isExternalStorageWritable(): Boolean
247     {
248         return (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED)
249     }
250
251     companion object
252     {
253         private val  TAG = this.toString()
254         private const val FILENAME_FORMAT = "yyyyMMdd_HHmmss"
255     }
256
257 }