diff options
| author | Po Lu | 2023-07-29 11:24:07 +0800 |
|---|---|---|
| committer | Po Lu | 2023-07-29 11:24:07 +0800 |
| commit | 47f97b5ae49af49ef3adcad4d4d9cab3668002ff (patch) | |
| tree | 157ee1d853cbef310060a6640d1ddcfafdbb6e3f /java | |
| parent | 25ef0c3a89a92e8f5ea12afe43224c84632e89c6 (diff) | |
| download | emacs-47f97b5ae49af49ef3adcad4d4d9cab3668002ff.tar.gz emacs-47f97b5ae49af49ef3adcad4d4d9cab3668002ff.zip | |
Update Android port
* java/org/gnu/emacs/EmacsSafThread.java (EmacsSafThread, getCache)
(pruneCache1, pruneCache, cacheChild, cacheDirectoryFromCursor)
(documentIdFromName1, openDocumentDirectory1): Implement the
cache referred to by the commentary.
* java/org/gnu/emacs/EmacsService.java (deleteDocument):
Invalidate the cache upon document removal.
* src/androidvfs.c (android_saf_exception_check)
(android_document_id_from_name): Correctly preserve or set errno
in error cases.
Diffstat (limited to 'java')
| -rw-r--r-- | java/org/gnu/emacs/EmacsSafThread.java | 584 | ||||
| -rw-r--r-- | java/org/gnu/emacs/EmacsService.java | 14 |
2 files changed, 531 insertions, 67 deletions
diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index cbd4ff0d25b..b3d6ab49f6d 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java | |||
| @@ -1,24 +1,30 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- |
| 2 | 2 | ||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | 3 | Copyright (C) 2023 Free Software Foundation, Inc. |
| 4 | 4 | ||
| 5 | This file is part of GNU Emacs. | 5 | This file is part of GNU Emacs. |
| 6 | 6 | ||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | 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 | 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 | 9 | the Free Software Foundation, either version 3 of the License, or (at |
| 10 | your option) any later version. | 10 | your option) any later version. |
| 11 | 11 | ||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | 12 | GNU Emacs is distributed in the hope that it will be useful, |
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | GNU General Public License for more details. | 15 | GNU General Public License for more details. |
| 16 | 16 | ||
| 17 | You should have received a copy of the GNU General Public License | 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/>. */ | 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ |
| 19 | 19 | ||
| 20 | package org.gnu.emacs; | 20 | package org.gnu.emacs; |
| 21 | 21 | ||
| 22 | import java.util.Collection; | ||
| 23 | import java.util.HashMap; | ||
| 24 | import java.util.Iterator; | ||
| 25 | |||
| 26 | import java.io.FileNotFoundException; | ||
| 27 | |||
| 22 | import android.content.ContentResolver; | 28 | import android.content.ContentResolver; |
| 23 | import android.database.Cursor; | 29 | import android.database.Cursor; |
| 24 | import android.net.Uri; | 30 | import android.net.Uri; |
| @@ -29,6 +35,8 @@ import android.os.Handler; | |||
| 29 | import android.os.HandlerThread; | 35 | import android.os.HandlerThread; |
| 30 | import android.os.ParcelFileDescriptor; | 36 | import android.os.ParcelFileDescriptor; |
| 31 | 37 | ||
| 38 | import android.util.Log; | ||
| 39 | |||
| 32 | import android.provider.DocumentsContract; | 40 | import android.provider.DocumentsContract; |
| 33 | import android.provider.DocumentsContract.Document; | 41 | import android.provider.DocumentsContract.Document; |
| 34 | 42 | ||
| @@ -38,7 +46,6 @@ import android.provider.DocumentsContract.Document; | |||
| 38 | its own handler. These operations include opening files and | 46 | its own handler. These operations include opening files and |
| 39 | maintaining the path to document ID cache. | 47 | maintaining the path to document ID cache. |
| 40 | 48 | ||
| 41 | #if 0 | ||
| 42 | Because Emacs paths are based on file display names, while Android | 49 | Because Emacs paths are based on file display names, while Android |
| 43 | document identifiers have no discernible hierarchy of their own, | 50 | document identifiers have no discernible hierarchy of their own, |
| 44 | each file name lookup must carry out a repeated search for | 51 | each file name lookup must carry out a repeated search for |
| @@ -53,13 +60,10 @@ import android.provider.DocumentsContract.Document; | |||
| 53 | periodically remove entries that are older than a predetermined | 60 | periodically remove entries that are older than a predetermined |
| 54 | amount of a time. | 61 | amount of a time. |
| 55 | 62 | ||
| 56 | The cache is structured much like the directory trees whose | 63 | The cache is split into two levels: the first caches the |
| 57 | information it records, with each entry in the cache containing a | 64 | relationships between display names and document IDs, while the |
| 58 | list of entries for their children. File name lookup consults the | 65 | second caches individual document IDs and their contents (children, |
| 59 | cache and populates it with missing information simultaneously. | 66 | type, etc.) |
| 60 | |||
| 61 | This is not yet implemented. | ||
| 62 | #endif | ||
| 63 | 67 | ||
| 64 | Long-running operations are also run on this thread for another | 68 | Long-running operations are also run on this thread for another |
| 65 | reason: Android uses special cancellation objects to terminate | 69 | reason: Android uses special cancellation objects to terminate |
| @@ -76,9 +80,15 @@ import android.provider.DocumentsContract.Document; | |||
| 76 | 80 | ||
| 77 | public final class EmacsSafThread extends HandlerThread | 81 | public final class EmacsSafThread extends HandlerThread |
| 78 | { | 82 | { |
| 83 | private static final String TAG = "EmacsSafThread"; | ||
| 84 | |||
| 79 | /* The content resolver used by this thread. */ | 85 | /* The content resolver used by this thread. */ |
| 80 | private final ContentResolver resolver; | 86 | private final ContentResolver resolver; |
| 81 | 87 | ||
| 88 | /* Map between tree URIs and the cache entry representing its | ||
| 89 | toplevel directory. */ | ||
| 90 | private final HashMap<Uri, CacheToplevel> cacheToplevels; | ||
| 91 | |||
| 82 | /* Handler for this thread's main loop. */ | 92 | /* Handler for this thread's main loop. */ |
| 83 | private Handler handler; | 93 | private Handler handler; |
| 84 | 94 | ||
| @@ -89,11 +99,20 @@ public final class EmacsSafThread extends HandlerThread | |||
| 89 | public static final int S_IFDIR = 0040000; | 99 | public static final int S_IFDIR = 0040000; |
| 90 | public static final int S_IFREG = 0100000; | 100 | public static final int S_IFREG = 0100000; |
| 91 | 101 | ||
| 102 | /* Number of seconds in between each attempt to prune the storage | ||
| 103 | cache. */ | ||
| 104 | public static final int CACHE_PRUNE_TIME = 10; | ||
| 105 | |||
| 106 | /* Number of seconds after which an entry in the cache is to be | ||
| 107 | considered invalid. */ | ||
| 108 | public static final int CACHE_INVALID_TIME = 10; | ||
| 109 | |||
| 92 | public | 110 | public |
| 93 | EmacsSafThread (ContentResolver resolver) | 111 | EmacsSafThread (ContentResolver resolver) |
| 94 | { | 112 | { |
| 95 | super ("Document provider access thread"); | 113 | super ("Document provider access thread"); |
| 96 | this.resolver = resolver; | 114 | this.resolver = resolver; |
| 115 | this.cacheToplevels = new HashMap<Uri, CacheToplevel> (); | ||
| 97 | } | 116 | } |
| 98 | 117 | ||
| 99 | 118 | ||
| @@ -106,6 +125,376 @@ public final class EmacsSafThread extends HandlerThread | |||
| 106 | 125 | ||
| 107 | /* Set up the handler after the thread starts. */ | 126 | /* Set up the handler after the thread starts. */ |
| 108 | handler = new Handler (getLooper ()); | 127 | handler = new Handler (getLooper ()); |
| 128 | |||
| 129 | /* And start periodically pruning the cache. */ | ||
| 130 | postPruneMessage (); | ||
| 131 | } | ||
| 132 | |||
| 133 | |||
| 134 | private final class CacheToplevel | ||
| 135 | { | ||
| 136 | /* Map between document names and children. */ | ||
| 137 | HashMap<String, DocIdEntry> children; | ||
| 138 | |||
| 139 | /* Map between document IDs and cache items. */ | ||
| 140 | HashMap<String, CacheEntry> idCache; | ||
| 141 | }; | ||
| 142 | |||
| 143 | private final class DocIdEntry | ||
| 144 | { | ||
| 145 | /* The document ID. */ | ||
| 146 | String documentId; | ||
| 147 | |||
| 148 | /* The time this entry was created. */ | ||
| 149 | long time; | ||
| 150 | |||
| 151 | public | ||
| 152 | DocIdEntry () | ||
| 153 | { | ||
| 154 | time = System.currentTimeMillis (); | ||
| 155 | } | ||
| 156 | |||
| 157 | /* Return a cache entry comprised of the state of the file | ||
| 158 | identified by `documentId'. TREE is the URI of the tree | ||
| 159 | containing this entry, and TOPLEVEL is the toplevel | ||
| 160 | representing it. SIGNAL is a cancellation signal. | ||
| 161 | |||
| 162 | Value is NULL if the file cannot be found. */ | ||
| 163 | |||
| 164 | public CacheEntry | ||
| 165 | getCacheEntry (Uri tree, CacheToplevel toplevel, | ||
| 166 | CancellationSignal signal) | ||
| 167 | { | ||
| 168 | Uri uri; | ||
| 169 | String[] projection; | ||
| 170 | String type; | ||
| 171 | Cursor cursor; | ||
| 172 | int column; | ||
| 173 | CacheEntry entry; | ||
| 174 | |||
| 175 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 176 | authority. */ | ||
| 177 | |||
| 178 | uri = DocumentsContract.buildDocumentUriUsingTree (tree, | ||
| 179 | documentId); | ||
| 180 | projection = new String[] { | ||
| 181 | Document.COLUMN_MIME_TYPE, | ||
| 182 | }; | ||
| 183 | |||
| 184 | cursor = null; | ||
| 185 | |||
| 186 | try | ||
| 187 | { | ||
| 188 | cursor = resolver.query (uri, projection, null, | ||
| 189 | null, null, signal); | ||
| 190 | |||
| 191 | if (!cursor.moveToFirst ()) | ||
| 192 | return null; | ||
| 193 | |||
| 194 | column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 195 | |||
| 196 | if (column < 0) | ||
| 197 | return null; | ||
| 198 | |||
| 199 | type = cursor.getString (column); | ||
| 200 | |||
| 201 | if (type == null) | ||
| 202 | return null; | ||
| 203 | |||
| 204 | entry = new CacheEntry (); | ||
| 205 | entry.type = type; | ||
| 206 | toplevel.idCache.put (documentId, entry); | ||
| 207 | return entry; | ||
| 208 | } | ||
| 209 | catch (Throwable e) | ||
| 210 | { | ||
| 211 | if (e instanceof FileNotFoundException) | ||
| 212 | return null; | ||
| 213 | |||
| 214 | throw e; | ||
| 215 | } | ||
| 216 | finally | ||
| 217 | { | ||
| 218 | if (cursor != null) | ||
| 219 | cursor.close (); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | public boolean | ||
| 224 | isValid () | ||
| 225 | { | ||
| 226 | return ((System.currentTimeMillis () - time) | ||
| 227 | < CACHE_INVALID_TIME); | ||
| 228 | } | ||
| 229 | }; | ||
| 230 | |||
| 231 | private final class CacheEntry | ||
| 232 | { | ||
| 233 | /* The type of this document. */ | ||
| 234 | String type; | ||
| 235 | |||
| 236 | /* Map between document names and children. */ | ||
| 237 | HashMap<String, DocIdEntry> children; | ||
| 238 | |||
| 239 | /* The time this entry was created. */ | ||
| 240 | long time; | ||
| 241 | |||
| 242 | public | ||
| 243 | CacheEntry () | ||
| 244 | { | ||
| 245 | children = new HashMap<String, DocIdEntry> (); | ||
| 246 | time = System.currentTimeMillis (); | ||
| 247 | } | ||
| 248 | |||
| 249 | public boolean | ||
| 250 | isValid () | ||
| 251 | { | ||
| 252 | return ((System.currentTimeMillis () - time) | ||
| 253 | < CACHE_INVALID_TIME); | ||
| 254 | } | ||
| 255 | }; | ||
| 256 | |||
| 257 | /* Create or return a toplevel for the given tree URI. */ | ||
| 258 | |||
| 259 | private CacheToplevel | ||
| 260 | getCache (Uri uri) | ||
| 261 | { | ||
| 262 | CacheToplevel toplevel; | ||
| 263 | |||
| 264 | toplevel = cacheToplevels.get (uri); | ||
| 265 | |||
| 266 | if (toplevel != null) | ||
| 267 | return toplevel; | ||
| 268 | |||
| 269 | toplevel = new CacheToplevel (); | ||
| 270 | toplevel.children = new HashMap<String, DocIdEntry> (); | ||
| 271 | toplevel.idCache = new HashMap<String, CacheEntry> (); | ||
| 272 | cacheToplevels.put (uri, toplevel); | ||
| 273 | return toplevel; | ||
| 274 | } | ||
| 275 | |||
| 276 | /* Remove each cache entry within COLLECTION older than | ||
| 277 | CACHE_INVALID_TIME. */ | ||
| 278 | |||
| 279 | private void | ||
| 280 | pruneCache1 (Collection<DocIdEntry> collection) | ||
| 281 | { | ||
| 282 | Iterator<DocIdEntry> iter; | ||
| 283 | DocIdEntry tem; | ||
| 284 | |||
| 285 | iter = collection.iterator (); | ||
| 286 | while (iter.hasNext ()) | ||
| 287 | { | ||
| 288 | /* Get the cache entry. */ | ||
| 289 | tem = iter.next (); | ||
| 290 | |||
| 291 | /* If it's not valid anymore, remove it. Iterating over a | ||
| 292 | collection whose contents are being removed is undefined | ||
| 293 | unless the removal is performed using the iterator's own | ||
| 294 | `remove' function, so tem.remove cannot be used here. */ | ||
| 295 | |||
| 296 | if (tem.isValid ()) | ||
| 297 | continue; | ||
| 298 | |||
| 299 | iter.remove (); | ||
| 300 | } | ||
| 301 | } | ||
| 302 | |||
| 303 | /* Remove every entry older than CACHE_INVALID_TIME from each | ||
| 304 | toplevel inside `cachedToplevels'. */ | ||
| 305 | |||
| 306 | private void | ||
| 307 | pruneCache () | ||
| 308 | { | ||
| 309 | Iterator<CacheEntry> iter; | ||
| 310 | CacheEntry tem; | ||
| 311 | |||
| 312 | for (CacheToplevel toplevel : cacheToplevels.values ()) | ||
| 313 | { | ||
| 314 | /* First, clean up expired cache entries. */ | ||
| 315 | iter = toplevel.idCache.values ().iterator (); | ||
| 316 | |||
| 317 | while (iter.hasNext ()) | ||
| 318 | { | ||
| 319 | /* Get the cache entry. */ | ||
| 320 | tem = iter.next (); | ||
| 321 | |||
| 322 | /* If it's not valid anymore, remove it. Iterating over a | ||
| 323 | collection whose contents are being removed is | ||
| 324 | undefined unless the removal is performed using the | ||
| 325 | iterator's own `remove' function, so tem.remove cannot | ||
| 326 | be used here. */ | ||
| 327 | |||
| 328 | if (tem.isValid ()) | ||
| 329 | { | ||
| 330 | /* Otherwise, clean up expired items in its document | ||
| 331 | ID cache. */ | ||
| 332 | pruneCache1 (tem.children.values ()); | ||
| 333 | continue; | ||
| 334 | } | ||
| 335 | |||
| 336 | iter.remove (); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | postPruneMessage (); | ||
| 341 | } | ||
| 342 | |||
| 343 | /* Cache file information within TOPLEVEL, under the list of | ||
| 344 | children CHILDREN. | ||
| 345 | |||
| 346 | NAME, ID, and TYPE should respectively be the display name of the | ||
| 347 | document within its parent document (the CacheEntry whose | ||
| 348 | `children' field is CHILDREN), its document ID, and its MIME | ||
| 349 | type. | ||
| 350 | |||
| 351 | If ID_ENTRY_EXISTS, don't create a new document ID entry within | ||
| 352 | CHILDREN indexed by NAME. | ||
| 353 | |||
| 354 | Value is the cache entry saved for the document ID. */ | ||
| 355 | |||
| 356 | private CacheEntry | ||
| 357 | cacheChild (CacheToplevel toplevel, | ||
| 358 | HashMap<String, DocIdEntry> children, | ||
| 359 | String name, String id, String type, | ||
| 360 | boolean id_entry_exists) | ||
| 361 | { | ||
| 362 | DocIdEntry idEntry; | ||
| 363 | CacheEntry cacheEntry; | ||
| 364 | |||
| 365 | if (!id_entry_exists) | ||
| 366 | { | ||
| 367 | idEntry = new DocIdEntry (); | ||
| 368 | idEntry.documentId = id; | ||
| 369 | children.put (name, idEntry); | ||
| 370 | } | ||
| 371 | |||
| 372 | cacheEntry = new CacheEntry (); | ||
| 373 | cacheEntry.type = type; | ||
| 374 | toplevel.idCache.put (id, cacheEntry); | ||
| 375 | return cacheEntry; | ||
| 376 | } | ||
| 377 | |||
| 378 | /* Cache the type and as many of the children of the directory | ||
| 379 | designated by DOC_ID as possible into TOPLEVEL. | ||
| 380 | |||
| 381 | CURSOR should be a cursor representing an open directory stream, | ||
| 382 | with its projection consisting of at least the display name, | ||
| 383 | document ID and MIME type columns. | ||
| 384 | |||
| 385 | Rewind the position of CURSOR to before its first element after | ||
| 386 | completion. */ | ||
| 387 | |||
| 388 | private void | ||
| 389 | cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId, | ||
| 390 | Cursor cursor) | ||
| 391 | { | ||
| 392 | CacheEntry entry, constitutent; | ||
| 393 | int nameColumn, idColumn, typeColumn; | ||
| 394 | String id, name, type; | ||
| 395 | DocIdEntry idEntry; | ||
| 396 | |||
| 397 | /* Find the numbers of the columns wanted. */ | ||
| 398 | |||
| 399 | nameColumn | ||
| 400 | = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 401 | idColumn | ||
| 402 | = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 403 | typeColumn | ||
| 404 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 405 | |||
| 406 | if (nameColumn < 0 || idColumn < 0 || typeColumn < 0) | ||
| 407 | return; | ||
| 408 | |||
| 409 | entry = new CacheEntry (); | ||
| 410 | |||
| 411 | /* We know this is a directory already. */ | ||
| 412 | entry.type = Document.MIME_TYPE_DIR; | ||
| 413 | toplevel.idCache.put (documentId, entry); | ||
| 414 | |||
| 415 | /* Now, try to cache each of its constituents. */ | ||
| 416 | |||
| 417 | while (cursor.moveToNext ()) | ||
| 418 | { | ||
| 419 | try | ||
| 420 | { | ||
| 421 | name = cursor.getString (nameColumn); | ||
| 422 | id = cursor.getString (idColumn); | ||
| 423 | type = cursor.getString (typeColumn); | ||
| 424 | |||
| 425 | if (name == null || id == null || type == null) | ||
| 426 | continue; | ||
| 427 | |||
| 428 | /* First, add the name and ID to ENTRY's map of | ||
| 429 | children. */ | ||
| 430 | idEntry = new DocIdEntry (); | ||
| 431 | idEntry.documentId = id; | ||
| 432 | entry.children.put (id, idEntry); | ||
| 433 | |||
| 434 | /* If this constituent is a directory, don't cache any | ||
| 435 | information about it. It cannot be cached without | ||
| 436 | knowing its children. */ | ||
| 437 | |||
| 438 | if (type.equals (Document.MIME_TYPE_DIR)) | ||
| 439 | continue; | ||
| 440 | |||
| 441 | /* Otherwise, create a new cache entry comprised of its | ||
| 442 | type. */ | ||
| 443 | constitutent = new CacheEntry (); | ||
| 444 | constitutent.type = type; | ||
| 445 | toplevel.idCache.put (documentId, entry); | ||
| 446 | } | ||
| 447 | catch (Exception e) | ||
| 448 | { | ||
| 449 | e.printStackTrace (); | ||
| 450 | continue; | ||
| 451 | } | ||
| 452 | } | ||
| 453 | |||
| 454 | /* Rewind cursor back to the beginning. */ | ||
| 455 | cursor.moveToPosition (-1); | ||
| 456 | } | ||
| 457 | |||
| 458 | /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME | ||
| 459 | seconds. */ | ||
| 460 | |||
| 461 | private void | ||
| 462 | postPruneMessage () | ||
| 463 | { | ||
| 464 | handler.postDelayed (new Runnable () { | ||
| 465 | @Override | ||
| 466 | public void | ||
| 467 | run () | ||
| 468 | { | ||
| 469 | pruneCache (); | ||
| 470 | } | ||
| 471 | }, CACHE_PRUNE_TIME * 1000); | ||
| 472 | } | ||
| 473 | |||
| 474 | /* Invalidate the cache entry denoted by DOCUMENT_ID, within the | ||
| 475 | document tree URI. | ||
| 476 | Call this after deleting a document or directory. | ||
| 477 | |||
| 478 | Caveat emptor: this does not remove the component name cache | ||
| 479 | entries linked to the name(s) of the directory being removed, the | ||
| 480 | assumption being that the next time `documentIdFromName1' is | ||
| 481 | called, it will notice that the document is missing and remove | ||
| 482 | the outdated cache entry. */ | ||
| 483 | |||
| 484 | public void | ||
| 485 | postInvalidateCache (final Uri uri, final String documentId) | ||
| 486 | { | ||
| 487 | handler.post (new Runnable () { | ||
| 488 | @Override | ||
| 489 | public void | ||
| 490 | run () | ||
| 491 | { | ||
| 492 | CacheToplevel toplevel; | ||
| 493 | |||
| 494 | toplevel = getCache (uri); | ||
| 495 | toplevel.idCache.remove (documentId); | ||
| 496 | } | ||
| 497 | }); | ||
| 109 | } | 498 | } |
| 110 | 499 | ||
| 111 | 500 | ||
| @@ -289,10 +678,14 @@ public final class EmacsSafThread extends HandlerThread | |||
| 289 | String[] id_return, CancellationSignal signal) | 678 | String[] id_return, CancellationSignal signal) |
| 290 | { | 679 | { |
| 291 | Uri uri, treeUri; | 680 | Uri uri, treeUri; |
| 292 | String id, type; | 681 | String id, type, newId, newType; |
| 293 | String[] components, projection; | 682 | String[] components, projection; |
| 294 | Cursor cursor; | 683 | Cursor cursor; |
| 295 | int column; | 684 | int nameColumn, idColumn, typeColumn; |
| 685 | CacheToplevel toplevel; | ||
| 686 | DocIdEntry idEntry; | ||
| 687 | HashMap<String, DocIdEntry> children, next; | ||
| 688 | CacheEntry cache; | ||
| 296 | 689 | ||
| 297 | projection = new String[] { | 690 | projection = new String[] { |
| 298 | Document.COLUMN_DISPLAY_NAME, | 691 | Document.COLUMN_DISPLAY_NAME, |
| @@ -310,6 +703,12 @@ public final class EmacsSafThread extends HandlerThread | |||
| 310 | type = id = null; | 703 | type = id = null; |
| 311 | cursor = null; | 704 | cursor = null; |
| 312 | 705 | ||
| 706 | /* Obtain the top level of this cache. */ | ||
| 707 | toplevel = getCache (uri); | ||
| 708 | |||
| 709 | /* Set the current map of children to this top level. */ | ||
| 710 | children = toplevel.children; | ||
| 711 | |||
| 313 | /* For each component... */ | 712 | /* For each component... */ |
| 314 | 713 | ||
| 315 | try | 714 | try |
| @@ -321,6 +720,48 @@ public final class EmacsSafThread extends HandlerThread | |||
| 321 | if (component.isEmpty ()) | 720 | if (component.isEmpty ()) |
| 322 | continue; | 721 | continue; |
| 323 | 722 | ||
| 723 | /* Search for component within the currently cached list | ||
| 724 | of children. */ | ||
| 725 | |||
| 726 | idEntry = children.get (component); | ||
| 727 | |||
| 728 | if (idEntry != null) | ||
| 729 | { | ||
| 730 | /* The document ID is known. Now find the | ||
| 731 | corresponding document ID cache. */ | ||
| 732 | |||
| 733 | cache = toplevel.idCache.get (idEntry.documentId); | ||
| 734 | |||
| 735 | /* Fetch just the information for this document. */ | ||
| 736 | |||
| 737 | if (cache == null) | ||
| 738 | cache = idEntry.getCacheEntry (uri, toplevel, signal); | ||
| 739 | |||
| 740 | if (cache == null) | ||
| 741 | { | ||
| 742 | /* File status matching idEntry could not be | ||
| 743 | obtained. Treat this as if the file does not | ||
| 744 | exist. */ | ||
| 745 | |||
| 746 | children.remove (idEntry); | ||
| 747 | |||
| 748 | if ((type == null | ||
| 749 | || type.equals (Document.MIME_TYPE_DIR)) | ||
| 750 | /* ... and type and id currently represent the | ||
| 751 | penultimate component. */ | ||
| 752 | && component == components[components.length - 1]) | ||
| 753 | return -2; | ||
| 754 | |||
| 755 | return -1; | ||
| 756 | } | ||
| 757 | |||
| 758 | /* Otherwise, use the cached information. */ | ||
| 759 | id = idEntry.documentId; | ||
| 760 | type = cache.type; | ||
| 761 | children = cache.children; | ||
| 762 | continue; | ||
| 763 | } | ||
| 764 | |||
| 324 | /* Create the tree URI for URI from ID if it exists, or | 765 | /* Create the tree URI for URI from ID if it exists, or |
| 325 | the root otherwise. */ | 766 | the root otherwise. */ |
| 326 | 767 | ||
| @@ -342,6 +783,21 @@ public final class EmacsSafThread extends HandlerThread | |||
| 342 | if (cursor == null) | 783 | if (cursor == null) |
| 343 | return -1; | 784 | return -1; |
| 344 | 785 | ||
| 786 | /* Find the column numbers for each of the columns that | ||
| 787 | are wanted. */ | ||
| 788 | |||
| 789 | nameColumn | ||
| 790 | = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 791 | idColumn | ||
| 792 | = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 793 | typeColumn | ||
| 794 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 795 | |||
| 796 | if (nameColumn < 0 || idColumn < 0 || typeColumn < 0) | ||
| 797 | return -1; | ||
| 798 | |||
| 799 | next = null; | ||
| 800 | |||
| 345 | while (true) | 801 | while (true) |
| 346 | { | 802 | { |
| 347 | /* Even though the query selects for a specific | 803 | /* Even though the query selects for a specific |
| @@ -350,6 +806,12 @@ public final class EmacsSafThread extends HandlerThread | |||
| 350 | 806 | ||
| 351 | if (!cursor.moveToNext ()) | 807 | if (!cursor.moveToNext ()) |
| 352 | { | 808 | { |
| 809 | /* If a component has been found, break out of the | ||
| 810 | loop. */ | ||
| 811 | |||
| 812 | if (next != null) | ||
| 813 | break; | ||
| 814 | |||
| 353 | /* If the last component considered is a | 815 | /* If the last component considered is a |
| 354 | directory... */ | 816 | directory... */ |
| 355 | if ((type == null | 817 | if ((type == null |
| @@ -382,52 +844,38 @@ public final class EmacsSafThread extends HandlerThread | |||
| 382 | /* So move CURSOR to a row with the right display | 844 | /* So move CURSOR to a row with the right display |
| 383 | name. */ | 845 | name. */ |
| 384 | 846 | ||
| 385 | column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | 847 | name = cursor.getString (nameColumn); |
| 386 | 848 | newId = cursor.getString (idColumn); | |
| 387 | if (column < 0) | 849 | newType = cursor.getString (typeColumn); |
| 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 | 850 | ||
| 407 | /* Now replace ID with the document ID. */ | 851 | /* Any of the three variables above may be NULL if the |
| 852 | column data is of the wrong type depending on how | ||
| 853 | the Cursor returned is implemented. */ | ||
| 408 | 854 | ||
| 409 | id = cursor.getString (column); | 855 | if (name == null || newId == null || newType == null) |
| 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; | 856 | return -1; |
| 421 | 857 | ||
| 422 | type = cursor.getString (column); | 858 | /* Cache this name, even if it isn't the document |
| 859 | that's being searched for. */ | ||
| 423 | 860 | ||
| 424 | /* Type may be NULL depending on how the Cursor | 861 | cache = cacheChild (toplevel, children, name, |
| 425 | returned is implemented. */ | 862 | newId, newType, |
| 863 | idEntry != null); | ||
| 426 | 864 | ||
| 427 | if (type == null) | 865 | /* Record the desired component once it is located, |
| 428 | return -1; | 866 | but continue reading and caching items from the |
| 867 | cursor. */ | ||
| 868 | |||
| 869 | if (name.equals (component)) | ||
| 870 | { | ||
| 871 | id = newId; | ||
| 872 | next = cache.children; | ||
| 873 | type = newType; | ||
| 874 | } | ||
| 429 | } | 875 | } |
| 430 | 876 | ||
| 877 | children = next; | ||
| 878 | |||
| 431 | /* Now close the cursor. */ | 879 | /* Now close the cursor. */ |
| 432 | cursor.close (); | 880 | cursor.close (); |
| 433 | cursor = null; | 881 | cursor = null; |
| @@ -771,11 +1219,12 @@ public final class EmacsSafThread extends HandlerThread | |||
| 771 | openDocumentDirectory1 (String uri, String documentId, | 1219 | openDocumentDirectory1 (String uri, String documentId, |
| 772 | CancellationSignal signal) | 1220 | CancellationSignal signal) |
| 773 | { | 1221 | { |
| 774 | Uri uriObject; | 1222 | Uri uriObject, tree; |
| 775 | Cursor cursor; | 1223 | Cursor cursor; |
| 776 | String projection[]; | 1224 | String projection[]; |
| 1225 | CacheToplevel toplevel; | ||
| 777 | 1226 | ||
| 778 | uriObject = Uri.parse (uri); | 1227 | tree = uriObject = Uri.parse (uri); |
| 779 | 1228 | ||
| 780 | /* If documentId is not set, use the document ID of the tree URI | 1229 | /* If documentId is not set, use the document ID of the tree URI |
| 781 | itself. */ | 1230 | itself. */ |
| @@ -792,11 +1241,22 @@ public final class EmacsSafThread extends HandlerThread | |||
| 792 | 1241 | ||
| 793 | projection = new String [] { | 1242 | projection = new String [] { |
| 794 | Document.COLUMN_DISPLAY_NAME, | 1243 | Document.COLUMN_DISPLAY_NAME, |
| 1244 | Document.COLUMN_DOCUMENT_ID, | ||
| 795 | Document.COLUMN_MIME_TYPE, | 1245 | Document.COLUMN_MIME_TYPE, |
| 796 | }; | 1246 | }; |
| 797 | 1247 | ||
| 798 | cursor = resolver.query (uriObject, projection, null, null, | 1248 | cursor = resolver.query (uriObject, projection, null, null, |
| 799 | null, signal); | 1249 | null, signal); |
| 1250 | |||
| 1251 | /* Create a new cache entry tied to this document ID. */ | ||
| 1252 | |||
| 1253 | if (cursor != null) | ||
| 1254 | { | ||
| 1255 | toplevel = getCache (tree); | ||
| 1256 | cacheDirectoryFromCursor (toplevel, documentId, | ||
| 1257 | cursor); | ||
| 1258 | } | ||
| 1259 | |||
| 800 | /* Return the cursor. */ | 1260 | /* Return the cursor. */ |
| 801 | return cursor; | 1261 | return cursor; |
| 802 | } | 1262 | } |
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e410754071b..e2abd6c96ef 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java | |||
| @@ -1680,14 +1680,18 @@ public final class EmacsService extends Service | |||
| 1680 | deleteDocument (String uri, String id) | 1680 | deleteDocument (String uri, String id) |
| 1681 | throws FileNotFoundException | 1681 | throws FileNotFoundException |
| 1682 | { | 1682 | { |
| 1683 | Uri uriObject; | 1683 | Uri uriObject, tree; |
| 1684 | 1684 | ||
| 1685 | uriObject = Uri.parse (uri); | 1685 | tree = Uri.parse (uri); |
| 1686 | uriObject = DocumentsContract.buildDocumentUriUsingTree (uriObject, | 1686 | uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id); |
| 1687 | id); | ||
| 1688 | 1687 | ||
| 1689 | if (DocumentsContract.deleteDocument (resolver, uriObject)) | 1688 | if (DocumentsContract.deleteDocument (resolver, uriObject)) |
| 1690 | return 0; | 1689 | { |
| 1690 | if (storageThread != null) | ||
| 1691 | storageThread.postInvalidateCache (tree, id); | ||
| 1692 | |||
| 1693 | return 0; | ||
| 1694 | } | ||
| 1691 | 1695 | ||
| 1692 | return -1; | 1696 | return -1; |
| 1693 | } | 1697 | } |