comparison android/DWindows.kt @ 2715:e9ad53d2271b

Android: Fix Intent based file chooser and switch to using it by default. The old dialog based file chooser is still included as a fallback for now. Only run the color chooser if called on the main thread, this should not be a problem since almost nothing except expose callbacks run on the main thread. The intent based file chooser can't choose directories, so for now operate like normal, but return the path to the chosen file instead of the file itself.
author bsmith@81767d24-ef19-dc11-ae90-00e081727c95
date Sun, 05 Dec 2021 15:08:13 +0000
parents 26bb1e4a97d0
children a1fea6b9f308
comparison
equal deleted inserted replaced
2714:26bb1e4a97d0 2715:e9ad53d2271b
62 import java.util.zip.ZipFile 62 import java.util.zip.ZipFile
63 import android.content.Intent 63 import android.content.Intent
64 import android.util.* 64 import android.util.*
65 import android.util.Base64 65 import android.util.Base64
66 import kotlin.math.* 66 import kotlin.math.*
67 import android.content.ContentUris
68
69
70
67 71
68 // Color Wheel section 72 // Color Wheel section
69 private val HUE_COLORS = intArrayOf( 73 private val HUE_COLORS = intArrayOf(
70 Color.RED, 74 Color.RED,
71 Color.YELLOW, 75 Color.YELLOW,
1653 private var appID: String? = null 1657 private var appID: String? = null
1654 private var paint = Paint() 1658 private var paint = Paint()
1655 private var bgcolor: Int? = null 1659 private var bgcolor: Int? = null
1656 private var fileURI: Uri? = null 1660 private var fileURI: Uri? = null
1657 private var fileLock = ReentrantLock() 1661 private var fileLock = ReentrantLock()
1658 private var fileCond = threadLock.newCondition() 1662 private var fileCond = fileLock.newCondition()
1659 // Lists of data for our Windows 1663 // Lists of data for our Windows
1660 private var windowTitles = mutableListOf<String?>() 1664 private var windowTitles = mutableListOf<String?>()
1661 private var windowMenuBars = mutableListOf<DWMenu?>() 1665 private var windowMenuBars = mutableListOf<DWMenu?>()
1662 private var windowStyles = mutableListOf<Int>() 1666 private var windowStyles = mutableListOf<Int>()
1663 private var windowDefault = mutableListOf<View?>() 1667 private var windowDefault = mutableListOf<View?>()
5016 fileCond.signal() 5020 fileCond.signal()
5017 fileLock.unlock() 5021 fileLock.unlock()
5018 } 5022 }
5019 } 5023 }
5020 5024
5025 fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String?>?): String? {
5026 var cursor: Cursor? = null
5027 val column = "_data"
5028 val projection = arrayOf(column)
5029
5030 try {
5031 cursor = context.contentResolver.query(
5032 uri!!, projection, selection, selectionArgs,
5033 null
5034 )
5035 if (cursor != null && cursor.moveToFirst()) {
5036 val index = cursor.getColumnIndexOrThrow(column)
5037 return cursor.getString(index)
5038 }
5039 } finally {
5040 cursor?.close()
5041 }
5042 return null
5043 }
5044
5045 // Defpath does not seem to be supported on Android using the ACTION_GET_CONTENT Intent
5021 fun fileBrowseNew(title: String, defpath: String?, ext: String?, flags: Int): String? 5046 fun fileBrowseNew(title: String, defpath: String?, ext: String?, flags: Int): String?
5022 { 5047 {
5023 var retval: String? = null 5048 var retval: String? = null
5024 5049
5025 // This can't be called from the main thread 5050 // This can't be called from the main thread
5026 if(Looper.getMainLooper() != Looper.myLooper()) { 5051 if(Looper.getMainLooper() != Looper.myLooper()) {
5052 var success = true
5053
5027 fileLock.lock() 5054 fileLock.lock()
5028 waitOnUiThread { 5055 waitOnUiThread {
5029 val fileintent = Intent(Intent.ACTION_GET_CONTENT) 5056 val fileintent = Intent(Intent.ACTION_GET_CONTENT)
5030 fileintent.type = "text/plain" 5057 // TODO: Filtering requires MIME types, not extensions
5058 fileintent.type = "*/*"
5031 fileintent.addCategory(Intent.CATEGORY_OPENABLE) 5059 fileintent.addCategory(Intent.CATEGORY_OPENABLE)
5032 startActivityForResult(fileintent, 100) 5060 try {
5033 } 5061 startActivityForResult(fileintent, 100)
5034 5062 } catch (e: ActivityNotFoundException) {
5035 // Wait until the intent finishes. 5063 success = false
5036 fileCond.await() 5064 }
5037 fileLock.unlock() 5065 }
5038 5066
5039 if(fileURI != null) { 5067 if(success) {
5040 retval = getUriRealPath(this, fileURI) 5068 // Wait until the intent finishes.
5069 fileCond.await()
5070 fileLock.unlock()
5071
5072 if (DocumentsContract.isDocumentUri(this, fileURI)) {
5073 // ExternalStorageProvider
5074 if (fileURI?.authority == "com.android.externalstorage.documents") {
5075 val docId = DocumentsContract.getDocumentId(fileURI)
5076 val split = docId.split(":").toTypedArray()
5077 retval = Environment.getExternalStorageDirectory().toString() + "/" + split[1]
5078 } else if (fileURI?.authority == "com.android.providers.downloads.documents") {
5079 val id = DocumentsContract.getDocumentId(fileURI)
5080 val contentUri = ContentUris.withAppendedId(
5081 Uri.parse("content://downloads/public_downloads"),
5082 java.lang.Long.valueOf(id)
5083 )
5084 retval = getDataColumn(this, contentUri, null, null)
5085 } else if (fileURI?.authority == "com.android.providers.media.documents") {
5086 val docId = DocumentsContract.getDocumentId(fileURI)
5087 val split = docId.split(":").toTypedArray()
5088 val type = split[0]
5089 var contentUri: Uri? = null
5090 if ("image" == type) {
5091 contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
5092 } else if ("video" == type) {
5093 contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
5094 } else if ("audio" == type) {
5095 contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
5096 }
5097 val selection = "_id=?"
5098 val selectionArgs = arrayOf<String?>(
5099 split[1]
5100 )
5101 retval = getDataColumn(this, contentUri, selection, selectionArgs)
5102 }
5103 } else if (fileURI?.scheme == "content") {
5104 retval = getDataColumn(this, fileURI, null, null)
5105 }
5106 // File
5107 else if (fileURI?.scheme == "file") {
5108 retval = fileURI?.path
5109 }
5110
5111 // If we are opening a directory DW_DIRECTORY_OPEN
5112 if(retval != null && flags == 2) {
5113 val split = retval.split("/")
5114 val filename = split.last()
5115
5116 if(filename != null) {
5117 val pathlen = retval.length
5118 val filelen = filename.length
5119
5120 retval = retval.substring(0, pathlen - filelen - 1)
5121 }
5122 }
5123 } else {
5124 // If we failed to start the intent... use old dialog
5125 fileLock.unlock()
5126 retval = fileBrowse(title, defpath, ext, flags)
5041 } 5127 }
5042 } 5128 }
5043 return retval 5129 return retval
5044 } 5130 }
5045 5131
5071 return retval 5157 return retval
5072 } 5158 }
5073 5159
5074 fun colorChoose(color: Int, alpha: Int, red: Int, green: Int, blue: Int): Int 5160 fun colorChoose(color: Int, alpha: Int, red: Int, green: Int, blue: Int): Int
5075 { 5161 {
5076 var retval: Int = 0 5162 var retval: Int = color
5077 5163
5078 waitOnUiThread { 5164 // This can't be called from the main thread
5079 val dialog = Dialog(this) 5165 if(Looper.getMainLooper() != Looper.myLooper()) {
5080 val colorWheel = ColorWheel(this, null, 0) 5166 waitOnUiThread {
5081 5167 val dialog = Dialog(this)
5082 dialog.setContentView(colorWheel) 5168 val colorWheel = ColorWheel(this, null, 0)
5083 colorWheel.rgb = Color.rgb(red, green, blue) 5169
5084 dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 5170 dialog.setContentView(colorWheel)
5085 dialog.show() 5171 colorWheel.rgb = Color.rgb(red, green, blue)
5086 retval = colorWheel.rgb 5172 dialog.window?.setLayout(
5173 ViewGroup.LayoutParams.MATCH_PARENT,
5174 ViewGroup.LayoutParams.MATCH_PARENT
5175 )
5176 dialog.show()
5177 retval = colorWheel.rgb
5178 }
5087 } 5179 }
5088 return retval 5180 return retval
5089 } 5181 }
5090 5182
5091 fun messageBox(title: String, body: String, flags: Int): Int 5183 fun messageBox(title: String, body: String, flags: Int): Int
5243 5335
5244 waitOnUiThread { 5336 waitOnUiThread {
5245 builder = NotificationCompat.Builder(this, appid) 5337 builder = NotificationCompat.Builder(this, appid)
5246 .setContentTitle(title) 5338 .setContentTitle(title)
5247 .setContentText(text) 5339 .setContentText(text)
5340 .setSmallIcon(R.mipmap.sym_def_app_icon)
5248 .setPriority(NotificationCompat.PRIORITY_DEFAULT) 5341 .setPriority(NotificationCompat.PRIORITY_DEFAULT)
5249 } 5342 }
5250 return builder 5343 return builder
5251 } 5344 }
5252 5345
5257 with(NotificationManagerCompat.from(this)) { 5350 with(NotificationManagerCompat.from(this)) {
5258 // notificationId is a unique int for each notification that you must define 5351 // notificationId is a unique int for each notification that you must define
5259 notify(notificationID, builder.build()) 5352 notify(notificationID, builder.build())
5260 } 5353 }
5261 } 5354 }
5262 }
5263
5264 /*
5265 * This method will parse out the real local file path from the file content URI.
5266 */
5267 private fun getUriRealPath(ctx: Context?, uri: Uri?): String? {
5268 var ret: String? = ""
5269 if (ctx != null && uri != null) {
5270 if (isContentUri(uri)) {
5271 if (isGooglePhotoDoc(uri.authority)) {
5272 ret = uri.lastPathSegment
5273 } else {
5274 ret = getImageRealPath(contentResolver, uri, null)
5275 }
5276 } else if (isFileUri(uri)) {
5277 ret = uri.path
5278 } else if (isDocumentUri(ctx, uri)) {
5279
5280 // Get uri related document id.
5281 val documentId = DocumentsContract.getDocumentId(uri)
5282
5283 // Get uri authority.
5284 val uriAuthority: String? = uri.authority
5285 if (isMediaDoc(uriAuthority)) {
5286 val idArr = documentId.split(":").toTypedArray()
5287 if (idArr.size == 2) {
5288 // First item is document type.
5289 val docType = idArr[0]
5290
5291 // Second item is document real id.
5292 val realDocId = idArr[1]
5293
5294 // Get content uri by document type.
5295 var mediaContentUri: Uri? = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
5296 if ("image" == docType) {
5297 mediaContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
5298 } else if ("video" == docType) {
5299 mediaContentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
5300 } else if ("audio" == docType) {
5301 mediaContentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
5302 }
5303
5304 // Get where clause with real document id.
5305 val whereClause = MediaStore.Images.Media._ID + " = " + realDocId
5306 ret = getImageRealPath(contentResolver, mediaContentUri, whereClause)
5307 }
5308 } else if (isDownloadDoc(uriAuthority)) {
5309 // Build download uri.
5310 val downloadUri: Uri = Uri.parse("content://downloads/public_downloads")
5311
5312 // Append download document id at uri end.
5313 val downloadUriAppendId: Uri =
5314 ContentUris.withAppendedId(downloadUri, java.lang.Long.valueOf(documentId))
5315 ret = getImageRealPath(contentResolver, downloadUriAppendId, null)
5316 } else if (isExternalStoreDoc(uriAuthority)) {
5317 val idArr = documentId.split(":").toTypedArray()
5318 if (idArr.size == 2) {
5319 val type = idArr[0]
5320 val realDocId = idArr[1]
5321 if ("primary".equals(type, ignoreCase = true)) {
5322 ret = Environment.getExternalStorageDirectory()
5323 .toString() + "/" + realDocId
5324 }
5325 }
5326 }
5327 }
5328 }
5329 return ret
5330 }
5331
5332 /* Check whether this uri represent a document or not. */
5333 private fun isDocumentUri(ctx: Context?, uri: Uri?): Boolean {
5334 var ret = false
5335 if (ctx != null && uri != null) {
5336 ret = DocumentsContract.isDocumentUri(ctx, uri)
5337 }
5338 return ret
5339 }
5340
5341 /* Check whether this uri is a content uri or not.
5342 * content uri like content://media/external/images/media/1302716
5343 */
5344 private fun isContentUri(uri: Uri?): Boolean {
5345 var ret = false
5346 if (uri != null) {
5347 val uriSchema: String? = uri.scheme
5348 if ("content".equals(uriSchema, ignoreCase = true)) {
5349 ret = true
5350 }
5351 }
5352 return ret
5353 }
5354
5355 /* Check whether this uri is a file uri or not.
5356 * file uri like file:///storage/41B7-12F1/DCIM/Camera/IMG_20180211_095139.jpg
5357 */
5358 private fun isFileUri(uri: Uri?): Boolean {
5359 var ret = false
5360 if (uri != null) {
5361 val uriSchema: String? = uri.scheme
5362 if ("file".equals(uriSchema, ignoreCase = true)) {
5363 ret = true
5364 }
5365 }
5366 return ret
5367 }
5368
5369
5370 /* Check whether this document is provided by ExternalStorageProvider. Return true means the file is saved in external storage. */
5371 private fun isExternalStoreDoc(uriAuthority: String?): Boolean {
5372 var ret = false
5373 if (uriAuthority != null && "com.android.externalstorage.documents" == uriAuthority) {
5374 ret = true
5375 }
5376 return ret
5377 }
5378
5379 /* Check whether this document is provided by DownloadsProvider. return true means this file is a downloaed file. */
5380 private fun isDownloadDoc(uriAuthority: String?): Boolean {
5381 var ret = false
5382 if (uriAuthority != null && "com.android.providers.downloads.documents" == uriAuthority) {
5383 ret = true
5384 }
5385 return ret
5386 }
5387
5388 /*
5389 * Check if MediaProvider provide this document, if true means this image is created in android media app.
5390 */
5391 private fun isMediaDoc(uriAuthority: String?): Boolean {
5392 var ret = false
5393 if (uriAuthority != null && "com.android.providers.media.documents" == uriAuthority) {
5394 ret = true
5395 }
5396 return ret
5397 }
5398
5399 /*
5400 * Check whether google photos provide this document, if true means this image is created in google photos app.
5401 */
5402 private fun isGooglePhotoDoc(uriAuthority: String?): Boolean {
5403 var ret = false
5404 if (uriAuthority != null && "com.google.android.apps.photos.content" == uriAuthority) {
5405 ret = true
5406 }
5407 return ret
5408 }
5409
5410 /* Return uri represented document file real local path.*/
5411 private fun getImageRealPath(
5412 contentResolver: ContentResolver,
5413 uri: Uri?,
5414 whereClause: String?
5415 ): String {
5416 var ret = ""
5417
5418 if(uri != null) {
5419 // Query the uri with condition.
5420 val cursor: Cursor? = contentResolver.query(uri, null, whereClause, null, null)
5421 if (cursor != null) {
5422 val moveToFirst: Boolean = cursor.moveToFirst()
5423 if (moveToFirst) {
5424
5425 // Get columns name by uri type.
5426 var columnName = MediaStore.Images.Media.DATA
5427 if (uri === MediaStore.Images.Media.EXTERNAL_CONTENT_URI) {
5428 columnName = MediaStore.Images.Media.DATA
5429 } else if (uri === MediaStore.Audio.Media.EXTERNAL_CONTENT_URI) {
5430 columnName = MediaStore.Audio.Media.DATA
5431 } else if (uri === MediaStore.Video.Media.EXTERNAL_CONTENT_URI) {
5432 columnName = MediaStore.Video.Media.DATA
5433 }
5434
5435 // Get column index.
5436 val imageColumnIndex: Int = cursor.getColumnIndex(columnName)
5437
5438 // Get column value which is the uri related file local path.
5439 ret = cursor.getString(imageColumnIndex)
5440 // Clean up
5441 cursor.close()
5442 }
5443 }
5444 }
5445 return ret
5446 } 5355 }
5447 5356
5448 /* 5357 /*
5449 * Native methods that are implemented by the 'dwindows' native library, 5358 * Native methods that are implemented by the 'dwindows' native library,
5450 * which is packaged with this application. 5359 * which is packaged with this application.