CH-J SM Mobilespráva serverů přes SSH
Menu
Vydané zdroje

CH-J SM Mobile

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / SM Mobile 175 souborů
sm-mobile-20261016T185235Z-8e93ecc1b59a Přidáno: · Sestaveno:
Documents.kt Přidáno: Datum není evidováno 15,0 KB · 329 řádků
Kód Stáhnout soubor
1package eu.chj.sm_mobile
3import android.app.Activity
4import android.content.ActivityNotFoundException
5import android.content.Intent
6import android.database.Cursor
7import android.graphics.Bitmap
8import android.graphics.Color
9import android.graphics.pdf.PdfRenderer
10import android.net.Uri
11import android.os.Handler
12import android.os.Looper
13import android.os.ParcelFileDescriptor
14import android.provider.DocumentsContract
15import android.provider.OpenableColumns
16import androidx.activity.result.ActivityResultLauncher
17import androidx.activity.result.contract.ActivityResultContracts
18import androidx.core.content.FileProvider
19import androidx.fragment.app.FragmentActivity
20import io.flutter.plugin.common.MethodCall
21import io.flutter.plugin.common.MethodChannel
22import java.io.ByteArrayOutputStream
23import java.io.File
24import java.io.FileNotFoundException
25import java.io.IOException
26import java.util.concurrent.Executors
28/**
29 * Files on the device through the Storage Access Framework. Only documents
30 * the user picks (and folders the user grants) are accessed; no broad
31 * storage permission is requested.
32 */
33class Documents(private val activity: FragmentActivity) : MethodChannel.MethodCallHandler {
34 var channel: MethodChannel? = null
35 private val io = Executors.newSingleThreadExecutor()
36 private val main = Handler(Looper.getMainLooper())
37 private var pending: ((Intent?) -> Unit)? = null
38 private val incoming = mutableListOf<Map<String, Any?>>()
39 private val launcher: ActivityResultLauncher<Intent> =
40 activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
41 val callback = pending
42 pending = null
43 callback?.invoke(if (r.resultCode == Activity.RESULT_OK) r.data else null)
44 }
46 private val resolver get() = activity.contentResolver
48 private fun launch(intent: Intent, result: MethodChannel.Result, then: (Intent?) -> Unit) {
49 if (pending != null) {
50 result.error("busy", "A picker is already open", null)
51 return
52 }
53 pending = then
54 try {
55 launcher.launch(intent)
56 } catch (e: ActivityNotFoundException) {
57 pending = null
58 result.error("unsupported", "No document provider", null)
59 }
60 }
62 private fun background(result: MethodChannel.Result, work: () -> Any?) {
63 io.execute {
64 try {
65 val value = work()
66 main.post { result.success(value) }
67 } catch (e: SecurityException) {
68 main.post { result.error("denied", e.message, null) }
69 } catch (e: FileNotFoundException) {
70 main.post { result.error("notFound", e.message, null) }
71 } catch (e: ReadOnly) {
72 main.post { result.error("readOnly", e.message, null) }
73 } catch (e: IOException) {
74 val code = if (e.message?.contains("ENOSPC") == true ||
75 e.message?.contains("No space") == true) "noSpace" else "io"
76 main.post { result.error(code, e.message, null) }
77 } catch (e: Exception) {
78 main.post { result.error("io", e.message, null) }
79 }
80 }
81 }
83 class ReadOnly(message: String) : Exception(message)
85 private fun persist(uri: Uri, flags: Int) {
86 val rw = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
87 try {
88 resolver.takePersistableUriPermission(uri, flags and rw)
89 } catch (_: SecurityException) {
90 try {
91 resolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
92 } catch (_: SecurityException) {
93 // A temporary grant only (for example a file shared by another app).
94 }
95 }
96 }
98 private fun canWrite(uri: Uri): Boolean =
99 activity.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ==
100 android.content.pm.PackageManager.PERMISSION_GRANTED
102 private fun describe(uri: Uri, location: String? = null): Map<String, Any?> {
103 var name: String? = null
104 var size: Long? = null
105 var mime: String? = null
106 var modified: Long? = null
107 var flags = 0
108 try {
109 resolver.query(uri, null, null, null, null)?.use { c: Cursor ->
110 if (c.moveToFirst()) {
111 fun col(n: String) = c.getColumnIndex(n)
112 col(OpenableColumns.DISPLAY_NAME).takeIf { it >= 0 }?.let { name = c.getString(it) }
113 col(OpenableColumns.SIZE).takeIf { it >= 0 && !c.isNull(it) }?.let { size = c.getLong(it) }
114 col(DocumentsContract.Document.COLUMN_MIME_TYPE).takeIf { it >= 0 }?.let { mime = c.getString(it) }
115 col(DocumentsContract.Document.COLUMN_LAST_MODIFIED).takeIf { it >= 0 && !c.isNull(it) }
116 ?.let { modified = c.getLong(it) }
117 col(DocumentsContract.Document.COLUMN_FLAGS).takeIf { it >= 0 }?.let { flags = c.getInt(it) }
118 }
119 }
120 } catch (_: Exception) {
121 // Some providers answer only part of the columns.
122 }
123 if (mime == null) mime = resolver.getType(uri)
124 val directory = mime == DocumentsContract.Document.MIME_TYPE_DIR
125 val providerWrite = flags and DocumentsContract.Document.FLAG_SUPPORTS_WRITE != 0 ||
126 !DocumentsContract.isDocumentUri(activity, uri)
127 return mapOf(
128 "id" to uri.toString(),
129 "name" to (name ?: uri.lastPathSegment ?: "?"),
130 "size" to size,
131 "mime" to mime,
132 "modified" to modified,
133 "directory" to directory,
134 "writable" to (!directory && providerWrite && canWrite(uri)),
135 "location" to (location ?: uri.authority),
136 )
137 }
139 private fun readAll(uri: Uri): ByteArray =
140 resolver.openInputStream(uri)?.use { it.readBytes() } ?: throw FileNotFoundException(uri.toString())
142 private fun writeAll(uri: Uri, bytes: ByteArray) {
143 val out = try {
144 resolver.openOutputStream(uri, "wt")
145 } catch (_: IllegalArgumentException) {
146 resolver.openOutputStream(uri, "w")
147 } ?: throw ReadOnly("No output stream")
148 out.use {
149 it.write(bytes)
150 it.flush()
151 }
152 }
154 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
155 when (call.method) {
156 "pick" -> {
157 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
158 .addCategory(Intent.CATEGORY_OPENABLE)
159 .setType("*/*")
160 .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, call.argument<Boolean>("multiple") == true)
161 .addFlags(
162 Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
163 Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
164 )
165 launch(intent, result) { data ->
166 if (data == null) return@launch result.success(emptyList<Any>())
167 val uris = mutableListOf<Uri>()
168 data.clipData?.let { clip -> for (i in 0 until clip.itemCount) uris.add(clip.getItemAt(i).uri) }
169 if (uris.isEmpty()) data.data?.let { uris.add(it) }
170 background(result) {
171 uris.map {
172 persist(it, data.flags)
173 describe(it)
174 }
175 }
176 }
177 }
178 "pickFolder" -> {
179 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).addFlags(
180 Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
181 Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
182 )
183 launch(intent, result) { data ->
184 val tree = data?.data ?: return@launch result.success(null)
185 background(result) {
186 persist(tree, data.flags)
187 val doc = DocumentsContract.buildDocumentUriUsingTree(tree, DocumentsContract.getTreeDocumentId(tree))
188 describe(doc)
189 }
190 }
191 }
192 "list" -> background(result) {
193 val folder = Uri.parse(call.argument<String>("id")!!)
194 val children = DocumentsContract.buildChildDocumentsUriUsingTree(
195 folder, DocumentsContract.getDocumentId(folder),
196 )
197 val out = mutableListOf<Map<String, Any?>>()
198 resolver.query(
199 children, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, null, null,
200 )?.use { c ->
201 while (c.moveToNext()) {
202 out.add(describe(DocumentsContract.buildDocumentUriUsingTree(folder, c.getString(0))))
203 }
204 }
205 out
206 }
207 "stat" -> background(result) { describe(Uri.parse(call.argument<String>("id")!!)) }
208 "read" -> background(result) { readAll(Uri.parse(call.argument<String>("id")!!)) }
209 "write" -> background(result) {
210 val uri = Uri.parse(call.argument<String>("id")!!)
211 val bytes = call.argument<ByteArray>("bytes")!!
212 if (!canWrite(uri)) throw ReadOnly("No write permission")
213 // SAF has no atomic replace: keep the old content and put it back if
214 // writing fails part way.
215 val previous = try { readAll(uri) } catch (_: Exception) { null }
216 try {
217 writeAll(uri, bytes)
218 } catch (e: Exception) {
219 if (previous != null) try { writeAll(uri, previous) } catch (_: Exception) {}
220 throw e
221 }
222 describe(uri)
223 }
224 "saveAs" -> {
225 val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
226 .addCategory(Intent.CATEGORY_OPENABLE)
227 .setType(call.argument<String>("mime") ?: "application/octet-stream")
228 .putExtra(Intent.EXTRA_TITLE, call.argument<String>("name"))
229 val bytes = call.argument<ByteArray>("bytes")!!
230 launch(intent, result) { data ->
231 val uri = data?.data ?: return@launch result.success(null)
232 background(result) {
233 persist(uri, data.flags or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
234 writeAll(uri, bytes)
235 describe(uri)
236 }
237 }
238 }
239 "create" -> background(result) {
240 val folder = Uri.parse(call.argument<String>("folder")!!)
241 val uri = DocumentsContract.createDocument(
242 resolver, folder, call.argument<String>("mime") ?: "application/octet-stream",
243 call.argument<String>("name")!!,
244 ) ?: throw ReadOnly("Cannot create")
245 writeAll(uri, call.argument<ByteArray>("bytes")!!)
246 describe(uri)
247 }
248 "preview" -> {
249 val file = File(call.argument<String>("path")!!)
250 if (!file.canonicalPath.startsWith(activity.cacheDir.canonicalPath)) {
251 return result.error("denied", "Only private temporary copies", null)
252 }
253 val uri = FileProvider.getUriForFile(activity, "${activity.packageName}.smfiles", file)
254 val view = Intent(Intent.ACTION_VIEW)
255 .setDataAndType(uri, call.argument<String>("mime") ?: resolver.getType(uri) ?: "*/*")
256 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
257 try {
258 activity.startActivity(Intent.createChooser(view, file.name))
259 result.success(true)
260 } catch (_: ActivityNotFoundException) {
261 result.success(false)
262 }
263 }
264 "pdfPageCount" -> background(result) {
265 ParcelFileDescriptor.open(File(call.argument<String>("path")!!), ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
266 PdfRenderer(fd).use { it.pageCount }
267 }
268 }
269 "pdfPage" -> background(result) {
270 val width = call.argument<Int>("width")!!.coerceIn(200, 3000)
271 ParcelFileDescriptor.open(File(call.argument<String>("path")!!), ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
272 PdfRenderer(fd).use { renderer ->
273 renderer.openPage(call.argument<Int>("index")!!).use { page ->
274 val height = (width.toLong() * page.height / page.width).toInt().coerceAtLeast(1)
275 val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
276 bitmap.eraseColor(Color.WHITE)
277 page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
278 val out = ByteArrayOutputStream()
279 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
280 bitmap.recycle()
281 out.toByteArray()
282 }
283 }
284 }
285 }
286 "takeIncoming" -> {
287 val list = incoming.toList()
288 incoming.clear()
289 result.success(list)
290 }
291 "forget" -> {
292 try {
293 resolver.releasePersistableUriPermission(
294 Uri.parse(call.argument<String>("id")!!),
295 Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
296 )
297 } catch (_: Exception) {}
298 result.success(null)
299 }
300 else -> result.notImplemented()
301 }
302 }
304 /** Files from "Open with" / "Share" in other apps. */
305 fun handle(intent: Intent?) {
306 if (intent == null) return
307 val uris = mutableListOf<Uri>()
308 when (intent.action) {
309 Intent.ACTION_VIEW -> intent.data?.let { uris.add(it) }
310 Intent.ACTION_SEND -> @Suppress("DEPRECATION")
311 (intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM))?.let { uris.add(it) }
312 Intent.ACTION_SEND_MULTIPLE -> @Suppress("DEPRECATION")
313 intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let { uris.addAll(it) }
314 }
315 val content = uris.filter { it.scheme == "content" }
316 if (content.isEmpty()) return
317 io.execute {
318 val docs = content.map { describe(it) }
319 main.post {
320 val ch = channel
321 if (ch == null) incoming.addAll(docs) else {
322 incoming.addAll(docs)
323 ch.invokeMethod("incoming", incoming.toList())
324 incoming.clear()
325 }
326 }
327 }
328 }
SHA-2568a6973643dbcb318eadeb1ed102e88b0aabf56021e4aa39ada3081fe95f65dd9

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Soubory