diff options
| author | Po Lu | 2023-07-28 15:19:37 +0800 |
|---|---|---|
| committer | Po Lu | 2023-07-28 15:19:37 +0800 |
| commit | 0709e03f88cdef8f785338cab9315b527db0854e (patch) | |
| tree | e4f81f91e0f2e18bb60e75aa9cb3d0c61ac4bb8e /java | |
| parent | 03cf3bbb5c38aa55abd6f7d4860025f7482fcfc3 (diff) | |
| download | emacs-0709e03f88cdef8f785338cab9315b527db0854e.tar.gz emacs-0709e03f88cdef8f785338cab9315b527db0854e.zip | |
Allow quitting from Android content provider operations
* doc/emacs/android.texi (Android Document Providers): Say that
quitting is now possible.
* java/org/gnu/emacs/EmacsNative.java (EmacsNative): New
functions `safSyncAndReadInput', `safync' and `safPostRequest'.
* java/org/gnu/emacs/EmacsSafThread.java: New file. Move
cancel-able SAF operations here.
* java/org/gnu/emacs/EmacsService.java (EmacsService): Allow
quitting from most SAF operations.
* src/androidvfs.c (android_saf_exception_check): Return EINTR
if OperationCanceledException is received.
(android_saf_stat, android_saf_access)
(android_document_id_from_name, android_saf_tree_opendir_1)
(android_saf_file_open): Don't allow reentrant calls from async
input handlers.
(NATIVE_NAME): Implement new synchronization primitives for JNI.
(android_vfs_init): Initialize new class.
* src/dired.c (open_directory): Handle EINTR from opendir.
* src/sysdep.c: Describe which operations may return EINTR on
Android.
Diffstat (limited to 'java')
| -rw-r--r-- | java/org/gnu/emacs/EmacsNative.java | 17 | ||||
| -rw-r--r-- | java/org/gnu/emacs/EmacsSafThread.java | 922 | ||||
| -rw-r--r-- | java/org/gnu/emacs/EmacsService.java | 520 |
3 files changed, 986 insertions, 473 deletions
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index d4d502ede5a..ea200037218 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java | |||
| @@ -257,6 +257,23 @@ public final class EmacsNative | |||
| 257 | 257 | ||
| 258 | public static native void notifyPixelsChanged (Bitmap bitmap); | 258 | public static native void notifyPixelsChanged (Bitmap bitmap); |
| 259 | 259 | ||
| 260 | |||
| 261 | /* Functions used to synchronize document provider access with the | ||
| 262 | main thread. */ | ||
| 263 | |||
| 264 | /* Wait for a call to `safPostRequest' while also reading async | ||
| 265 | input. | ||
| 266 | |||
| 267 | If asynchronous input arrives and sets Vquit_flag, return 1. */ | ||
| 268 | public static native int safSyncAndReadInput (); | ||
| 269 | |||
| 270 | /* Wait for a call to `safPostRequest'. */ | ||
| 271 | public static native void safSync (); | ||
| 272 | |||
| 273 | /* Post the semaphore used to await the completion of SAF | ||
| 274 | operations. */ | ||
| 275 | public static native void safPostRequest (); | ||
| 276 | |||
| 260 | static | 277 | static |
| 261 | { | 278 | { |
| 262 | /* Older versions of Android cannot link correctly with shared | 279 | /* Older versions of Android cannot link correctly with shared |
diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java new file mode 100644 index 00000000000..fd06603fab3 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSafThread.java | |||
| @@ -0,0 +1,922 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.ContentResolver; | ||
| 23 | import android.database.Cursor; | ||
| 24 | import android.net.Uri; | ||
| 25 | |||
| 26 | import android.os.Build; | ||
| 27 | import android.os.CancellationSignal; | ||
| 28 | import android.os.Handler; | ||
| 29 | import android.os.HandlerThread; | ||
| 30 | import android.os.ParcelFileDescriptor; | ||
| 31 | |||
| 32 | import android.provider.DocumentsContract; | ||
| 33 | import android.provider.DocumentsContract.Document; | ||
| 34 | |||
| 35 | |||
| 36 | |||
| 37 | /* Emacs runs long-running SAF operations on a second thread running | ||
| 38 | its own handler. These operations include opening files and | ||
| 39 | maintaining the path to document ID cache. | ||
| 40 | |||
| 41 | #if 0 | ||
| 42 | Because Emacs paths are based on file display names, while Android | ||
| 43 | document identifiers have no discernible hierarchy of their own, | ||
| 44 | each file name lookup must carry out a repeated search for | ||
| 45 | directory documents with the names of all of the file name's | ||
| 46 | constituent components, where each iteration searches within the | ||
| 47 | directory document identified by the previous iteration. | ||
| 48 | |||
| 49 | A time limited cache tying components to document IDs is maintained | ||
| 50 | in order to speed up consecutive searches for file names sharing | ||
| 51 | the same components. Since listening for changes to each document | ||
| 52 | in the cache is prohibitively expensive, Emacs instead elects to | ||
| 53 | periodically remove entries that are older than a predetermined | ||
| 54 | amount of a time. | ||
| 55 | |||
| 56 | The cache is structured much like the directory trees whose | ||
| 57 | information it records, with each entry in the cache containing a | ||
| 58 | list of entries for their children. File name lookup consults the | ||
| 59 | cache and populates it with missing information simultaneously. | ||
| 60 | |||
| 61 | This is not yet implemented. | ||
| 62 | #endif | ||
| 63 | |||
| 64 | Long-running operations are also run on this thread for another | ||
| 65 | reason: Android uses special cancellation objects to terminate | ||
| 66 | ongoing IPC operations. However, the functions that perform these | ||
| 67 | operations block instead of providing mechanisms for the caller to | ||
| 68 | wait for their completion while also reading async input, as a | ||
| 69 | consequence of which the calling thread is unable to signal the | ||
| 70 | cancellation objects that it provides. Performing the blocking | ||
| 71 | operations in this auxiliary thread enables the main thread to wait | ||
| 72 | for completion itself, signaling the cancellation objects when it | ||
| 73 | deems necessary. */ | ||
| 74 | |||
| 75 | |||
| 76 | |||
| 77 | public final class EmacsSafThread extends HandlerThread | ||
| 78 | { | ||
| 79 | /* The content resolver used by this thread. */ | ||
| 80 | private final ContentResolver resolver; | ||
| 81 | |||
| 82 | /* Handler for this thread's main loop. */ | ||
| 83 | private Handler handler; | ||
| 84 | |||
| 85 | /* File access mode constants. See `man 7 inode'. */ | ||
| 86 | public static final int S_IRUSR = 0000400; | ||
| 87 | public static final int S_IWUSR = 0000200; | ||
| 88 | public static final int S_IFCHR = 0020000; | ||
| 89 | public static final int S_IFDIR = 0040000; | ||
| 90 | public static final int S_IFREG = 0100000; | ||
| 91 | |||
| 92 | public | ||
| 93 | EmacsSafThread (ContentResolver resolver) | ||
| 94 | { | ||
| 95 | super ("Document provider access thread"); | ||
| 96 | this.resolver = resolver; | ||
| 97 | } | ||
| 98 | |||
| 99 | |||
| 100 | |||
| 101 | @Override | ||
| 102 | public void | ||
| 103 | start () | ||
| 104 | { | ||
| 105 | super.start (); | ||
| 106 | |||
| 107 | /* Set up the handler after the thread starts. */ | ||
| 108 | handler = new Handler (getLooper ()); | ||
| 109 | } | ||
| 110 | |||
| 111 | |||
| 112 | |||
| 113 | /* ``Prototypes'' for nested functions that are run within the SAF | ||
| 114 | thread and accepts a cancellation signal. They differ in their | ||
| 115 | return types. */ | ||
| 116 | |||
| 117 | private abstract class SafIntFunction | ||
| 118 | { | ||
| 119 | /* The ``throws Throwable'' here is a Java idiosyncracy that tells | ||
| 120 | the compiler to allow arbitrary error objects to be signaled | ||
| 121 | from within this function. | ||
| 122 | |||
| 123 | Later, runIntFunction will try to re-throw any error object | ||
| 124 | generated by this function in the Emacs thread, using a trick | ||
| 125 | to avoid the compiler requirement to expressly declare that an | ||
| 126 | error (and which types of errors) will be signaled. */ | ||
| 127 | |||
| 128 | public abstract int runInt (CancellationSignal signal) | ||
| 129 | throws Throwable; | ||
| 130 | }; | ||
| 131 | |||
| 132 | private abstract class SafObjectFunction | ||
| 133 | { | ||
| 134 | /* The ``throws Throwable'' here is a Java idiosyncracy that tells | ||
| 135 | the compiler to allow arbitrary error objects to be signaled | ||
| 136 | from within this function. | ||
| 137 | |||
| 138 | Later, runObjectFunction will try to re-throw any error object | ||
| 139 | generated by this function in the Emacs thread, using a trick | ||
| 140 | to avoid the compiler requirement to expressly declare that an | ||
| 141 | error (and which types of errors) will be signaled. */ | ||
| 142 | |||
| 143 | public abstract Object runObject (CancellationSignal signal) | ||
| 144 | throws Throwable; | ||
| 145 | }; | ||
| 146 | |||
| 147 | |||
| 148 | |||
| 149 | /* Functions that run cancel-able queries. These functions are | ||
| 150 | internally run within the SAF thread. */ | ||
| 151 | |||
| 152 | /* Throw the specified EXCEPTION. The type template T is erased by | ||
| 153 | the compiler before the object is compiled, so the compiled code | ||
| 154 | simply throws EXCEPTION without the cast being verified. | ||
| 155 | |||
| 156 | T should be RuntimeException to obtain the desired effect of | ||
| 157 | throwing an exception without a compiler check. */ | ||
| 158 | |||
| 159 | @SuppressWarnings("unchecked") | ||
| 160 | private static <T extends Throwable> void | ||
| 161 | throwException (Throwable exception) | ||
| 162 | throws T | ||
| 163 | { | ||
| 164 | throw (T) exception; | ||
| 165 | } | ||
| 166 | |||
| 167 | /* Run the given function (or rather, its `runInt' field) within the | ||
| 168 | SAF thread, waiting for it to complete. | ||
| 169 | |||
| 170 | If async input arrives in the meantime and sets Vquit_flag, | ||
| 171 | signal the cancellation signal supplied to that function. | ||
| 172 | |||
| 173 | Rethrow any exception thrown from that function, and return its | ||
| 174 | value otherwise. */ | ||
| 175 | |||
| 176 | private int | ||
| 177 | runIntFunction (final SafIntFunction function) | ||
| 178 | { | ||
| 179 | final EmacsHolder<Object> result; | ||
| 180 | final CancellationSignal signal; | ||
| 181 | Throwable throwable; | ||
| 182 | |||
| 183 | result = new EmacsHolder<Object> (); | ||
| 184 | signal = new CancellationSignal (); | ||
| 185 | |||
| 186 | handler.post (new Runnable () { | ||
| 187 | @Override | ||
| 188 | public void | ||
| 189 | run () | ||
| 190 | { | ||
| 191 | try | ||
| 192 | { | ||
| 193 | result.thing | ||
| 194 | = Integer.valueOf (function.runInt (signal)); | ||
| 195 | } | ||
| 196 | catch (Throwable throwable) | ||
| 197 | { | ||
| 198 | result.thing = throwable; | ||
| 199 | } | ||
| 200 | |||
| 201 | EmacsNative.safPostRequest (); | ||
| 202 | } | ||
| 203 | }); | ||
| 204 | |||
| 205 | if (EmacsNative.safSyncAndReadInput () != 0) | ||
| 206 | { | ||
| 207 | signal.cancel (); | ||
| 208 | |||
| 209 | /* Now wait for the function to finish. Either the signal has | ||
| 210 | arrived after the query took place, in which case it will | ||
| 211 | finish normally, or an OperationCanceledException will be | ||
| 212 | thrown. */ | ||
| 213 | |||
| 214 | EmacsNative.safSync (); | ||
| 215 | } | ||
| 216 | |||
| 217 | if (result.thing instanceof Throwable) | ||
| 218 | { | ||
| 219 | throwable = (Throwable) result.thing; | ||
| 220 | EmacsSafThread.<RuntimeException>throwException (throwable); | ||
| 221 | } | ||
| 222 | |||
| 223 | return (Integer) result.thing; | ||
| 224 | } | ||
| 225 | |||
| 226 | /* Run the given function (or rather, its `runObject' field) within | ||
| 227 | the SAF thread, waiting for it to complete. | ||
| 228 | |||
| 229 | If async input arrives in the meantime and sets Vquit_flag, | ||
| 230 | signal the cancellation signal supplied to that function. | ||
| 231 | |||
| 232 | Rethrow any exception thrown from that function, and return its | ||
| 233 | value otherwise. */ | ||
| 234 | |||
| 235 | private Object | ||
| 236 | runObjectFunction (final SafObjectFunction function) | ||
| 237 | { | ||
| 238 | final EmacsHolder<Object> result; | ||
| 239 | final CancellationSignal signal; | ||
| 240 | Throwable throwable; | ||
| 241 | |||
| 242 | result = new EmacsHolder<Object> (); | ||
| 243 | signal = new CancellationSignal (); | ||
| 244 | |||
| 245 | handler.post (new Runnable () { | ||
| 246 | @Override | ||
| 247 | public void | ||
| 248 | run () | ||
| 249 | { | ||
| 250 | try | ||
| 251 | { | ||
| 252 | result.thing = function.runObject (signal); | ||
| 253 | } | ||
| 254 | catch (Throwable throwable) | ||
| 255 | { | ||
| 256 | result.thing = throwable; | ||
| 257 | } | ||
| 258 | |||
| 259 | EmacsNative.safPostRequest (); | ||
| 260 | } | ||
| 261 | }); | ||
| 262 | |||
| 263 | if (EmacsNative.safSyncAndReadInput () != 0) | ||
| 264 | { | ||
| 265 | signal.cancel (); | ||
| 266 | |||
| 267 | /* Now wait for the function to finish. Either the signal has | ||
| 268 | arrived after the query took place, in which case it will | ||
| 269 | finish normally, or an OperationCanceledException will be | ||
| 270 | thrown. */ | ||
| 271 | |||
| 272 | EmacsNative.safSync (); | ||
| 273 | } | ||
| 274 | |||
| 275 | if (result.thing instanceof Throwable) | ||
| 276 | { | ||
| 277 | throwable = (Throwable) result.thing; | ||
| 278 | EmacsSafThread.<RuntimeException>throwException (throwable); | ||
| 279 | } | ||
| 280 | |||
| 281 | return result.thing; | ||
| 282 | } | ||
| 283 | |||
| 284 | /* The crux of `documentIdFromName1', run within the SAF thread. | ||
| 285 | SIGNAL should be a cancellation signal run upon quitting. */ | ||
| 286 | |||
| 287 | private int | ||
| 288 | documentIdFromName1 (String tree_uri, String name, | ||
| 289 | String[] id_return, CancellationSignal signal) | ||
| 290 | { | ||
| 291 | Uri uri, treeUri; | ||
| 292 | String id, type; | ||
| 293 | String[] components, projection; | ||
| 294 | Cursor cursor; | ||
| 295 | int column; | ||
| 296 | |||
| 297 | projection = new String[] { | ||
| 298 | Document.COLUMN_DISPLAY_NAME, | ||
| 299 | Document.COLUMN_DOCUMENT_ID, | ||
| 300 | Document.COLUMN_MIME_TYPE, | ||
| 301 | }; | ||
| 302 | |||
| 303 | /* Parse the URI identifying the tree first. */ | ||
| 304 | uri = Uri.parse (tree_uri); | ||
| 305 | |||
| 306 | /* Now, split NAME into its individual components. */ | ||
| 307 | components = name.split ("/"); | ||
| 308 | |||
| 309 | /* Set id and type to the value at the root of the tree. */ | ||
| 310 | type = id = null; | ||
| 311 | cursor = null; | ||
| 312 | |||
| 313 | /* For each component... */ | ||
| 314 | |||
| 315 | try | ||
| 316 | { | ||
| 317 | for (String component : components) | ||
| 318 | { | ||
| 319 | /* Java split doesn't behave very much like strtok when it | ||
| 320 | comes to trailing and leading delimiters... */ | ||
| 321 | if (component.isEmpty ()) | ||
| 322 | continue; | ||
| 323 | |||
| 324 | /* Create the tree URI for URI from ID if it exists, or | ||
| 325 | the root otherwise. */ | ||
| 326 | |||
| 327 | if (id == null) | ||
| 328 | id = DocumentsContract.getTreeDocumentId (uri); | ||
| 329 | |||
| 330 | treeUri | ||
| 331 | = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); | ||
| 332 | |||
| 333 | /* Look for a file in this directory by the name of | ||
| 334 | component. */ | ||
| 335 | |||
| 336 | cursor = resolver.query (treeUri, projection, | ||
| 337 | (Document.COLUMN_DISPLAY_NAME | ||
| 338 | + " = ?s"), | ||
| 339 | new String[] { component, }, | ||
| 340 | null, signal); | ||
| 341 | |||
| 342 | if (cursor == null) | ||
| 343 | return -1; | ||
| 344 | |||
| 345 | while (true) | ||
| 346 | { | ||
| 347 | /* Even though the query selects for a specific | ||
| 348 | display name, some content providers nevertheless | ||
| 349 | return every file within the directory. */ | ||
| 350 | |||
| 351 | if (!cursor.moveToNext ()) | ||
| 352 | { | ||
| 353 | /* If the last component considered is a | ||
| 354 | directory... */ | ||
| 355 | if ((type == null | ||
| 356 | || type.equals (Document.MIME_TYPE_DIR)) | ||
| 357 | /* ... and type and id currently represent the | ||
| 358 | penultimate component. */ | ||
| 359 | && component == components[components.length - 1]) | ||
| 360 | { | ||
| 361 | /* The cursor is empty. In this case, return | ||
| 362 | -2 and the current document ID (belonging | ||
| 363 | to the previous component) in | ||
| 364 | ID_RETURN. */ | ||
| 365 | |||
| 366 | id_return[0] = id; | ||
| 367 | |||
| 368 | /* But return -1 on the off chance that id is | ||
| 369 | null. */ | ||
| 370 | |||
| 371 | if (id == null) | ||
| 372 | return -1; | ||
| 373 | |||
| 374 | return -2; | ||
| 375 | } | ||
| 376 | |||
| 377 | /* The last component found is not a directory, so | ||
| 378 | return -1. */ | ||
| 379 | return -1; | ||
| 380 | } | ||
| 381 | |||
| 382 | /* So move CURSOR to a row with the right display | ||
| 383 | name. */ | ||
| 384 | |||
| 385 | column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 386 | |||
| 387 | if (column < 0) | ||
| 388 | continue; | ||
| 389 | |||
| 390 | name = cursor.getString (column); | ||
| 391 | |||
| 392 | /* Break out of the loop only once a matching | ||
| 393 | component is found. */ | ||
| 394 | |||
| 395 | if (name.equals (component)) | ||
| 396 | break; | ||
| 397 | } | ||
| 398 | |||
| 399 | /* Look for a column by the name of | ||
| 400 | COLUMN_DOCUMENT_ID. */ | ||
| 401 | |||
| 402 | column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 403 | |||
| 404 | if (column < 0) | ||
| 405 | return -1; | ||
| 406 | |||
| 407 | /* Now replace ID with the document ID. */ | ||
| 408 | |||
| 409 | id = cursor.getString (column); | ||
| 410 | |||
| 411 | /* If this is the last component, be sure to initialize | ||
| 412 | the document type. */ | ||
| 413 | |||
| 414 | if (component == components[components.length - 1]) | ||
| 415 | { | ||
| 416 | column | ||
| 417 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 418 | |||
| 419 | if (column < 0) | ||
| 420 | return -1; | ||
| 421 | |||
| 422 | type = cursor.getString (column); | ||
| 423 | |||
| 424 | /* Type may be NULL depending on how the Cursor | ||
| 425 | returned is implemented. */ | ||
| 426 | |||
| 427 | if (type == null) | ||
| 428 | return -1; | ||
| 429 | } | ||
| 430 | |||
| 431 | /* Now close the cursor. */ | ||
| 432 | cursor.close (); | ||
| 433 | cursor = null; | ||
| 434 | |||
| 435 | /* ID may have become NULL if the data is in an invalid | ||
| 436 | format. */ | ||
| 437 | if (id == null) | ||
| 438 | return -1; | ||
| 439 | } | ||
| 440 | } | ||
| 441 | finally | ||
| 442 | { | ||
| 443 | /* If an error is thrown within the block above, let | ||
| 444 | android_saf_exception_check handle it, but make sure the | ||
| 445 | cursor is closed. */ | ||
| 446 | |||
| 447 | if (cursor != null) | ||
| 448 | cursor.close (); | ||
| 449 | } | ||
| 450 | |||
| 451 | /* Here, id is either NULL (meaning the same as TREE_URI), and | ||
| 452 | type is either NULL (in which case id should also be NULL) or | ||
| 453 | the MIME type of the file. */ | ||
| 454 | |||
| 455 | /* First return the ID. */ | ||
| 456 | |||
| 457 | if (id == null) | ||
| 458 | id_return[0] = DocumentsContract.getTreeDocumentId (uri); | ||
| 459 | else | ||
| 460 | id_return[0] = id; | ||
| 461 | |||
| 462 | /* Next, return whether or not this is a directory. */ | ||
| 463 | if (type == null || type.equals (Document.MIME_TYPE_DIR)) | ||
| 464 | return 1; | ||
| 465 | |||
| 466 | return 0; | ||
| 467 | } | ||
| 468 | |||
| 469 | /* Find the document ID of the file within TREE_URI designated by | ||
| 470 | NAME. | ||
| 471 | |||
| 472 | NAME is a ``file name'' comprised of the display names of | ||
| 473 | individual files. Each constituent component prior to the last | ||
| 474 | must name a directory file within TREE_URI. | ||
| 475 | |||
| 476 | Upon success, return 0 or 1 (contingent upon whether or not the | ||
| 477 | last component within NAME is a directory) and place the document | ||
| 478 | ID of the named file in ID_RETURN[0]. | ||
| 479 | |||
| 480 | If the designated file can't be located, but each component of | ||
| 481 | NAME up to the last component can and is a directory, return -2 | ||
| 482 | and the ID of the last component located in ID_RETURN[0]. | ||
| 483 | |||
| 484 | If the designated file can't be located, return -1, or signal one | ||
| 485 | of OperationCanceledException, SecurityException, | ||
| 486 | FileNotFoundException, or UnsupportedOperationException. */ | ||
| 487 | |||
| 488 | public int | ||
| 489 | documentIdFromName (final String tree_uri, final String name, | ||
| 490 | final String[] id_return) | ||
| 491 | { | ||
| 492 | return runIntFunction (new SafIntFunction () { | ||
| 493 | @Override | ||
| 494 | public int | ||
| 495 | runInt (CancellationSignal signal) | ||
| 496 | { | ||
| 497 | return documentIdFromName1 (tree_uri, name, id_return, | ||
| 498 | signal); | ||
| 499 | } | ||
| 500 | }); | ||
| 501 | } | ||
| 502 | |||
| 503 | /* The bulk of `statDocument'. SIGNAL should be a cancelation | ||
| 504 | signal. */ | ||
| 505 | |||
| 506 | private long[] | ||
| 507 | statDocument1 (String uri, String documentId, | ||
| 508 | CancellationSignal signal) | ||
| 509 | { | ||
| 510 | Uri uriObject; | ||
| 511 | String[] projection; | ||
| 512 | long[] stat; | ||
| 513 | int index; | ||
| 514 | long tem; | ||
| 515 | String tem1; | ||
| 516 | Cursor cursor; | ||
| 517 | |||
| 518 | uriObject = Uri.parse (uri); | ||
| 519 | |||
| 520 | if (documentId == null) | ||
| 521 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 522 | |||
| 523 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 524 | authority. */ | ||
| 525 | |||
| 526 | uriObject | ||
| 527 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 528 | |||
| 529 | /* Now stat this document. */ | ||
| 530 | |||
| 531 | projection = new String[] { | ||
| 532 | Document.COLUMN_FLAGS, | ||
| 533 | Document.COLUMN_LAST_MODIFIED, | ||
| 534 | Document.COLUMN_MIME_TYPE, | ||
| 535 | Document.COLUMN_SIZE, | ||
| 536 | }; | ||
| 537 | |||
| 538 | cursor = resolver.query (uriObject, projection, null, | ||
| 539 | null, null, signal); | ||
| 540 | |||
| 541 | if (cursor == null) | ||
| 542 | return null; | ||
| 543 | |||
| 544 | if (!cursor.moveToFirst ()) | ||
| 545 | { | ||
| 546 | cursor.close (); | ||
| 547 | return null; | ||
| 548 | } | ||
| 549 | |||
| 550 | /* Create the array of file status. */ | ||
| 551 | stat = new long[3]; | ||
| 552 | |||
| 553 | try | ||
| 554 | { | ||
| 555 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 556 | if (index < 0) | ||
| 557 | return null; | ||
| 558 | |||
| 559 | tem = cursor.getInt (index); | ||
| 560 | |||
| 561 | stat[0] |= S_IRUSR; | ||
| 562 | if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) | ||
| 563 | stat[0] |= S_IWUSR; | ||
| 564 | |||
| 565 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N | ||
| 566 | && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) | ||
| 567 | stat[0] |= S_IFCHR; | ||
| 568 | |||
| 569 | index = cursor.getColumnIndex (Document.COLUMN_SIZE); | ||
| 570 | if (index < 0) | ||
| 571 | return null; | ||
| 572 | |||
| 573 | if (cursor.isNull (index)) | ||
| 574 | stat[1] = -1; /* The size is unknown. */ | ||
| 575 | else | ||
| 576 | stat[1] = cursor.getLong (index); | ||
| 577 | |||
| 578 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 579 | if (index < 0) | ||
| 580 | return null; | ||
| 581 | |||
| 582 | tem1 = cursor.getString (index); | ||
| 583 | |||
| 584 | /* Check if this is a directory file. */ | ||
| 585 | if (tem1.equals (Document.MIME_TYPE_DIR) | ||
| 586 | /* Files shouldn't be specials and directories at the same | ||
| 587 | time, but Android doesn't forbid document providers | ||
| 588 | from returning this information. */ | ||
| 589 | && (stat[0] & S_IFCHR) == 0) | ||
| 590 | /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, | ||
| 591 | just assume they're writable. */ | ||
| 592 | stat[0] |= S_IFDIR | S_IWUSR; | ||
| 593 | |||
| 594 | /* If this file is neither a character special nor a | ||
| 595 | directory, indicate that it's a regular file. */ | ||
| 596 | |||
| 597 | if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) | ||
| 598 | stat[0] |= S_IFREG; | ||
| 599 | |||
| 600 | index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); | ||
| 601 | |||
| 602 | if (index >= 0 && !cursor.isNull (index)) | ||
| 603 | { | ||
| 604 | /* Content providers are allowed to not provide mtime. */ | ||
| 605 | tem = cursor.getLong (index); | ||
| 606 | stat[2] = tem; | ||
| 607 | } | ||
| 608 | } | ||
| 609 | finally | ||
| 610 | { | ||
| 611 | cursor.close (); | ||
| 612 | } | ||
| 613 | |||
| 614 | return stat; | ||
| 615 | } | ||
| 616 | |||
| 617 | /* Return file status for the document designated by the given | ||
| 618 | DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document | ||
| 619 | ID in URI itself. | ||
| 620 | |||
| 621 | Value is null upon failure, or an array of longs [MODE, SIZE, | ||
| 622 | MTIM] upon success, where MODE contains the file type and access | ||
| 623 | modes of the file as in `struct stat', SIZE is the size of the | ||
| 624 | file in BYTES or -1 if not known, and MTIM is the time of the | ||
| 625 | last modification to this file in milliseconds since 00:00, | ||
| 626 | January 1st, 1970. | ||
| 627 | |||
| 628 | OperationCanceledException and other typical exceptions may be | ||
| 629 | signaled upon receiving async input or other errors. */ | ||
| 630 | |||
| 631 | public long[] | ||
| 632 | statDocument (final String uri, final String documentId) | ||
| 633 | { | ||
| 634 | return (long[]) runObjectFunction (new SafObjectFunction () { | ||
| 635 | @Override | ||
| 636 | public Object | ||
| 637 | runObject (CancellationSignal signal) | ||
| 638 | { | ||
| 639 | return statDocument1 (uri, documentId, signal); | ||
| 640 | } | ||
| 641 | }); | ||
| 642 | } | ||
| 643 | |||
| 644 | /* The bulk of `accessDocument'. SIGNAL should be a cancellation | ||
| 645 | signal. */ | ||
| 646 | |||
| 647 | private int | ||
| 648 | accessDocument1 (String uri, String documentId, boolean writable, | ||
| 649 | CancellationSignal signal) | ||
| 650 | { | ||
| 651 | Uri uriObject; | ||
| 652 | String[] projection; | ||
| 653 | int tem, index; | ||
| 654 | String tem1; | ||
| 655 | Cursor cursor; | ||
| 656 | |||
| 657 | uriObject = Uri.parse (uri); | ||
| 658 | |||
| 659 | if (documentId == null) | ||
| 660 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 661 | |||
| 662 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 663 | authority. */ | ||
| 664 | |||
| 665 | uriObject | ||
| 666 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 667 | |||
| 668 | /* Now stat this document. */ | ||
| 669 | |||
| 670 | projection = new String[] { | ||
| 671 | Document.COLUMN_FLAGS, | ||
| 672 | Document.COLUMN_MIME_TYPE, | ||
| 673 | }; | ||
| 674 | |||
| 675 | cursor = resolver.query (uriObject, projection, null, | ||
| 676 | null, null, signal); | ||
| 677 | |||
| 678 | if (cursor == null) | ||
| 679 | return -1; | ||
| 680 | |||
| 681 | try | ||
| 682 | { | ||
| 683 | if (!cursor.moveToFirst ()) | ||
| 684 | return -1; | ||
| 685 | |||
| 686 | if (!writable) | ||
| 687 | return 0; | ||
| 688 | |||
| 689 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 690 | if (index < 0) | ||
| 691 | return -3; | ||
| 692 | |||
| 693 | /* Get the type of this file to check if it's a directory. */ | ||
| 694 | tem1 = cursor.getString (index); | ||
| 695 | |||
| 696 | /* Check if this is a directory file. */ | ||
| 697 | if (tem1.equals (Document.MIME_TYPE_DIR)) | ||
| 698 | { | ||
| 699 | /* If so, don't check for FLAG_SUPPORTS_WRITE. | ||
| 700 | Check for FLAG_DIR_SUPPORTS_CREATE instead. */ | ||
| 701 | |||
| 702 | if (!writable) | ||
| 703 | return 0; | ||
| 704 | |||
| 705 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 706 | if (index < 0) | ||
| 707 | return -3; | ||
| 708 | |||
| 709 | tem = cursor.getInt (index); | ||
| 710 | if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) | ||
| 711 | return -3; | ||
| 712 | |||
| 713 | return 0; | ||
| 714 | } | ||
| 715 | |||
| 716 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 717 | if (index < 0) | ||
| 718 | return -3; | ||
| 719 | |||
| 720 | tem = cursor.getInt (index); | ||
| 721 | if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) | ||
| 722 | return -3; | ||
| 723 | } | ||
| 724 | finally | ||
| 725 | { | ||
| 726 | /* Close the cursor if an exception occurs. */ | ||
| 727 | cursor.close (); | ||
| 728 | } | ||
| 729 | |||
| 730 | return 0; | ||
| 731 | } | ||
| 732 | |||
| 733 | /* Find out whether Emacs has access to the document designated by | ||
| 734 | the specified DOCUMENTID within the tree URI. If DOCUMENTID is | ||
| 735 | NULL, use the document ID in URI itself. | ||
| 736 | |||
| 737 | If WRITABLE, also check that the file is writable, which is true | ||
| 738 | if it is either a directory or its flags contains | ||
| 739 | FLAG_SUPPORTS_WRITE. | ||
| 740 | |||
| 741 | Value is 0 if the file is accessible, and one of the following if | ||
| 742 | not: | ||
| 743 | |||
| 744 | -1, if the file does not exist. | ||
| 745 | -2, if WRITABLE and the file is not writable. | ||
| 746 | -3, upon any other error. | ||
| 747 | |||
| 748 | In addition, arbitrary runtime exceptions (such as | ||
| 749 | SecurityException or UnsupportedOperationException) may be | ||
| 750 | thrown. */ | ||
| 751 | |||
| 752 | public int | ||
| 753 | accessDocument (final String uri, final String documentId, | ||
| 754 | final boolean writable) | ||
| 755 | { | ||
| 756 | return runIntFunction (new SafIntFunction () { | ||
| 757 | @Override | ||
| 758 | public int | ||
| 759 | runInt (CancellationSignal signal) | ||
| 760 | { | ||
| 761 | return accessDocument1 (uri, documentId, writable, | ||
| 762 | signal); | ||
| 763 | } | ||
| 764 | }); | ||
| 765 | } | ||
| 766 | |||
| 767 | /* The crux of openDocumentDirectory. SIGNAL must be a cancellation | ||
| 768 | signal. */ | ||
| 769 | |||
| 770 | private Cursor | ||
| 771 | openDocumentDirectory1 (String uri, String documentId, | ||
| 772 | CancellationSignal signal) | ||
| 773 | { | ||
| 774 | Uri uriObject; | ||
| 775 | Cursor cursor; | ||
| 776 | String projection[]; | ||
| 777 | |||
| 778 | uriObject = Uri.parse (uri); | ||
| 779 | |||
| 780 | /* If documentId is not set, use the document ID of the tree URI | ||
| 781 | itself. */ | ||
| 782 | |||
| 783 | if (documentId == null) | ||
| 784 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 785 | |||
| 786 | /* Build a URI representing each directory entry within | ||
| 787 | DOCUMENTID. */ | ||
| 788 | |||
| 789 | uriObject | ||
| 790 | = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, | ||
| 791 | documentId); | ||
| 792 | |||
| 793 | projection = new String [] { | ||
| 794 | Document.COLUMN_DISPLAY_NAME, | ||
| 795 | Document.COLUMN_MIME_TYPE, | ||
| 796 | }; | ||
| 797 | |||
| 798 | cursor = resolver.query (uriObject, projection, null, null, | ||
| 799 | null, signal); | ||
| 800 | /* Return the cursor. */ | ||
| 801 | return cursor; | ||
| 802 | } | ||
| 803 | |||
| 804 | /* Open a cursor representing each entry within the directory | ||
| 805 | designated by the specified DOCUMENTID within the tree URI. | ||
| 806 | |||
| 807 | If DOCUMENTID is NULL, use the document ID within URI itself. | ||
| 808 | Value is NULL upon failure. | ||
| 809 | |||
| 810 | In addition, arbitrary runtime exceptions (such as | ||
| 811 | SecurityException or UnsupportedOperationException) may be | ||
| 812 | thrown. */ | ||
| 813 | |||
| 814 | public Cursor | ||
| 815 | openDocumentDirectory (final String uri, final String documentId) | ||
| 816 | { | ||
| 817 | return (Cursor) runObjectFunction (new SafObjectFunction () { | ||
| 818 | @Override | ||
| 819 | public Object | ||
| 820 | runObject (CancellationSignal signal) | ||
| 821 | { | ||
| 822 | return openDocumentDirectory1 (uri, documentId, signal); | ||
| 823 | } | ||
| 824 | }); | ||
| 825 | } | ||
| 826 | |||
| 827 | /* The crux of `openDocument'. SIGNAL must be a cancellation | ||
| 828 | signal. */ | ||
| 829 | |||
| 830 | public ParcelFileDescriptor | ||
| 831 | openDocument1 (String uri, String documentId, boolean write, | ||
| 832 | boolean truncate, CancellationSignal signal) | ||
| 833 | throws Throwable | ||
| 834 | { | ||
| 835 | Uri treeUri, documentUri; | ||
| 836 | String mode; | ||
| 837 | ParcelFileDescriptor fileDescriptor; | ||
| 838 | |||
| 839 | treeUri = Uri.parse (uri); | ||
| 840 | |||
| 841 | /* documentId must be set for this request, since it doesn't make | ||
| 842 | sense to ``open'' the root of the directory tree. */ | ||
| 843 | |||
| 844 | documentUri | ||
| 845 | = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); | ||
| 846 | |||
| 847 | if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) | ||
| 848 | { | ||
| 849 | /* Select the mode used to open the file. `rw' means open | ||
| 850 | a stat-able file, while `rwt' means that and to | ||
| 851 | truncate the file as well. */ | ||
| 852 | |||
| 853 | if (truncate) | ||
| 854 | mode = "rwt"; | ||
| 855 | else | ||
| 856 | mode = "rw"; | ||
| 857 | |||
| 858 | fileDescriptor | ||
| 859 | = resolver.openFileDescriptor (documentUri, mode, | ||
| 860 | signal); | ||
| 861 | } | ||
| 862 | else | ||
| 863 | { | ||
| 864 | /* Select the mode used to open the file. `openFile' | ||
| 865 | below means always open a stat-able file. */ | ||
| 866 | |||
| 867 | if (truncate) | ||
| 868 | /* Invalid mode! */ | ||
| 869 | return null; | ||
| 870 | else | ||
| 871 | mode = "r"; | ||
| 872 | |||
| 873 | fileDescriptor = resolver.openFile (documentUri, mode, | ||
| 874 | signal); | ||
| 875 | } | ||
| 876 | |||
| 877 | return fileDescriptor; | ||
| 878 | } | ||
| 879 | |||
| 880 | /* Open a file descriptor for a file document designated by | ||
| 881 | DOCUMENTID within the document tree identified by URI. If | ||
| 882 | TRUNCATE and the document already exists, truncate its contents | ||
| 883 | before returning. | ||
| 884 | |||
| 885 | On Android 9.0 and earlier, always open the document in | ||
| 886 | ``read-write'' mode; this instructs the document provider to | ||
| 887 | return a seekable file that is stored on disk and returns correct | ||
| 888 | file status. | ||
| 889 | |||
| 890 | Under newer versions of Android, open the document in a | ||
| 891 | non-writable mode if WRITE is false. This is possible because | ||
| 892 | these versions allow Emacs to explicitly request a seekable | ||
| 893 | on-disk file. | ||
| 894 | |||
| 895 | Value is NULL upon failure or a parcel file descriptor upon | ||
| 896 | success. Call `ParcelFileDescriptor.close' on this file | ||
| 897 | descriptor instead of using the `close' system call. | ||
| 898 | |||
| 899 | FileNotFoundException and/or SecurityException and/or | ||
| 900 | UnsupportedOperationException and/or OperationCanceledException | ||
| 901 | may be thrown upon failure. */ | ||
| 902 | |||
| 903 | public ParcelFileDescriptor | ||
| 904 | openDocument (final String uri, final String documentId, | ||
| 905 | final boolean write, final boolean truncate) | ||
| 906 | { | ||
| 907 | Object tem; | ||
| 908 | |||
| 909 | tem = runObjectFunction (new SafObjectFunction () { | ||
| 910 | @Override | ||
| 911 | public Object | ||
| 912 | runObject (CancellationSignal signal) | ||
| 913 | throws Throwable | ||
| 914 | { | ||
| 915 | return openDocument1 (uri, documentId, write, truncate, | ||
| 916 | signal); | ||
| 917 | } | ||
| 918 | }); | ||
| 919 | |||
| 920 | return (ParcelFileDescriptor) tem; | ||
| 921 | } | ||
| 922 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index aa672994f12..e410754071b 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java | |||
| @@ -109,13 +109,6 @@ public final class EmacsService extends Service | |||
| 109 | public static final int IC_MODE_ACTION = 1; | 109 | public static final int IC_MODE_ACTION = 1; |
| 110 | public static final int IC_MODE_TEXT = 2; | 110 | public static final int IC_MODE_TEXT = 2; |
| 111 | 111 | ||
| 112 | /* File access mode constants. See `man 7 inode'. */ | ||
| 113 | public static final int S_IRUSR = 0000400; | ||
| 114 | public static final int S_IWUSR = 0000200; | ||
| 115 | public static final int S_IFCHR = 0020000; | ||
| 116 | public static final int S_IFDIR = 0040000; | ||
| 117 | public static final int S_IFREG = 0100000; | ||
| 118 | |||
| 119 | /* Display metrics used by font backends. */ | 112 | /* Display metrics used by font backends. */ |
| 120 | public DisplayMetrics metrics; | 113 | public DisplayMetrics metrics; |
| 121 | 114 | ||
| @@ -134,6 +127,10 @@ public final class EmacsService extends Service | |||
| 134 | being called, and 2 if icBeginSynchronous was called. */ | 127 | being called, and 2 if icBeginSynchronous was called. */ |
| 135 | public static final AtomicInteger servicingQuery; | 128 | public static final AtomicInteger servicingQuery; |
| 136 | 129 | ||
| 130 | /* Thread used to query document providers, or null if it hasn't | ||
| 131 | been created yet. */ | ||
| 132 | private EmacsSafThread storageThread; | ||
| 133 | |||
| 137 | static | 134 | static |
| 138 | { | 135 | { |
| 139 | servicingQuery = new AtomicInteger (); | 136 | servicingQuery = new AtomicInteger (); |
| @@ -1160,10 +1157,7 @@ public final class EmacsService extends Service | |||
| 1160 | 1157 | ||
| 1161 | 1158 | ||
| 1162 | /* Document tree management functions. These functions shouldn't be | 1159 | /* Document tree management functions. These functions shouldn't be |
| 1163 | called before Android 5.0. | 1160 | called before Android 5.0. */ |
| 1164 | |||
| 1165 | TODO: a timeout, let alone quitting, has yet to be implemented | ||
| 1166 | for any of these functions. */ | ||
| 1167 | 1161 | ||
| 1168 | /* Return an array of each document authority providing at least one | 1162 | /* Return an array of each document authority providing at least one |
| 1169 | tree URI that Emacs holds the rights to persistently access. */ | 1163 | tree URI that Emacs holds the rights to persistently access. */ |
| @@ -1319,223 +1313,26 @@ public final class EmacsService extends Service | |||
| 1319 | 1313 | ||
| 1320 | If the designated file can't be located, but each component of | 1314 | If the designated file can't be located, but each component of |
| 1321 | NAME up to the last component can and is a directory, return -2 | 1315 | NAME up to the last component can and is a directory, return -2 |
| 1322 | and the ID of the last component located in ID_RETURN[0]; | 1316 | and the ID of the last component located in ID_RETURN[0]. |
| 1323 | 1317 | ||
| 1324 | If the designated file can't be located, return -1. */ | 1318 | If the designated file can't be located, return -1, or signal one |
| 1319 | of OperationCanceledException, SecurityException, | ||
| 1320 | FileNotFoundException, or UnsupportedOperationException. */ | ||
| 1325 | 1321 | ||
| 1326 | private int | 1322 | private int |
| 1327 | documentIdFromName (String tree_uri, String name, String[] id_return) | 1323 | documentIdFromName (String tree_uri, String name, String[] id_return) |
| 1328 | { | 1324 | { |
| 1329 | Uri uri, treeUri; | 1325 | /* Start the thread used to run SAF requests if it isn't already |
| 1330 | String id, type; | 1326 | running. */ |
| 1331 | String[] components, projection; | ||
| 1332 | Cursor cursor; | ||
| 1333 | int column; | ||
| 1334 | |||
| 1335 | projection = new String[] { | ||
| 1336 | Document.COLUMN_DISPLAY_NAME, | ||
| 1337 | Document.COLUMN_DOCUMENT_ID, | ||
| 1338 | Document.COLUMN_MIME_TYPE, | ||
| 1339 | }; | ||
| 1340 | |||
| 1341 | /* Parse the URI identifying the tree first. */ | ||
| 1342 | uri = Uri.parse (tree_uri); | ||
| 1343 | |||
| 1344 | /* Now, split NAME into its individual components. */ | ||
| 1345 | components = name.split ("/"); | ||
| 1346 | 1327 | ||
| 1347 | /* Set id and type to the value at the root of the tree. */ | 1328 | if (storageThread == null) |
| 1348 | type = id = null; | ||
| 1349 | |||
| 1350 | /* For each component... */ | ||
| 1351 | |||
| 1352 | for (String component : components) | ||
| 1353 | { | 1329 | { |
| 1354 | /* Java split doesn't behave very much like strtok when it | 1330 | storageThread = new EmacsSafThread (resolver); |
| 1355 | comes to trailing and leading delimiters... */ | 1331 | storageThread.start (); |
| 1356 | if (component.isEmpty ()) | ||
| 1357 | continue; | ||
| 1358 | |||
| 1359 | /* Create the tree URI for URI from ID if it exists, or the | ||
| 1360 | root otherwise. */ | ||
| 1361 | |||
| 1362 | if (id == null) | ||
| 1363 | id = DocumentsContract.getTreeDocumentId (uri); | ||
| 1364 | |||
| 1365 | treeUri | ||
| 1366 | = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); | ||
| 1367 | |||
| 1368 | /* Look for a file in this directory by the name of | ||
| 1369 | component. */ | ||
| 1370 | |||
| 1371 | try | ||
| 1372 | { | ||
| 1373 | cursor = resolver.query (treeUri, projection, | ||
| 1374 | (Document.COLUMN_DISPLAY_NAME | ||
| 1375 | + " = ?s"), | ||
| 1376 | new String[] { component, }, null); | ||
| 1377 | } | ||
| 1378 | catch (SecurityException exception) | ||
| 1379 | { | ||
| 1380 | /* A SecurityException can be thrown if Emacs doesn't have | ||
| 1381 | access to treeUri. */ | ||
| 1382 | return -1; | ||
| 1383 | } | ||
| 1384 | catch (Exception exception) | ||
| 1385 | { | ||
| 1386 | exception.printStackTrace (); | ||
| 1387 | |||
| 1388 | /* Why is this? */ | ||
| 1389 | return -1; | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | if (cursor == null) | ||
| 1393 | return -1; | ||
| 1394 | |||
| 1395 | while (true) | ||
| 1396 | { | ||
| 1397 | /* Even though the query selects for a specific display | ||
| 1398 | name, some content providers nevertheless return every | ||
| 1399 | file within the directory. */ | ||
| 1400 | |||
| 1401 | if (!cursor.moveToNext ()) | ||
| 1402 | { | ||
| 1403 | cursor.close (); | ||
| 1404 | |||
| 1405 | /* If the last component considered is a | ||
| 1406 | directory... */ | ||
| 1407 | if ((type == null | ||
| 1408 | || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1409 | /* ... and type and id currently represent the | ||
| 1410 | penultimate component. */ | ||
| 1411 | && component == components[components.length - 1]) | ||
| 1412 | { | ||
| 1413 | /* The cursor is empty. In this case, return -2 | ||
| 1414 | and the current document ID (belonging to the | ||
| 1415 | previous component) in ID_RETURN. */ | ||
| 1416 | |||
| 1417 | id_return[0] = id; | ||
| 1418 | |||
| 1419 | /* But return -1 on the off chance that id is | ||
| 1420 | null. */ | ||
| 1421 | |||
| 1422 | if (id == null) | ||
| 1423 | return -1; | ||
| 1424 | |||
| 1425 | return -2; | ||
| 1426 | } | ||
| 1427 | |||
| 1428 | /* The last component found is not a directory, so | ||
| 1429 | return -1. */ | ||
| 1430 | return -1; | ||
| 1431 | } | ||
| 1432 | |||
| 1433 | /* So move CURSOR to a row with the right display | ||
| 1434 | name. */ | ||
| 1435 | |||
| 1436 | column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 1437 | |||
| 1438 | if (column < 0) | ||
| 1439 | continue; | ||
| 1440 | |||
| 1441 | try | ||
| 1442 | { | ||
| 1443 | name = cursor.getString (column); | ||
| 1444 | } | ||
| 1445 | catch (Exception exception) | ||
| 1446 | { | ||
| 1447 | cursor.close (); | ||
| 1448 | return -1; | ||
| 1449 | } | ||
| 1450 | |||
| 1451 | /* Break out of the loop only once a matching component is | ||
| 1452 | found. */ | ||
| 1453 | |||
| 1454 | if (name.equals (component)) | ||
| 1455 | break; | ||
| 1456 | } | ||
| 1457 | |||
| 1458 | /* Look for a column by the name of COLUMN_DOCUMENT_ID. */ | ||
| 1459 | |||
| 1460 | column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 1461 | |||
| 1462 | if (column < 0) | ||
| 1463 | { | ||
| 1464 | cursor.close (); | ||
| 1465 | return -1; | ||
| 1466 | } | ||
| 1467 | |||
| 1468 | /* Now replace ID with the document ID. */ | ||
| 1469 | |||
| 1470 | try | ||
| 1471 | { | ||
| 1472 | id = cursor.getString (column); | ||
| 1473 | } | ||
| 1474 | catch (Exception exception) | ||
| 1475 | { | ||
| 1476 | cursor.close (); | ||
| 1477 | return -1; | ||
| 1478 | } | ||
| 1479 | |||
| 1480 | /* If this is the last component, be sure to initialize the | ||
| 1481 | document type. */ | ||
| 1482 | |||
| 1483 | if (component == components[components.length - 1]) | ||
| 1484 | { | ||
| 1485 | column | ||
| 1486 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1487 | |||
| 1488 | if (column < 0) | ||
| 1489 | { | ||
| 1490 | cursor.close (); | ||
| 1491 | return -1; | ||
| 1492 | } | ||
| 1493 | |||
| 1494 | try | ||
| 1495 | { | ||
| 1496 | type = cursor.getString (column); | ||
| 1497 | } | ||
| 1498 | catch (Exception exception) | ||
| 1499 | { | ||
| 1500 | cursor.close (); | ||
| 1501 | return -1; | ||
| 1502 | } | ||
| 1503 | |||
| 1504 | /* Type may be NULL depending on how the Cursor returned | ||
| 1505 | is implemented. */ | ||
| 1506 | |||
| 1507 | if (type == null) | ||
| 1508 | { | ||
| 1509 | cursor.close (); | ||
| 1510 | return -1; | ||
| 1511 | } | ||
| 1512 | } | ||
| 1513 | |||
| 1514 | /* Now close the cursor. */ | ||
| 1515 | cursor.close (); | ||
| 1516 | |||
| 1517 | /* ID may have become NULL if the data is in an invalid | ||
| 1518 | format. */ | ||
| 1519 | if (id == null) | ||
| 1520 | return -1; | ||
| 1521 | } | 1332 | } |
| 1522 | 1333 | ||
| 1523 | /* Here, id is either NULL (meaning the same as TREE_URI), and | 1334 | return storageThread.documentIdFromName (tree_uri, name, |
| 1524 | type is either NULL (in which case id should also be NULL) or | 1335 | id_return); |
| 1525 | the MIME type of the file. */ | ||
| 1526 | |||
| 1527 | /* First return the ID. */ | ||
| 1528 | |||
| 1529 | if (id == null) | ||
| 1530 | id_return[0] = DocumentsContract.getTreeDocumentId (uri); | ||
| 1531 | else | ||
| 1532 | id_return[0] = id; | ||
| 1533 | |||
| 1534 | /* Next, return whether or not this is a directory. */ | ||
| 1535 | if (type == null || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1536 | return 1; | ||
| 1537 | |||
| 1538 | return 0; | ||
| 1539 | } | 1336 | } |
| 1540 | 1337 | ||
| 1541 | /* Return an encoded document URI representing a tree with the | 1338 | /* Return an encoded document URI representing a tree with the |
| @@ -1585,130 +1382,24 @@ public final class EmacsService extends Service | |||
| 1585 | modes of the file as in `struct stat', SIZE is the size of the | 1382 | modes of the file as in `struct stat', SIZE is the size of the |
| 1586 | file in BYTES or -1 if not known, and MTIM is the time of the | 1383 | file in BYTES or -1 if not known, and MTIM is the time of the |
| 1587 | last modification to this file in milliseconds since 00:00, | 1384 | last modification to this file in milliseconds since 00:00, |
| 1588 | January 1st, 1970. */ | 1385 | January 1st, 1970. |
| 1386 | |||
| 1387 | OperationCanceledException and other typical exceptions may be | ||
| 1388 | signaled upon receiving async input or other errors. */ | ||
| 1589 | 1389 | ||
| 1590 | public long[] | 1390 | public long[] |
| 1591 | statDocument (String uri, String documentId) | 1391 | statDocument (String uri, String documentId) |
| 1592 | { | 1392 | { |
| 1593 | Uri uriObject; | 1393 | /* Start the thread used to run SAF requests if it isn't already |
| 1594 | String[] projection; | 1394 | running. */ |
| 1595 | long[] stat; | ||
| 1596 | int index; | ||
| 1597 | long tem; | ||
| 1598 | String tem1; | ||
| 1599 | Cursor cursor; | ||
| 1600 | |||
| 1601 | uriObject = Uri.parse (uri); | ||
| 1602 | |||
| 1603 | if (documentId == null) | ||
| 1604 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1605 | |||
| 1606 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 1607 | authority. */ | ||
| 1608 | |||
| 1609 | uriObject | ||
| 1610 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 1611 | |||
| 1612 | /* Now stat this document. */ | ||
| 1613 | |||
| 1614 | projection = new String[] { | ||
| 1615 | Document.COLUMN_FLAGS, | ||
| 1616 | Document.COLUMN_LAST_MODIFIED, | ||
| 1617 | Document.COLUMN_MIME_TYPE, | ||
| 1618 | Document.COLUMN_SIZE, | ||
| 1619 | }; | ||
| 1620 | |||
| 1621 | try | ||
| 1622 | { | ||
| 1623 | cursor = resolver.query (uriObject, projection, null, | ||
| 1624 | null, null); | ||
| 1625 | } | ||
| 1626 | catch (SecurityException exception) | ||
| 1627 | { | ||
| 1628 | /* A SecurityException can be thrown if Emacs doesn't have | ||
| 1629 | access to uriObject. */ | ||
| 1630 | return null; | ||
| 1631 | } | ||
| 1632 | catch (UnsupportedOperationException exception) | ||
| 1633 | { | ||
| 1634 | exception.printStackTrace (); | ||
| 1635 | |||
| 1636 | /* Why is this? */ | ||
| 1637 | return null; | ||
| 1638 | } | ||
| 1639 | |||
| 1640 | if (cursor == null || !cursor.moveToFirst ()) | ||
| 1641 | return null; | ||
| 1642 | |||
| 1643 | /* Create the array of file status. */ | ||
| 1644 | stat = new long[3]; | ||
| 1645 | 1395 | ||
| 1646 | try | 1396 | if (storageThread == null) |
| 1647 | { | 1397 | { |
| 1648 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | 1398 | storageThread = new EmacsSafThread (resolver); |
| 1649 | if (index < 0) | 1399 | storageThread.start (); |
| 1650 | return null; | ||
| 1651 | |||
| 1652 | tem = cursor.getInt (index); | ||
| 1653 | |||
| 1654 | stat[0] |= S_IRUSR; | ||
| 1655 | if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) | ||
| 1656 | stat[0] |= S_IWUSR; | ||
| 1657 | |||
| 1658 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N | ||
| 1659 | && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) | ||
| 1660 | stat[0] |= S_IFCHR; | ||
| 1661 | |||
| 1662 | index = cursor.getColumnIndex (Document.COLUMN_SIZE); | ||
| 1663 | if (index < 0) | ||
| 1664 | return null; | ||
| 1665 | |||
| 1666 | if (cursor.isNull (index)) | ||
| 1667 | stat[1] = -1; /* The size is unknown. */ | ||
| 1668 | else | ||
| 1669 | stat[1] = cursor.getLong (index); | ||
| 1670 | |||
| 1671 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1672 | if (index < 0) | ||
| 1673 | return null; | ||
| 1674 | |||
| 1675 | tem1 = cursor.getString (index); | ||
| 1676 | |||
| 1677 | /* Check if this is a directory file. */ | ||
| 1678 | if (tem1.equals (Document.MIME_TYPE_DIR) | ||
| 1679 | /* Files shouldn't be specials and directories at the same | ||
| 1680 | time, but Android doesn't forbid document providers | ||
| 1681 | from returning this information. */ | ||
| 1682 | && (stat[0] & S_IFCHR) == 0) | ||
| 1683 | /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, | ||
| 1684 | just assume they're writable. */ | ||
| 1685 | stat[0] |= S_IFDIR | S_IWUSR; | ||
| 1686 | |||
| 1687 | /* If this file is neither a character special nor a | ||
| 1688 | directory, indicate that it's a regular file. */ | ||
| 1689 | |||
| 1690 | if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) | ||
| 1691 | stat[0] |= S_IFREG; | ||
| 1692 | |||
| 1693 | index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); | ||
| 1694 | |||
| 1695 | if (index >= 0 && !cursor.isNull (index)) | ||
| 1696 | { | ||
| 1697 | /* Content providers are allowed to not provide mtime. */ | ||
| 1698 | tem = cursor.getLong (index); | ||
| 1699 | stat[2] = tem; | ||
| 1700 | } | ||
| 1701 | } | ||
| 1702 | catch (Exception exception) | ||
| 1703 | { | ||
| 1704 | /* Whether or not type errors cause exceptions to be signaled | ||
| 1705 | is defined ``by the implementation of Cursor'', whatever | ||
| 1706 | that means. */ | ||
| 1707 | exception.printStackTrace (); | ||
| 1708 | return null; | ||
| 1709 | } | 1400 | } |
| 1710 | 1401 | ||
| 1711 | return stat; | 1402 | return storageThread.statDocument (uri, documentId); |
| 1712 | } | 1403 | } |
| 1713 | 1404 | ||
| 1714 | /* Find out whether Emacs has access to the document designated by | 1405 | /* Find out whether Emacs has access to the document designated by |
| @@ -1733,83 +1424,16 @@ public final class EmacsService extends Service | |||
| 1733 | public int | 1424 | public int |
| 1734 | accessDocument (String uri, String documentId, boolean writable) | 1425 | accessDocument (String uri, String documentId, boolean writable) |
| 1735 | { | 1426 | { |
| 1736 | Uri uriObject; | 1427 | /* Start the thread used to run SAF requests if it isn't already |
| 1737 | String[] projection; | 1428 | running. */ |
| 1738 | int tem, index; | ||
| 1739 | String tem1; | ||
| 1740 | Cursor cursor; | ||
| 1741 | |||
| 1742 | uriObject = Uri.parse (uri); | ||
| 1743 | |||
| 1744 | if (documentId == null) | ||
| 1745 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1746 | |||
| 1747 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 1748 | authority. */ | ||
| 1749 | |||
| 1750 | uriObject | ||
| 1751 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 1752 | |||
| 1753 | /* Now stat this document. */ | ||
| 1754 | 1429 | ||
| 1755 | projection = new String[] { | 1430 | if (storageThread == null) |
| 1756 | Document.COLUMN_FLAGS, | ||
| 1757 | Document.COLUMN_MIME_TYPE, | ||
| 1758 | }; | ||
| 1759 | |||
| 1760 | cursor = resolver.query (uriObject, projection, null, | ||
| 1761 | null, null); | ||
| 1762 | |||
| 1763 | if (cursor == null || !cursor.moveToFirst ()) | ||
| 1764 | return -1; | ||
| 1765 | |||
| 1766 | if (!writable) | ||
| 1767 | return 0; | ||
| 1768 | |||
| 1769 | try | ||
| 1770 | { | 1431 | { |
| 1771 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | 1432 | storageThread = new EmacsSafThread (resolver); |
| 1772 | if (index < 0) | 1433 | storageThread.start (); |
| 1773 | return -3; | ||
| 1774 | |||
| 1775 | /* Get the type of this file to check if it's a directory. */ | ||
| 1776 | tem1 = cursor.getString (index); | ||
| 1777 | |||
| 1778 | /* Check if this is a directory file. */ | ||
| 1779 | if (tem1.equals (Document.MIME_TYPE_DIR)) | ||
| 1780 | { | ||
| 1781 | /* If so, don't check for FLAG_SUPPORTS_WRITE. | ||
| 1782 | Check for FLAG_DIR_SUPPORTS_CREATE instead. */ | ||
| 1783 | |||
| 1784 | if (!writable) | ||
| 1785 | return 0; | ||
| 1786 | |||
| 1787 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1788 | if (index < 0) | ||
| 1789 | return -3; | ||
| 1790 | |||
| 1791 | tem = cursor.getInt (index); | ||
| 1792 | if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) | ||
| 1793 | return -3; | ||
| 1794 | |||
| 1795 | return 0; | ||
| 1796 | } | ||
| 1797 | |||
| 1798 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1799 | if (index < 0) | ||
| 1800 | return -3; | ||
| 1801 | |||
| 1802 | tem = cursor.getInt (index); | ||
| 1803 | if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) | ||
| 1804 | return -3; | ||
| 1805 | } | ||
| 1806 | finally | ||
| 1807 | { | ||
| 1808 | /* Close the cursor if an exception occurs. */ | ||
| 1809 | cursor.close (); | ||
| 1810 | } | 1434 | } |
| 1811 | 1435 | ||
| 1812 | return 0; | 1436 | return storageThread.accessDocument (uri, documentId, writable); |
| 1813 | } | 1437 | } |
| 1814 | 1438 | ||
| 1815 | /* Open a cursor representing each entry within the directory | 1439 | /* Open a cursor representing each entry within the directory |
| @@ -1825,34 +1449,16 @@ public final class EmacsService extends Service | |||
| 1825 | public Cursor | 1449 | public Cursor |
| 1826 | openDocumentDirectory (String uri, String documentId) | 1450 | openDocumentDirectory (String uri, String documentId) |
| 1827 | { | 1451 | { |
| 1828 | Uri uriObject; | 1452 | /* Start the thread used to run SAF requests if it isn't already |
| 1829 | Cursor cursor; | 1453 | running. */ |
| 1830 | String projection[]; | ||
| 1831 | |||
| 1832 | uriObject = Uri.parse (uri); | ||
| 1833 | |||
| 1834 | /* If documentId is not set, use the document ID of the tree URI | ||
| 1835 | itself. */ | ||
| 1836 | |||
| 1837 | if (documentId == null) | ||
| 1838 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1839 | |||
| 1840 | /* Build a URI representing each directory entry within | ||
| 1841 | DOCUMENTID. */ | ||
| 1842 | |||
| 1843 | uriObject | ||
| 1844 | = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, | ||
| 1845 | documentId); | ||
| 1846 | 1454 | ||
| 1847 | projection = new String [] { | 1455 | if (storageThread == null) |
| 1848 | Document.COLUMN_DISPLAY_NAME, | 1456 | { |
| 1849 | Document.COLUMN_MIME_TYPE, | 1457 | storageThread = new EmacsSafThread (resolver); |
| 1850 | }; | 1458 | storageThread.start (); |
| 1459 | } | ||
| 1851 | 1460 | ||
| 1852 | cursor = resolver.query (uriObject, projection, null, null, | 1461 | return storageThread.openDocumentDirectory (uri, documentId); |
| 1853 | null); | ||
| 1854 | /* Return the cursor. */ | ||
| 1855 | return cursor; | ||
| 1856 | } | 1462 | } |
| 1857 | 1463 | ||
| 1858 | /* Read a single directory entry from the specified CURSOR. Return | 1464 | /* Read a single directory entry from the specified CURSOR. Return |
| @@ -1945,50 +1551,18 @@ public final class EmacsService extends Service | |||
| 1945 | public ParcelFileDescriptor | 1551 | public ParcelFileDescriptor |
| 1946 | openDocument (String uri, String documentId, boolean write, | 1552 | openDocument (String uri, String documentId, boolean write, |
| 1947 | boolean truncate) | 1553 | boolean truncate) |
| 1948 | throws FileNotFoundException | ||
| 1949 | { | 1554 | { |
| 1950 | Uri treeUri, documentUri; | 1555 | /* Start the thread used to run SAF requests if it isn't already |
| 1951 | String mode; | 1556 | running. */ |
| 1952 | ParcelFileDescriptor fileDescriptor; | ||
| 1953 | |||
| 1954 | treeUri = Uri.parse (uri); | ||
| 1955 | |||
| 1956 | /* documentId must be set for this request, since it doesn't make | ||
| 1957 | sense to ``open'' the root of the directory tree. */ | ||
| 1958 | 1557 | ||
| 1959 | documentUri | 1558 | if (storageThread == null) |
| 1960 | = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); | ||
| 1961 | |||
| 1962 | if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) | ||
| 1963 | { | 1559 | { |
| 1964 | /* Select the mode used to open the file. `rw' means open | 1560 | storageThread = new EmacsSafThread (resolver); |
| 1965 | a stat-able file, while `rwt' means that and to | 1561 | storageThread.start (); |
| 1966 | truncate the file as well. */ | ||
| 1967 | |||
| 1968 | if (truncate) | ||
| 1969 | mode = "rwt"; | ||
| 1970 | else | ||
| 1971 | mode = "rw"; | ||
| 1972 | |||
| 1973 | fileDescriptor | ||
| 1974 | = resolver.openFileDescriptor (documentUri, mode, | ||
| 1975 | null); | ||
| 1976 | } | ||
| 1977 | else | ||
| 1978 | { | ||
| 1979 | /* Select the mode used to open the file. `openFile' | ||
| 1980 | below means always open a stat-able file. */ | ||
| 1981 | |||
| 1982 | if (truncate) | ||
| 1983 | /* Invalid mode! */ | ||
| 1984 | return null; | ||
| 1985 | else | ||
| 1986 | mode = "r"; | ||
| 1987 | |||
| 1988 | fileDescriptor = resolver.openFile (documentUri, mode, null); | ||
| 1989 | } | 1562 | } |
| 1990 | 1563 | ||
| 1991 | return fileDescriptor; | 1564 | return storageThread.openDocument (uri, documentId, write, |
| 1565 | truncate); | ||
| 1992 | } | 1566 | } |
| 1993 | 1567 | ||
| 1994 | /* Create a new document with the given display NAME within the | 1568 | /* Create a new document with the given display NAME within the |