Mercurial > dwindows
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. |