343 lines
10 KiB
Kotlin
343 lines
10 KiB
Kotlin
package jp.juggler.util.data
|
|
|
|
import android.content.ClipData
|
|
import android.content.ContentResolver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.provider.OpenableColumns
|
|
import android.webkit.MimeTypeMap
|
|
import androidx.annotation.RawRes
|
|
import jp.juggler.util.log.LogCategory
|
|
import java.io.InputStream
|
|
|
|
private val log = LogCategory("StorageUtils")
|
|
|
|
// internal object StorageUtils{
|
|
//
|
|
// private val log = LogCategory("StorageUtils")
|
|
//
|
|
// private const val PATH_TREE = "tree"
|
|
// private const val PATH_DOCUMENT = "document"
|
|
//
|
|
// internal class FileInfo(any_uri : String?) {
|
|
//
|
|
// var uri : Uri? = null
|
|
// private var mime_type : String? = null
|
|
//
|
|
// init {
|
|
// if(any_uri != null) {
|
|
// uri = if(any_uri.startsWith("/")) {
|
|
// Uri.fromFile(File(any_uri))
|
|
// } else {
|
|
// any_uri.toUri()
|
|
// }
|
|
// val ext = MimeTypeMap.getFileExtensionFromUrl(any_uri)
|
|
// if(ext != null) {
|
|
// mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase(Locale.JAPAN))
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// private fun getSecondaryStorageVolumesMap(context : Context) : Map<String, String> {
|
|
// val result = HashMap<String, String>()
|
|
// try {
|
|
// val sm = context.applicationContext.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
|
|
// if(sm == null) {
|
|
// log.e("can't get StorageManager")
|
|
// } else {
|
|
//
|
|
// // SDカードスロットのある7.0端末が手元にないから検証できない
|
|
// // if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ){
|
|
// // for(StorageVolume volume : sm.getStorageVolumes() ){
|
|
// // // String path = volume.getPath();
|
|
// // String state = volume.getState();
|
|
// //
|
|
// // }
|
|
// // }
|
|
//
|
|
// val getVolumeList = sm.javaClass.getMethod("getVolumeList")
|
|
// val volumes = getVolumeList.invoke(sm)
|
|
// log.d("volumes type=%s", volumes.javaClass)
|
|
//
|
|
// if(volumes is ArrayList<*>) {
|
|
// //
|
|
// for(volume in volumes) {
|
|
// val volume_clazz = volume.javaClass
|
|
//
|
|
// val path = volume_clazz.getMethod("getPath").invoke(volume) as? String
|
|
// val state = volume_clazz.getMethod("getState").invoke(volume) as? String
|
|
// if(path != null && state == "mounted") {
|
|
// //
|
|
// val isPrimary = volume_clazz.getMethod("isPrimary").invoke(volume) as? Boolean
|
|
// if(isPrimary == true) result["primary"] = path
|
|
// //
|
|
// val uuid = volume_clazz.getMethod("getUuid").invoke(volume) as? String
|
|
// if(uuid != null) result[uuid] = path
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// } catch(ex : Throwable) {
|
|
// log.trace(ex)
|
|
// }
|
|
//
|
|
// return result
|
|
// }
|
|
//
|
|
// private fun isExternalStorageDocument(uri : Uri) : Boolean {
|
|
// return "com.android.externalstorage.documents" == uri.authority
|
|
// }
|
|
//
|
|
// private fun getDocumentId(documentUri : Uri) : String {
|
|
// val paths = documentUri.pathSegments
|
|
// if(paths.size >= 2 && PATH_DOCUMENT == paths[0]) {
|
|
// // document
|
|
// return paths[1]
|
|
// }
|
|
// if(paths.size >= 4 && PATH_TREE == paths[0]
|
|
// && PATH_DOCUMENT == paths[2]) {
|
|
// // document in tree
|
|
// return paths[3]
|
|
// }
|
|
// if(paths.size >= 2 && PATH_TREE == paths[0]) {
|
|
// // tree
|
|
// return paths[1]
|
|
// }
|
|
// throw IllegalArgumentException("Invalid URI: $documentUri")
|
|
// }
|
|
|
|
// fun getFile(context : Context, path : String) : File? {
|
|
// try {
|
|
// if(path.startsWith("/")) return File(path)
|
|
// val uri = path.toUri()
|
|
// if("file" == uri.scheme) return File(uri.path!!)
|
|
//
|
|
// // if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT )
|
|
// run {
|
|
// if(isExternalStorageDocument(uri)) {
|
|
// try {
|
|
// val docId = getDocumentId(uri)
|
|
// val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
// if(split.size >= 2) {
|
|
// val uuid = split[0]
|
|
// if("primary".equals(uuid, ignoreCase = true)) {
|
|
// return File(Environment.getExternalStorageDirectory().toString() + "/" + split[1])
|
|
// } else {
|
|
// val volume_map =
|
|
// getSecondaryStorageVolumesMap(
|
|
// context
|
|
// )
|
|
// val volume_path = volume_map[uuid]
|
|
// if(volume_path != null) {
|
|
// return File(volume_path + "/" + split[1])
|
|
// }
|
|
// }
|
|
// }
|
|
// } catch(ex : Throwable) {
|
|
// log.trace(ex)
|
|
// }
|
|
//
|
|
// }
|
|
// }
|
|
// // MediaStore Uri
|
|
// context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
|
// if(cursor.moveToFirst()) {
|
|
// val col_count = cursor.columnCount
|
|
// for(i in 0 until col_count) {
|
|
// val type = cursor.getType(i)
|
|
// if(type != Cursor.FIELD_TYPE_STRING) continue
|
|
// val name = cursor.getColumnName(i)
|
|
// val value = cursor.getStringOrNull(i)
|
|
// if(value != null && value.isNotEmpty() && "filePath" == name) return File(value)
|
|
// }
|
|
// }
|
|
// }
|
|
// } catch(ex : Throwable) {
|
|
// log.trace(ex)
|
|
// }
|
|
//
|
|
// return null
|
|
// }
|
|
//
|
|
|
|
//
|
|
//
|
|
//}
|
|
|
|
private const val MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"
|
|
|
|
private val mimeTypeExMap: HashMap<String, String> by lazy {
|
|
val map = HashMap<String, String>()
|
|
map["BDM"] = "application/vnd.syncml.dm+wbxml"
|
|
map["DAT"] = ""
|
|
map["TID"] = ""
|
|
map["js"] = "text/javascript"
|
|
map["sh"] = "application/x-sh"
|
|
map["lua"] = "text/x-lua"
|
|
map
|
|
}
|
|
|
|
@Suppress("unused")
|
|
fun getMimeType(log: LogCategory?, src: String): String {
|
|
MimeTypeMap.getFileExtensionFromUrl(src)
|
|
?.notEmpty()?.lowercase()?.let { ext ->
|
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
|
|
?.notEmpty()?.let { return it }
|
|
|
|
val mimeType = mimeTypeExMap[ext]
|
|
mimeType?.notEmpty()?.let { return it }
|
|
|
|
// 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない
|
|
if (mimeType == null && log != null) {
|
|
log.w("getMimeType(): unknown file extension '$ext'")
|
|
}
|
|
}
|
|
|
|
return MIME_TYPE_APPLICATION_OCTET_STREAM
|
|
}
|
|
|
|
fun getDocumentName(contentResolver: ContentResolver, uri: Uri): String {
|
|
val errorName = "no_name"
|
|
return contentResolver.query(uri, null, null, null, null, null)
|
|
?.use { cursor ->
|
|
return if (!cursor.moveToFirst()) {
|
|
errorName
|
|
} else {
|
|
cursor.getStringOrNull(OpenableColumns.DISPLAY_NAME) ?: errorName
|
|
}
|
|
}
|
|
?: errorName
|
|
}
|
|
|
|
fun getStreamSize(bClose: Boolean, inStream: InputStream): Long {
|
|
try {
|
|
var size = 0L
|
|
val tmpBuffer = ByteArray(0x10000)
|
|
while (true) {
|
|
val delta = inStream.read(tmpBuffer, 0, tmpBuffer.size)
|
|
if (delta <= 0) break
|
|
size += delta.toLong()
|
|
}
|
|
return size
|
|
} finally {
|
|
if (bClose) try {
|
|
inStream.close()
|
|
} catch (_: Throwable) {
|
|
}
|
|
}
|
|
}
|
|
|
|
//fun File.loadByteArray() : ByteArray {
|
|
// val size = this.length().toInt()
|
|
// val data = ByteArray(size)
|
|
// FileInputStream(this).use { inStream ->
|
|
// val nRead = 0
|
|
// while(nRead < size) {
|
|
// val delta = inStream.read(data, nRead, size - nRead)
|
|
// if(delta <= 0) break
|
|
// }
|
|
// return data
|
|
// }
|
|
//}
|
|
|
|
fun Context.loadRawResource(@RawRes resId: Int): ByteArray =
|
|
resources.openRawResource(resId).use { it.readBytes() }
|
|
|
|
fun intentOpenDocument(mimeType: String): Intent {
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
intent.type = mimeType // "image/*"
|
|
return intent
|
|
}
|
|
|
|
fun intentGetContent(
|
|
allowMultiple: Boolean,
|
|
caption: String,
|
|
mimeTypes: Array<out String>,
|
|
): Intent {
|
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
|
|
if (allowMultiple) {
|
|
// EXTRA_ALLOW_MULTIPLE は API 18 (4.3)以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
|
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
}
|
|
|
|
// EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
|
|
intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes)
|
|
|
|
intent.type = when (mimeTypes.size) {
|
|
1 -> mimeTypes[0]
|
|
|
|
// On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work
|
|
// it only recognizes the first filter you specify.
|
|
else -> "*/*"
|
|
}
|
|
|
|
return Intent.createChooser(intent, caption)
|
|
}
|
|
|
|
data class UriAndType(val uri: Uri, val mimeType: String?)
|
|
|
|
fun MutableList<UriAndType>.addNoDuplicate(
|
|
contentResolver: ContentResolver,
|
|
uri: Uri?,
|
|
type: String? = null,
|
|
) {
|
|
uri ?: return
|
|
if (any { it.uri == uri }) return
|
|
val mimeType = try {
|
|
type ?: contentResolver.getType(uri)
|
|
} catch (ex: Throwable) {
|
|
log.w(ex, "contentResolver.getType failed. uri=$uri")
|
|
null
|
|
}
|
|
add(UriAndType(uri, mimeType))
|
|
}
|
|
|
|
fun List<UriAndType>.grantPermissions(
|
|
contentResolver: ContentResolver,
|
|
) {
|
|
forEach {
|
|
try {
|
|
contentResolver.takePersistableUriPermission(
|
|
it.uri,
|
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
)
|
|
} catch (_: Throwable) {
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* URIのリストに対してMIMEタイプの取得とtakePersistableUriPermissionを行う
|
|
* @return UriAndTypeのリスト
|
|
*/
|
|
fun List<Uri>.checkMimeTypeAndGrant(
|
|
contentResolver: ContentResolver,
|
|
) = buildList {
|
|
this@checkMimeTypeAndGrant.forEach { addNoDuplicate(contentResolver, it) }
|
|
grantPermissions(contentResolver)
|
|
}
|
|
|
|
val ClipData.uris
|
|
get() = (0 until itemCount).mapNotNull { getItemAt(it)?.uri }
|
|
|
|
/**
|
|
* ピッカーが返したIntentからURIのリストを読み、MIMEタイプの取得とtakePersistableUriPermissionを行う
|
|
* @return UriAndTypeのリスト
|
|
*/
|
|
fun Intent.checkMimeTypeAndGrant(
|
|
contentResolver: ContentResolver,
|
|
) = buildList {
|
|
// 単一選択
|
|
addNoDuplicate(contentResolver, data, type)
|
|
|
|
// 複数選択
|
|
clipData?.uris?.forEach { addNoDuplicate(contentResolver, it) }
|
|
|
|
grantPermissions(contentResolver)
|
|
}
|