diff options
| author | Po Lu | 2023-07-27 17:13:39 +0800 |
|---|---|---|
| committer | Po Lu | 2023-07-27 17:13:39 +0800 |
| commit | 65b58251b1b07b50672367c675efdfdc97354c4a (patch) | |
| tree | 9e323a76c3d5f81ecce18321beca703c456b2802 /java/org | |
| parent | ce63f592f579e8963a3e7f44b31c7df50fb0cdba (diff) | |
| download | emacs-65b58251b1b07b50672367c675efdfdc97354c4a.tar.gz emacs-65b58251b1b07b50672367c675efdfdc97354c4a.zip | |
Update Android port
* configure.ac (ANDROID_STUBIFY): Add androidvfs.o when building
libemacs.so.
* doc/emacs/android.texi (Android): Add `Android Document Providers'.
(Android Startup): Update the location of the content identifier
directory.
(Android File System): Describe access to document provider
directories.
(Android Document Providers): New node.
* doc/emacs/emacs.texi (Top): Update the menu for the Android
appendix.
* java/Makefile.in (filename, install_temp/assets/build_info): Make
directory-tree depend on build_info.
* java/org/gnu/emacs/EmacsActivity.java (onActivityResult): New
function. When a document tree is accepted, persist access to it.
* java/org/gnu/emacs/EmacsDirectoryEntry.java (EmacsDirectoryEntry):
New struct.
* java/org/gnu/emacs/EmacsOpenActivity.java (checkReadableOrCopy): Use
EmacsService.buildContentName.
* java/org/gnu/emacs/EmacsService.java (getEmacsView, openContentUri)
(checkContentUri): Remove excessive debug logging.
(buildContentName, getDocumentAuthorities, requestDirectoryAccess)
(getDocumentTrees, decodeFileName, documentIdFromName, getTreeUri)
(statDocument, accessDocument, openDocumentDirectory, readDirectoryEntry)
(openDocument, createDocument): New functions.
* lib-src/asset-directory-tool.c: Improve commentary by illustrating
the difference between directory and ordinary files.
* src/android.c (ANDROID_THROW, enum android_fd_table_entry_flags)
(struct android_emacs_service, android_extract_long)
(android_scan_directory_tree, android_is_directory)
(android_get_asset_name, android_url_encode, android_content_name_p)
(android_get_content_name, android_check_content_access, android_fstat)
(android_fstatat, android_file_access_p, android_hack_asset_fd_fallback)
(android_detect_ashmem, android_hack_asset_fd, android_close_on_exec)
(android_open, android_close, android_fclose, android_create_lib_link)
(android_faccessat, struct android_dir, android_opendir, android_dirfd)
(android_readdir, android_closedir, android_lookup_asset_directory_fd)
(android_exception_check_3, android_get_current_api_level)
(android_open_asset, android_close_asset, android_asset_read_quit)
(android_asset_read, android_asset_lseek, android_asset_fstat): Move
content and asset related functions to androidvfs.c.
(android_init_emacs_service): Obtain handles for new JNI functions.
(initEmacsParams): Initialize the VFS layer.
(android_request_directory_access): New function.
(android_display_toast): Remove unused function.
* src/android.h (android_get_current_api_level): Assume that
this function never returns less than __ANDROID_API__.
(struct android_emacs_service): Move `struct
android_emacs_service' here.
* src/androidfns.c (Fandroid_request_directory_access): New
interactive function.
(syms_of_androidfns): Register new subr.
* src/androidvfs.c (struct android_vdir, struct android_vops)
(struct android_vnode, struct android_special_vnode)
(enum android_vnode_type, struct android_cursor_class)
(struct emacs_directory_entry_class)
(struct android_parcel_file_descriptor_class)
(android_init_cursor_class, android_init_entry_class)
(android_init_fd_class, android_vfs_canonicalize_name)
(struct android_unix_vnode, struct android_unix_vdir, unix_vfs_ops)
(android_unix_name, android_unix_vnode, android_unix_open)
(android_unix_close, android_unix_unlink, android_unix_symlink)
(android_unix_rmdir, android_unix_rename, android_unix_stat)
(android_unix_access, android_unix_mkdir, android_unix_readdir)
(android_unix_closedir, android_unix_dirfd, android_unix_opendir)
(android_extract_long, android_scan_directory_tree)
(android_is_directory, android_init_assets)
(android_hack_asset_fd_fallback, android_detect_ashmem)
(android_hack_asset_fd, struct android_afs_vnode)
(struct android_afs_vdir, struct android_afs_open_fd, afs_vfs_ops)
(android_afs_name, android_afs_initial, android_close_on_exec)
(android_afs_open, android_afs_close, android_afs_unlink)
(android_afs_symlink, android_afs_rmdir, android_afs_rename)
(android_afs_stat, android_afs_access, android_afs_mkdir)
(android_afs_readdir, android_afs_closedir, android_afs_dirfd)
(android_afs_opendir, android_afs_get_directory_name)
(struct android_content_vdir, content_vfs_ops)
(content_directory_contents, android_content_name)
(android_content_open, android_content_close)
(android_content_unlink, android_content_symlink)
(android_content_rmdir, android_content_rename)
(android_content_stat, android_content_access)
(android_content_mkdir, android_content_readdir)
(android_content_closedir, android_content_dirfd)
(android_content_opendir, android_content_get_directory_name)
(android_content_initial, android_get_content_name)
(android_check_content_access, struct android_authority_vnode)
(authority_vfs_ops, android_authority_name, android_authority_open)
(android_authority_close, android_authority_unlink)
(android_authority_symlink, android_authority_rmdir)
(android_authority_rename, android_authority_stat)
(android_authority_access, android_authority_mkdir)
(android_authority_opendir, android_authority_initial)
(struct android_saf_root_vnode, struct android_saf_root_vdir)
(saf_root_vfs_ops, android_saf_root_name, android_saf_root_open)
(android_saf_root_close, android_saf_root_unlink)
(android_saf_root_symlink, android_saf_root_rmdir)
(android_saf_root_rename, android_saf_root_stat)
(android_saf_root_access, android_saf_root_mkdir)
(android_saf_root_readdir, android_saf_root_closedir)
(android_saf_root_dirfd, android_saf_root_opendir)
(android_saf_root_initial, android_saf_root_get_directory)
(android_saf_stat, android_saf_access)
(struct android_saf_tree_vnode, struct android_saf_tree_vdir)
(saf_tree_vfs_ops, android_document_id_from_name)
(android_saf_tree_name, android_saf_tree_open)
(android_saf_tree_close, android_saf_tree_unlink)
(android_saf_tree_symlink, android_saf_tree_rmdir)
(android_saf_tree_rename, android_saf_tree_stat)
(android_saf_tree_access, android_saf_tree_mkdir)
(android_saf_tree_opendir_1, android_saf_tree_readdir)
(android_saf_tree_closedir, android_saf_tree_dirfd)
(android_saf_tree_opendir, android_saf_tree_from_name)
(android_saf_tree_get_directory, android_saf_file_vnode)
(saf_file_vfs_ops, android_saf_file_name, android_saf_file_open)
(android_saf_file_unlink, android_saf_file_rmdir)
(android_saf_file_opendir, android_close_parcel_fd)
(android_saf_new_vnode, android_saf_new_name, android_saf_new_open)
(android_saf_new_unlink, android_saf_new_symlink)
(android_saf_new_rmdir, android_saf_new_rename)
(android_saf_new_stat, android_saf_new_access)
(android_saf_new_mkdir, android_saf_new_opendir, root_vfs_ops)
(special_vnodes, android_root_name, android_name_file)
(android_vfs_init, android_open, android_unlink, android_symlink)
(android_rmdir, android_mkdir, android_renameat_noreplace)
(android_rename, android_fstat, android_fstatat_1, android_fstatat)
(android_faccessat, android_fdopen, android_close, android_fclose)
(android_open_asset, android_close_asset, android_asset_read_quit)
(android_asset_read, android_asset_lseek, android_asset_fstat)
(android_opendir, android_dirfd, android_readdir)
(android_closedir): Move file system emulation routines here.
Introduce a new ``VFS'' layer for translating between
Emacs-specific file names and the various disparate interfaces
for accessing files on Android.
* src/callproc.c (delete_temp_file):
* src/charset.c (load_charset_map_from_file):
* src/dired.c:
* src/emacs.c (Fkill_emacs):
* src/fileio.c (check_mutable_filename, Fcopy_file)
(Fmake_directory_internal, Fdelete_directory_internal)
(Fdelete_file, Frename_file, Fadd_name_to_file)
(Fmake_symbolic_link, file_accessible_directory_p, Fset_file_modes)
(Fset_file_times, write_region):
* src/filelock.c (get_boot_time, rename_lock_file)
(create_lock_file, current_lock_owner, unlock_file):
* src/image.c (slurp_file, png_load_body, jpeg_load_body):
* src/keyboard.c (Fopen_dribble_file):
* src/lisp.h:
* src/lread.c (Fload):
* src/process.c (handle_child_signal):
* src/sysdep.c (init_standard_fds, emacs_fopen, emacs_fdopen)
(emacs_unlink, emacs_symlink, emacs_rmdir, emacs_mkdir)
(emacs_renameat_noreplace, emacs_rename):
* src/term.c (Fresume_tty, init_tty): Use and add new wrappers
for fopen, fdopen, unlink, symlink, rmdir, mkdir,
renameat_norepalce and rename.
Diffstat (limited to 'java/org')
| -rw-r--r-- | java/org/gnu/emacs/EmacsActivity.java | 38 | ||||
| -rw-r--r-- | java/org/gnu/emacs/EmacsDirectoryEntry.java | 33 | ||||
| -rw-r--r-- | java/org/gnu/emacs/EmacsOpenActivity.java | 12 | ||||
| -rw-r--r-- | java/org/gnu/emacs/EmacsService.java | 1041 |
4 files changed, 1101 insertions, 23 deletions
diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index d7b51388929..86fed5396d7 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java | |||
| @@ -25,6 +25,7 @@ import java.util.ArrayList; | |||
| 25 | 25 | ||
| 26 | import android.app.Activity; | 26 | import android.app.Activity; |
| 27 | 27 | ||
| 28 | import android.content.ContentResolver; | ||
| 28 | import android.content.Context; | 29 | import android.content.Context; |
| 29 | import android.content.Intent; | 30 | import android.content.Intent; |
| 30 | 31 | ||
| @@ -33,6 +34,8 @@ import android.os.Bundle; | |||
| 33 | 34 | ||
| 34 | import android.util.Log; | 35 | import android.util.Log; |
| 35 | 36 | ||
| 37 | import android.net.Uri; | ||
| 38 | |||
| 36 | import android.view.Menu; | 39 | import android.view.Menu; |
| 37 | import android.view.View; | 40 | import android.view.View; |
| 38 | import android.view.ViewTreeObserver; | 41 | import android.view.ViewTreeObserver; |
| @@ -48,6 +51,9 @@ public class EmacsActivity extends Activity | |||
| 48 | { | 51 | { |
| 49 | public static final String TAG = "EmacsActivity"; | 52 | public static final String TAG = "EmacsActivity"; |
| 50 | 53 | ||
| 54 | /* ID for URIs from a granted document tree. */ | ||
| 55 | public static final int ACCEPT_DOCUMENT_TREE = 1; | ||
| 56 | |||
| 51 | /* The currently attached EmacsWindow, or null if none. */ | 57 | /* The currently attached EmacsWindow, or null if none. */ |
| 52 | private EmacsWindow window; | 58 | private EmacsWindow window; |
| 53 | 59 | ||
| @@ -431,4 +437,36 @@ public class EmacsActivity extends Activity | |||
| 431 | /* Update the window insets. */ | 437 | /* Update the window insets. */ |
| 432 | syncFullscreenWith (window); | 438 | syncFullscreenWith (window); |
| 433 | } | 439 | } |
| 440 | |||
| 441 | |||
| 442 | |||
| 443 | @Override | ||
| 444 | public final void | ||
| 445 | onActivityResult (int requestCode, int resultCode, Intent data) | ||
| 446 | { | ||
| 447 | ContentResolver resolver; | ||
| 448 | Uri uri; | ||
| 449 | int flags; | ||
| 450 | |||
| 451 | switch (requestCode) | ||
| 452 | { | ||
| 453 | case ACCEPT_DOCUMENT_TREE: | ||
| 454 | |||
| 455 | /* A document granted through | ||
| 456 | EmacsService.requestDirectoryAccess. */ | ||
| 457 | |||
| 458 | if (resultCode == RESULT_OK) | ||
| 459 | { | ||
| 460 | resolver = getContentResolver (); | ||
| 461 | uri = data.getData (); | ||
| 462 | flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 463 | | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); | ||
| 464 | |||
| 465 | if (uri != null) | ||
| 466 | resolver.takePersistableUriPermission (uri, flags); | ||
| 467 | } | ||
| 468 | |||
| 469 | break; | ||
| 470 | } | ||
| 471 | } | ||
| 434 | }; | 472 | }; |
diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java new file mode 100644 index 00000000000..9c10f2e8771 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java | |||
| @@ -0,0 +1,33 @@ | |||
| 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 | /* Structure holding a single ``directory entry'' from a document | ||
| 23 | provider. */ | ||
| 24 | |||
| 25 | public class EmacsDirectoryEntry | ||
| 26 | { | ||
| 27 | /* The type of this directory entry. 0 means a regular file and 1 | ||
| 28 | means a directory. */ | ||
| 29 | public int d_type; | ||
| 30 | |||
| 31 | /* The display name of the file represented. */ | ||
| 32 | public String d_name; | ||
| 33 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 9411f85d434..3832cd2faab 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java | |||
| @@ -243,18 +243,8 @@ public final class EmacsOpenActivity extends Activity | |||
| 243 | 243 | ||
| 244 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) | 244 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) |
| 245 | { | 245 | { |
| 246 | content = "/content/" + uri.getEncodedAuthority (); | 246 | content = EmacsService.buildContentName (uri); |
| 247 | |||
| 248 | for (String segment : uri.getPathSegments ()) | ||
| 249 | content += "/" + Uri.encode (segment); | ||
| 250 | |||
| 251 | /* Append the URI query. */ | ||
| 252 | |||
| 253 | if (uri.getEncodedQuery () != null) | ||
| 254 | content += "?" + uri.getEncodedQuery (); | ||
| 255 | |||
| 256 | Log.d (TAG, "checkReadableOrCopy: " + content); | 247 | Log.d (TAG, "checkReadableOrCopy: " + content); |
| 257 | |||
| 258 | return content; | 248 | return content; |
| 259 | } | 249 | } |
| 260 | 250 | ||
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 6240b0e475a..6059439551f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java | |||
| @@ -23,13 +23,19 @@ import java.io.FileNotFoundException; | |||
| 23 | import java.io.IOException; | 23 | import java.io.IOException; |
| 24 | import java.io.UnsupportedEncodingException; | 24 | import java.io.UnsupportedEncodingException; |
| 25 | 25 | ||
| 26 | import java.util.ArrayList; | ||
| 27 | import java.util.HashSet; | ||
| 26 | import java.util.List; | 28 | import java.util.List; |
| 27 | 29 | ||
| 28 | import java.util.concurrent.atomic.AtomicInteger; | 30 | import java.util.concurrent.atomic.AtomicInteger; |
| 29 | 31 | ||
| 32 | import android.database.Cursor; | ||
| 33 | |||
| 30 | import android.graphics.Matrix; | 34 | import android.graphics.Matrix; |
| 31 | import android.graphics.Point; | 35 | import android.graphics.Point; |
| 32 | 36 | ||
| 37 | import android.webkit.MimeTypeMap; | ||
| 38 | |||
| 33 | import android.view.InputDevice; | 39 | import android.view.InputDevice; |
| 34 | import android.view.KeyEvent; | 40 | import android.view.KeyEvent; |
| 35 | import android.view.inputmethod.CursorAnchorInfo; | 41 | import android.view.inputmethod.CursorAnchorInfo; |
| @@ -45,9 +51,12 @@ import android.content.Context; | |||
| 45 | import android.content.ContentResolver; | 51 | import android.content.ContentResolver; |
| 46 | import android.content.Intent; | 52 | import android.content.Intent; |
| 47 | import android.content.IntentFilter; | 53 | import android.content.IntentFilter; |
| 54 | import android.content.UriPermission; | ||
| 55 | |||
| 48 | import android.content.pm.ApplicationInfo; | 56 | import android.content.pm.ApplicationInfo; |
| 49 | import android.content.pm.PackageManager.ApplicationInfoFlags; | 57 | import android.content.pm.PackageManager.ApplicationInfoFlags; |
| 50 | import android.content.pm.PackageManager; | 58 | import android.content.pm.PackageManager; |
| 59 | |||
| 51 | import android.content.res.AssetManager; | 60 | import android.content.res.AssetManager; |
| 52 | 61 | ||
| 53 | import android.hardware.input.InputManager; | 62 | import android.hardware.input.InputManager; |
| @@ -65,6 +74,7 @@ import android.os.VibratorManager; | |||
| 65 | import android.os.VibrationEffect; | 74 | import android.os.VibrationEffect; |
| 66 | 75 | ||
| 67 | import android.provider.DocumentsContract; | 76 | import android.provider.DocumentsContract; |
| 77 | import android.provider.DocumentsContract.Document; | ||
| 68 | 78 | ||
| 69 | import android.util.Log; | 79 | import android.util.Log; |
| 70 | import android.util.DisplayMetrics; | 80 | import android.util.DisplayMetrics; |
| @@ -77,14 +87,21 @@ import android.widget.Toast; | |||
| 77 | public final class EmacsService extends Service | 87 | public final class EmacsService extends Service |
| 78 | { | 88 | { |
| 79 | public static final String TAG = "EmacsService"; | 89 | public static final String TAG = "EmacsService"; |
| 80 | public static volatile EmacsService SERVICE; | 90 | |
| 91 | /* The started Emacs service object. */ | ||
| 92 | public static EmacsService SERVICE; | ||
| 81 | 93 | ||
| 82 | /* If non-NULL, an extra argument to pass to | 94 | /* If non-NULL, an extra argument to pass to |
| 83 | `android_emacs_init'. */ | 95 | `android_emacs_init'. */ |
| 84 | public static String extraStartupArgument; | 96 | public static String extraStartupArgument; |
| 85 | 97 | ||
| 98 | /* The thread running Emacs C code. */ | ||
| 86 | private EmacsThread thread; | 99 | private EmacsThread thread; |
| 100 | |||
| 101 | /* Handler used to run tasks on the main thread. */ | ||
| 87 | private Handler handler; | 102 | private Handler handler; |
| 103 | |||
| 104 | /* Content resolver used to access URIs. */ | ||
| 88 | private ContentResolver resolver; | 105 | private ContentResolver resolver; |
| 89 | 106 | ||
| 90 | /* Keep this in synch with androidgui.h. */ | 107 | /* Keep this in synch with androidgui.h. */ |
| @@ -92,6 +109,13 @@ public final class EmacsService extends Service | |||
| 92 | public static final int IC_MODE_ACTION = 1; | 109 | public static final int IC_MODE_ACTION = 1; |
| 93 | public static final int IC_MODE_TEXT = 2; | 110 | public static final int IC_MODE_TEXT = 2; |
| 94 | 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 | |||
| 95 | /* Display metrics used by font backends. */ | 119 | /* Display metrics used by font backends. */ |
| 96 | public DisplayMetrics metrics; | 120 | public DisplayMetrics metrics; |
| 97 | 121 | ||
| @@ -305,6 +329,7 @@ public final class EmacsService extends Service | |||
| 305 | view = new EmacsHolder<EmacsView> (); | 329 | view = new EmacsHolder<EmacsView> (); |
| 306 | 330 | ||
| 307 | runnable = new Runnable () { | 331 | runnable = new Runnable () { |
| 332 | @Override | ||
| 308 | public void | 333 | public void |
| 309 | run () | 334 | run () |
| 310 | { | 335 | { |
| @@ -557,7 +582,7 @@ public final class EmacsService extends Service | |||
| 557 | return String.valueOf (keysym); | 582 | return String.valueOf (keysym); |
| 558 | } | 583 | } |
| 559 | 584 | ||
| 560 | 585 | ||
| 561 | 586 | ||
| 562 | /* Start the Emacs service if necessary. On Android 26 and up, | 587 | /* Start the Emacs service if necessary. On Android 26 and up, |
| 563 | start Emacs as a foreground service with a notification, to avoid | 588 | start Emacs as a foreground service with a notification, to avoid |
| @@ -872,6 +897,10 @@ public final class EmacsService extends Service | |||
| 872 | icEndSynchronous (); | 897 | icEndSynchronous (); |
| 873 | } | 898 | } |
| 874 | 899 | ||
| 900 | |||
| 901 | |||
| 902 | /* Content provider functions. */ | ||
| 903 | |||
| 875 | /* Open a content URI described by the bytes BYTES, a non-terminated | 904 | /* Open a content URI described by the bytes BYTES, a non-terminated |
| 876 | string; make it writable if WRITABLE, and readable if READABLE. | 905 | string; make it writable if WRITABLE, and readable if READABLE. |
| 877 | Truncate the file if TRUNCATE. | 906 | Truncate the file if TRUNCATE. |
| @@ -905,9 +934,8 @@ public final class EmacsService extends Service | |||
| 905 | { | 934 | { |
| 906 | /* The usual file name encoding question rears its ugly head | 935 | /* The usual file name encoding question rears its ugly head |
| 907 | again. */ | 936 | again. */ |
| 908 | name = new String (bytes, "UTF-8"); | ||
| 909 | Log.d (TAG, "openContentUri: " + Uri.parse (name)); | ||
| 910 | 937 | ||
| 938 | name = new String (bytes, "UTF-8"); | ||
| 911 | fd = resolver.openFileDescriptor (Uri.parse (name), mode); | 939 | fd = resolver.openFileDescriptor (Uri.parse (name), mode); |
| 912 | 940 | ||
| 913 | /* Use detachFd on newer versions of Android or plain old | 941 | /* Use detachFd on newer versions of Android or plain old |
| @@ -947,7 +975,6 @@ public final class EmacsService extends Service | |||
| 947 | /* The usual file name encoding question rears its ugly head | 975 | /* The usual file name encoding question rears its ugly head |
| 948 | again. */ | 976 | again. */ |
| 949 | name = new String (string, "UTF-8"); | 977 | name = new String (string, "UTF-8"); |
| 950 | Log.d (TAG, "checkContentUri: " + Uri.parse (name)); | ||
| 951 | } | 978 | } |
| 952 | catch (UnsupportedEncodingException exception) | 979 | catch (UnsupportedEncodingException exception) |
| 953 | { | 980 | { |
| @@ -960,25 +987,58 @@ public final class EmacsService extends Service | |||
| 960 | if (writable) | 987 | if (writable) |
| 961 | mode += "w"; | 988 | mode += "w"; |
| 962 | 989 | ||
| 963 | Log.d (TAG, "checkContentUri: checking against mode " + mode); | ||
| 964 | |||
| 965 | try | 990 | try |
| 966 | { | 991 | { |
| 967 | fd = resolver.openFileDescriptor (Uri.parse (name), mode); | 992 | fd = resolver.openFileDescriptor (Uri.parse (name), mode); |
| 968 | fd.close (); | 993 | fd.close (); |
| 969 | 994 | ||
| 970 | Log.d (TAG, "checkContentUri: YES"); | ||
| 971 | |||
| 972 | return true; | 995 | return true; |
| 973 | } | 996 | } |
| 974 | catch (Exception exception) | 997 | catch (Exception exception) |
| 975 | { | 998 | { |
| 976 | Log.d (TAG, "checkContentUri: NO"); | 999 | /* Fall through. */ |
| 977 | Log.d (TAG, exception.toString ()); | ||
| 978 | return false; | ||
| 979 | } | 1000 | } |
| 1001 | |||
| 1002 | return false; | ||
| 980 | } | 1003 | } |
| 981 | 1004 | ||
| 1005 | /* Build a content file name for URI. | ||
| 1006 | |||
| 1007 | Return a file name within the /contents/by-authority | ||
| 1008 | pseudo-directory that `android_get_content_name' can then | ||
| 1009 | transform back into an encoded URI. | ||
| 1010 | |||
| 1011 | A content name consists of any number of unencoded path segments | ||
| 1012 | separated by `/' characters, possibly followed by a question mark | ||
| 1013 | and an encoded query string. */ | ||
| 1014 | |||
| 1015 | public static String | ||
| 1016 | buildContentName (Uri uri) | ||
| 1017 | { | ||
| 1018 | StringBuilder builder; | ||
| 1019 | |||
| 1020 | builder = new StringBuilder ("/content/by-authority/"); | ||
| 1021 | builder.append (uri.getAuthority ()); | ||
| 1022 | |||
| 1023 | /* First, append each path segment. */ | ||
| 1024 | |||
| 1025 | for (String segment : uri.getPathSegments ()) | ||
| 1026 | { | ||
| 1027 | /* FIXME: what if segment contains a slash character? */ | ||
| 1028 | builder.append ('/'); | ||
| 1029 | builder.append (uri.encode (segment)); | ||
| 1030 | } | ||
| 1031 | |||
| 1032 | /* Now, append the query string if necessary. */ | ||
| 1033 | |||
| 1034 | if (uri.getEncodedQuery () != null) | ||
| 1035 | builder.append ('?').append (uri.getEncodedQuery ()); | ||
| 1036 | |||
| 1037 | return builder.toString (); | ||
| 1038 | } | ||
| 1039 | |||
| 1040 | |||
| 1041 | |||
| 982 | private long[] | 1042 | private long[] |
| 983 | queryBattery19 () | 1043 | queryBattery19 () |
| 984 | { | 1044 | { |
| @@ -1096,4 +1156,961 @@ public final class EmacsService extends Service | |||
| 1096 | window.view.imManager.updateExtractedText (window.view, | 1156 | window.view.imManager.updateExtractedText (window.view, |
| 1097 | token, text); | 1157 | token, text); |
| 1098 | } | 1158 | } |
| 1159 | |||
| 1160 | |||
| 1161 | |||
| 1162 | /* Document tree management functions. These functions shouldn't be | ||
| 1163 | 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 | |||
| 1168 | /* Return an array of each document authority providing at least one | ||
| 1169 | tree URI that Emacs holds the rights to persistently access. */ | ||
| 1170 | |||
| 1171 | public String[] | ||
| 1172 | getDocumentAuthorities () | ||
| 1173 | { | ||
| 1174 | List<UriPermission> permissions; | ||
| 1175 | HashSet<String> allProviders; | ||
| 1176 | Uri uri; | ||
| 1177 | |||
| 1178 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1179 | allProviders = new HashSet<String> (); | ||
| 1180 | |||
| 1181 | for (UriPermission permission : permissions) | ||
| 1182 | { | ||
| 1183 | uri = permission.getUri (); | ||
| 1184 | |||
| 1185 | if (DocumentsContract.isTreeUri (uri) | ||
| 1186 | && permission.isReadPermission ()) | ||
| 1187 | allProviders.add (uri.getAuthority ()); | ||
| 1188 | } | ||
| 1189 | |||
| 1190 | return allProviders.toArray (new String[0]); | ||
| 1191 | } | ||
| 1192 | |||
| 1193 | /* Start a file chooser activity to request access to a directory | ||
| 1194 | tree. | ||
| 1195 | |||
| 1196 | Value is 1 if the activity couldn't be started for some reason, | ||
| 1197 | and 0 in any other case. */ | ||
| 1198 | |||
| 1199 | public int | ||
| 1200 | requestDirectoryAccess () | ||
| 1201 | { | ||
| 1202 | Runnable runnable; | ||
| 1203 | final EmacsHolder<Integer> rc; | ||
| 1204 | |||
| 1205 | /* Return 1 if Android is too old to support this feature. */ | ||
| 1206 | |||
| 1207 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) | ||
| 1208 | return 1; | ||
| 1209 | |||
| 1210 | rc = new EmacsHolder<Integer> (); | ||
| 1211 | rc.thing = Integer.valueOf (1); | ||
| 1212 | |||
| 1213 | runnable = new Runnable () { | ||
| 1214 | @Override | ||
| 1215 | public void | ||
| 1216 | run () | ||
| 1217 | { | ||
| 1218 | EmacsActivity activity; | ||
| 1219 | Intent intent; | ||
| 1220 | int id; | ||
| 1221 | |||
| 1222 | synchronized (this) | ||
| 1223 | { | ||
| 1224 | /* Try to obtain an activity that will receive the | ||
| 1225 | response from the file chooser dialog. */ | ||
| 1226 | |||
| 1227 | if (EmacsActivity.focusedActivities.isEmpty ()) | ||
| 1228 | { | ||
| 1229 | /* If focusedActivities is empty then this dialog | ||
| 1230 | may have been displayed immediately after another | ||
| 1231 | popup dialog was dismissed. Try the | ||
| 1232 | EmacsActivity to be focused. */ | ||
| 1233 | |||
| 1234 | activity = EmacsActivity.lastFocusedActivity; | ||
| 1235 | |||
| 1236 | if (activity == null) | ||
| 1237 | { | ||
| 1238 | /* Still no luck. Return failure. */ | ||
| 1239 | notify (); | ||
| 1240 | return; | ||
| 1241 | } | ||
| 1242 | } | ||
| 1243 | else | ||
| 1244 | activity = EmacsActivity.focusedActivities.get (0); | ||
| 1245 | |||
| 1246 | /* Now create the intent. */ | ||
| 1247 | intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE); | ||
| 1248 | |||
| 1249 | try | ||
| 1250 | { | ||
| 1251 | id = EmacsActivity.ACCEPT_DOCUMENT_TREE; | ||
| 1252 | activity.startActivityForResult (intent, id, null); | ||
| 1253 | rc.thing = Integer.valueOf (0); | ||
| 1254 | } | ||
| 1255 | catch (Exception e) | ||
| 1256 | { | ||
| 1257 | e.printStackTrace (); | ||
| 1258 | } | ||
| 1259 | |||
| 1260 | notify (); | ||
| 1261 | } | ||
| 1262 | } | ||
| 1263 | }; | ||
| 1264 | |||
| 1265 | syncRunnable (runnable); | ||
| 1266 | return rc.thing; | ||
| 1267 | } | ||
| 1268 | |||
| 1269 | /* Return an array of each tree provided by the document PROVIDER | ||
| 1270 | that Emacs has permission to access. | ||
| 1271 | |||
| 1272 | Value is an array if the provider really does exist, NULL | ||
| 1273 | otherwise. */ | ||
| 1274 | |||
| 1275 | public String[] | ||
| 1276 | getDocumentTrees (byte provider[]) | ||
| 1277 | { | ||
| 1278 | String providerName; | ||
| 1279 | List<String> treeList; | ||
| 1280 | List<UriPermission> permissions; | ||
| 1281 | Uri uri; | ||
| 1282 | |||
| 1283 | try | ||
| 1284 | { | ||
| 1285 | providerName = new String (provider, "ASCII"); | ||
| 1286 | } | ||
| 1287 | catch (UnsupportedEncodingException exception) | ||
| 1288 | { | ||
| 1289 | return null; | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1293 | treeList = new ArrayList<String> (); | ||
| 1294 | |||
| 1295 | for (UriPermission permission : permissions) | ||
| 1296 | { | ||
| 1297 | uri = permission.getUri (); | ||
| 1298 | |||
| 1299 | if (DocumentsContract.isTreeUri (uri) | ||
| 1300 | && uri.getAuthority ().equals (providerName) | ||
| 1301 | && permission.isReadPermission ()) | ||
| 1302 | /* Make sure the tree document ID is encoded. */ | ||
| 1303 | treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri))); | ||
| 1304 | } | ||
| 1305 | |||
| 1306 | return treeList.toArray (new String[0]); | ||
| 1307 | } | ||
| 1308 | |||
| 1309 | /* Decode the specified STRING into a String object using the UTF-8 | ||
| 1310 | format. If an exception is thrown, return null. */ | ||
| 1311 | |||
| 1312 | private String | ||
| 1313 | decodeFileName (byte[] string) | ||
| 1314 | { | ||
| 1315 | try | ||
| 1316 | { | ||
| 1317 | return new String (string, "UTF-8"); | ||
| 1318 | } | ||
| 1319 | catch (Exception e) /* UnsupportedEncodingException, etc. */ | ||
| 1320 | { | ||
| 1321 | ;; | ||
| 1322 | } | ||
| 1323 | |||
| 1324 | return null; | ||
| 1325 | } | ||
| 1326 | |||
| 1327 | /* Find the document ID of the file within TREE_URI designated by | ||
| 1328 | NAME. | ||
| 1329 | |||
| 1330 | NAME is a ``file name'' comprised of the display names of | ||
| 1331 | individual files. Each constituent component prior to the last | ||
| 1332 | must name a directory file within TREE_URI. | ||
| 1333 | |||
| 1334 | Upon success, return 0 or 1 (contingent upon whether or not the | ||
| 1335 | last component within NAME is a directory) and place the document | ||
| 1336 | ID of the named file in ID_RETURN[0]. | ||
| 1337 | |||
| 1338 | If the designated file can't be located, but each component of | ||
| 1339 | NAME up to the last component can and is a directory, return -2 | ||
| 1340 | and the ID of the last component located in ID_RETURN[0]; | ||
| 1341 | |||
| 1342 | If the designated file can't be located, return -1. */ | ||
| 1343 | |||
| 1344 | private int | ||
| 1345 | documentIdFromName (String tree_uri, byte name[], | ||
| 1346 | String[] id_return) | ||
| 1347 | { | ||
| 1348 | Uri uri, treeUri; | ||
| 1349 | String nameString, id, type; | ||
| 1350 | String[] components, projection; | ||
| 1351 | Cursor cursor; | ||
| 1352 | int column; | ||
| 1353 | |||
| 1354 | projection = new String[] { | ||
| 1355 | Document.COLUMN_DISPLAY_NAME, | ||
| 1356 | Document.COLUMN_DOCUMENT_ID, | ||
| 1357 | Document.COLUMN_MIME_TYPE, | ||
| 1358 | }; | ||
| 1359 | |||
| 1360 | /* Parse the URI identifying the tree first. */ | ||
| 1361 | uri = Uri.parse (tree_uri); | ||
| 1362 | |||
| 1363 | /* Next, decode NAME. */ | ||
| 1364 | nameString = decodeFileName (name); | ||
| 1365 | |||
| 1366 | /* Now, split NAME into its individual components. */ | ||
| 1367 | components = nameString.split ("/"); | ||
| 1368 | |||
| 1369 | /* Set id and type to the value at the root of the tree. */ | ||
| 1370 | type = id = null; | ||
| 1371 | |||
| 1372 | /* For each component... */ | ||
| 1373 | |||
| 1374 | for (String component : components) | ||
| 1375 | { | ||
| 1376 | /* Java split doesn't behave very much like strtok when it | ||
| 1377 | comes to trailing and leading delimiters... */ | ||
| 1378 | if (component.isEmpty ()) | ||
| 1379 | continue; | ||
| 1380 | |||
| 1381 | /* Create the tree URI for URI from ID if it exists, or the | ||
| 1382 | root otherwise. */ | ||
| 1383 | |||
| 1384 | if (id == null) | ||
| 1385 | id = DocumentsContract.getTreeDocumentId (uri); | ||
| 1386 | |||
| 1387 | treeUri | ||
| 1388 | = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); | ||
| 1389 | |||
| 1390 | /* Look for a file in this directory by the name of | ||
| 1391 | component. */ | ||
| 1392 | |||
| 1393 | try | ||
| 1394 | { | ||
| 1395 | cursor = resolver.query (treeUri, projection, | ||
| 1396 | (Document.COLUMN_DISPLAY_NAME | ||
| 1397 | + " = ?s"), | ||
| 1398 | new String[] { component, }, null); | ||
| 1399 | } | ||
| 1400 | catch (SecurityException exception) | ||
| 1401 | { | ||
| 1402 | /* A SecurityException can be thrown if Emacs doesn't have | ||
| 1403 | access to treeUri. */ | ||
| 1404 | return -1; | ||
| 1405 | } | ||
| 1406 | catch (Exception exception) | ||
| 1407 | { | ||
| 1408 | exception.printStackTrace (); | ||
| 1409 | |||
| 1410 | /* Why is this? */ | ||
| 1411 | return -1; | ||
| 1412 | } | ||
| 1413 | |||
| 1414 | if (cursor == null) | ||
| 1415 | return -1; | ||
| 1416 | |||
| 1417 | while (true) | ||
| 1418 | { | ||
| 1419 | /* Even though the query selects for a specific display | ||
| 1420 | name, some content providers nevertheless return every | ||
| 1421 | file within the directory. */ | ||
| 1422 | |||
| 1423 | if (!cursor.moveToNext ()) | ||
| 1424 | { | ||
| 1425 | cursor.close (); | ||
| 1426 | |||
| 1427 | /* If the last component considered is a | ||
| 1428 | directory... */ | ||
| 1429 | if ((type == null | ||
| 1430 | || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1431 | /* ... and type and id currently represent the | ||
| 1432 | penultimate component. */ | ||
| 1433 | && component == components[components.length - 1]) | ||
| 1434 | { | ||
| 1435 | /* The cursor is empty. In this case, return -2 | ||
| 1436 | and the current document ID (belonging to the | ||
| 1437 | previous component) in ID_RETURN. */ | ||
| 1438 | |||
| 1439 | id_return[0] = id; | ||
| 1440 | |||
| 1441 | /* But return -1 on the off chance that id is | ||
| 1442 | null. */ | ||
| 1443 | |||
| 1444 | if (id == null) | ||
| 1445 | return -1; | ||
| 1446 | |||
| 1447 | return -2; | ||
| 1448 | } | ||
| 1449 | |||
| 1450 | /* The last component found is not a directory, so | ||
| 1451 | return -1. */ | ||
| 1452 | return -1; | ||
| 1453 | } | ||
| 1454 | |||
| 1455 | /* So move CURSOR to a row with the right display | ||
| 1456 | name. */ | ||
| 1457 | |||
| 1458 | column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 1459 | |||
| 1460 | if (column < 0) | ||
| 1461 | continue; | ||
| 1462 | |||
| 1463 | try | ||
| 1464 | { | ||
| 1465 | nameString = cursor.getString (column); | ||
| 1466 | } | ||
| 1467 | catch (Exception exception) | ||
| 1468 | { | ||
| 1469 | cursor.close (); | ||
| 1470 | return -1; | ||
| 1471 | } | ||
| 1472 | |||
| 1473 | /* Break out of the loop only once a matching component is | ||
| 1474 | found. */ | ||
| 1475 | |||
| 1476 | if (nameString.equals (component)) | ||
| 1477 | break; | ||
| 1478 | } | ||
| 1479 | |||
| 1480 | /* Look for a column by the name of COLUMN_DOCUMENT_ID. */ | ||
| 1481 | |||
| 1482 | column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 1483 | |||
| 1484 | if (column < 0) | ||
| 1485 | { | ||
| 1486 | cursor.close (); | ||
| 1487 | return -1; | ||
| 1488 | } | ||
| 1489 | |||
| 1490 | /* Now replace ID with the document ID. */ | ||
| 1491 | |||
| 1492 | try | ||
| 1493 | { | ||
| 1494 | id = cursor.getString (column); | ||
| 1495 | } | ||
| 1496 | catch (Exception exception) | ||
| 1497 | { | ||
| 1498 | cursor.close (); | ||
| 1499 | return -1; | ||
| 1500 | } | ||
| 1501 | |||
| 1502 | /* If this is the last component, be sure to initialize the | ||
| 1503 | document type. */ | ||
| 1504 | |||
| 1505 | if (component == components[components.length - 1]) | ||
| 1506 | { | ||
| 1507 | column | ||
| 1508 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1509 | |||
| 1510 | if (column < 0) | ||
| 1511 | { | ||
| 1512 | cursor.close (); | ||
| 1513 | return -1; | ||
| 1514 | } | ||
| 1515 | |||
| 1516 | try | ||
| 1517 | { | ||
| 1518 | type = cursor.getString (column); | ||
| 1519 | } | ||
| 1520 | catch (Exception exception) | ||
| 1521 | { | ||
| 1522 | cursor.close (); | ||
| 1523 | return -1; | ||
| 1524 | } | ||
| 1525 | |||
| 1526 | /* Type may be NULL depending on how the Cursor returned | ||
| 1527 | is implemented. */ | ||
| 1528 | |||
| 1529 | if (type == null) | ||
| 1530 | { | ||
| 1531 | cursor.close (); | ||
| 1532 | return -1; | ||
| 1533 | } | ||
| 1534 | } | ||
| 1535 | |||
| 1536 | /* Now close the cursor. */ | ||
| 1537 | cursor.close (); | ||
| 1538 | |||
| 1539 | /* ID may have become NULL if the data is in an invalid | ||
| 1540 | format. */ | ||
| 1541 | if (id == null) | ||
| 1542 | return -1; | ||
| 1543 | } | ||
| 1544 | |||
| 1545 | /* Here, id is either NULL (meaning the same as TREE_URI), and | ||
| 1546 | type is either NULL (in which case id should also be NULL) or | ||
| 1547 | the MIME type of the file. */ | ||
| 1548 | |||
| 1549 | /* First return the ID. */ | ||
| 1550 | |||
| 1551 | if (id == null) | ||
| 1552 | id_return[0] = DocumentsContract.getTreeDocumentId (uri); | ||
| 1553 | else | ||
| 1554 | id_return[0] = id; | ||
| 1555 | |||
| 1556 | /* Next, return whether or not this is a directory. */ | ||
| 1557 | if (type == null || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1558 | return 1; | ||
| 1559 | |||
| 1560 | return 0; | ||
| 1561 | } | ||
| 1562 | |||
| 1563 | /* Return an encoded document URI representing a tree with the | ||
| 1564 | specified IDENTIFIER supplied by the authority AUTHORITY. | ||
| 1565 | |||
| 1566 | Return null instead if Emacs does not have permanent access | ||
| 1567 | to the specified document tree recorded on disk. */ | ||
| 1568 | |||
| 1569 | public String | ||
| 1570 | getTreeUri (String tree, String authority) | ||
| 1571 | { | ||
| 1572 | Uri uri, grantedUri; | ||
| 1573 | List<UriPermission> permissions; | ||
| 1574 | |||
| 1575 | /* First, build the URI. */ | ||
| 1576 | tree = Uri.decode (tree); | ||
| 1577 | uri = DocumentsContract.buildTreeDocumentUri (authority, tree); | ||
| 1578 | |||
| 1579 | /* Now, search for it within the list of persisted URI | ||
| 1580 | permissions. */ | ||
| 1581 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1582 | |||
| 1583 | for (UriPermission permission : permissions) | ||
| 1584 | { | ||
| 1585 | /* If the permission doesn't entitle Emacs to read access, | ||
| 1586 | skip it. */ | ||
| 1587 | |||
| 1588 | if (!permission.isReadPermission ()) | ||
| 1589 | continue; | ||
| 1590 | |||
| 1591 | grantedUri = permission.getUri (); | ||
| 1592 | |||
| 1593 | if (grantedUri.equals (uri)) | ||
| 1594 | return uri.toString (); | ||
| 1595 | } | ||
| 1596 | |||
| 1597 | /* Emacs doesn't have permission to access this tree URI. */ | ||
| 1598 | return null; | ||
| 1599 | } | ||
| 1600 | |||
| 1601 | /* Return file status for the document designated by the given | ||
| 1602 | DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document | ||
| 1603 | ID in URI itself. | ||
| 1604 | |||
| 1605 | Value is null upon failure, or an array of longs [MODE, SIZE, | ||
| 1606 | MTIM] upon success, where MODE contains the file type and access | ||
| 1607 | modes of the file as in `struct stat', SIZE is the size of the | ||
| 1608 | file in BYTES or -1 if not known, and MTIM is the time of the | ||
| 1609 | last modification to this file in milliseconds since 00:00, | ||
| 1610 | January 1st, 1970. */ | ||
| 1611 | |||
| 1612 | public long[] | ||
| 1613 | statDocument (String uri, String documentId) | ||
| 1614 | { | ||
| 1615 | Uri uriObject; | ||
| 1616 | String[] projection; | ||
| 1617 | long[] stat; | ||
| 1618 | int index; | ||
| 1619 | long tem; | ||
| 1620 | String tem1; | ||
| 1621 | Cursor cursor; | ||
| 1622 | |||
| 1623 | uriObject = Uri.parse (uri); | ||
| 1624 | |||
| 1625 | if (documentId == null) | ||
| 1626 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1627 | |||
| 1628 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 1629 | authority. */ | ||
| 1630 | |||
| 1631 | uriObject | ||
| 1632 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 1633 | |||
| 1634 | /* Now stat this document. */ | ||
| 1635 | |||
| 1636 | projection = new String[] { | ||
| 1637 | Document.COLUMN_FLAGS, | ||
| 1638 | Document.COLUMN_LAST_MODIFIED, | ||
| 1639 | Document.COLUMN_MIME_TYPE, | ||
| 1640 | Document.COLUMN_SIZE, | ||
| 1641 | }; | ||
| 1642 | |||
| 1643 | try | ||
| 1644 | { | ||
| 1645 | cursor = resolver.query (uriObject, projection, null, | ||
| 1646 | null, null); | ||
| 1647 | } | ||
| 1648 | catch (SecurityException exception) | ||
| 1649 | { | ||
| 1650 | /* A SecurityException can be thrown if Emacs doesn't have | ||
| 1651 | access to uriObject. */ | ||
| 1652 | return null; | ||
| 1653 | } | ||
| 1654 | catch (UnsupportedOperationException exception) | ||
| 1655 | { | ||
| 1656 | exception.printStackTrace (); | ||
| 1657 | |||
| 1658 | /* Why is this? */ | ||
| 1659 | return null; | ||
| 1660 | } | ||
| 1661 | |||
| 1662 | if (cursor == null || !cursor.moveToFirst ()) | ||
| 1663 | return null; | ||
| 1664 | |||
| 1665 | /* Create the array of file status. */ | ||
| 1666 | stat = new long[3]; | ||
| 1667 | |||
| 1668 | try | ||
| 1669 | { | ||
| 1670 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1671 | if (index < 0) | ||
| 1672 | return null; | ||
| 1673 | |||
| 1674 | tem = cursor.getInt (index); | ||
| 1675 | |||
| 1676 | stat[0] |= S_IRUSR; | ||
| 1677 | if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) | ||
| 1678 | stat[0] |= S_IWUSR; | ||
| 1679 | |||
| 1680 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N | ||
| 1681 | && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) | ||
| 1682 | stat[0] |= S_IFCHR; | ||
| 1683 | |||
| 1684 | index = cursor.getColumnIndex (Document.COLUMN_SIZE); | ||
| 1685 | if (index < 0) | ||
| 1686 | return null; | ||
| 1687 | |||
| 1688 | if (cursor.isNull (index)) | ||
| 1689 | stat[1] = -1; /* The size is unknown. */ | ||
| 1690 | else | ||
| 1691 | stat[1] = cursor.getLong (index); | ||
| 1692 | |||
| 1693 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1694 | if (index < 0) | ||
| 1695 | return null; | ||
| 1696 | |||
| 1697 | tem1 = cursor.getString (index); | ||
| 1698 | |||
| 1699 | /* Check if this is a directory file. */ | ||
| 1700 | if (tem1.equals (Document.MIME_TYPE_DIR) | ||
| 1701 | /* Files shouldn't be specials and directories at the same | ||
| 1702 | time, but Android doesn't forbid document providers | ||
| 1703 | from returning this information. */ | ||
| 1704 | && (stat[0] & S_IFCHR) == 0) | ||
| 1705 | /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, | ||
| 1706 | just assume they're writable. */ | ||
| 1707 | stat[0] |= S_IFDIR | S_IWUSR; | ||
| 1708 | |||
| 1709 | /* If this file is neither a character special nor a | ||
| 1710 | directory, indicate that it's a regular file. */ | ||
| 1711 | |||
| 1712 | if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) | ||
| 1713 | stat[0] |= S_IFREG; | ||
| 1714 | |||
| 1715 | index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); | ||
| 1716 | |||
| 1717 | if (index >= 0 && !cursor.isNull (index)) | ||
| 1718 | { | ||
| 1719 | /* Content providers are allowed to not provide mtime. */ | ||
| 1720 | tem = cursor.getLong (index); | ||
| 1721 | stat[2] = tem; | ||
| 1722 | } | ||
| 1723 | } | ||
| 1724 | catch (Exception exception) | ||
| 1725 | { | ||
| 1726 | /* Whether or not type errors cause exceptions to be signaled | ||
| 1727 | is defined ``by the implementation of Cursor'', whatever | ||
| 1728 | that means. */ | ||
| 1729 | exception.printStackTrace (); | ||
| 1730 | return null; | ||
| 1731 | } | ||
| 1732 | |||
| 1733 | return stat; | ||
| 1734 | } | ||
| 1735 | |||
| 1736 | /* Find out whether Emacs has access to the document designated by | ||
| 1737 | the specified DOCUMENTID within the tree URI. If DOCUMENTID is | ||
| 1738 | NULL, use the document ID in URI itself. | ||
| 1739 | |||
| 1740 | If WRITABLE, also check that the file is writable, which is true | ||
| 1741 | if it is either a directory or its flags contains | ||
| 1742 | FLAG_SUPPORTS_WRITE. | ||
| 1743 | |||
| 1744 | Value is 0 if the file is accessible, and one of the following if | ||
| 1745 | not: | ||
| 1746 | |||
| 1747 | -1, if the file does not exist. | ||
| 1748 | -2, upon a security exception or if WRITABLE the file | ||
| 1749 | is not writable. | ||
| 1750 | -3, upon any other error. */ | ||
| 1751 | |||
| 1752 | public int | ||
| 1753 | accessDocument (String uri, String documentId, boolean writable) | ||
| 1754 | { | ||
| 1755 | Uri uriObject; | ||
| 1756 | String[] projection; | ||
| 1757 | int tem, index; | ||
| 1758 | String tem1; | ||
| 1759 | Cursor cursor; | ||
| 1760 | |||
| 1761 | uriObject = Uri.parse (uri); | ||
| 1762 | |||
| 1763 | if (documentId == null) | ||
| 1764 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1765 | |||
| 1766 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 1767 | authority. */ | ||
| 1768 | |||
| 1769 | uriObject | ||
| 1770 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 1771 | |||
| 1772 | /* Now stat this document. */ | ||
| 1773 | |||
| 1774 | projection = new String[] { | ||
| 1775 | Document.COLUMN_FLAGS, | ||
| 1776 | Document.COLUMN_MIME_TYPE, | ||
| 1777 | }; | ||
| 1778 | |||
| 1779 | try | ||
| 1780 | { | ||
| 1781 | cursor = resolver.query (uriObject, projection, null, | ||
| 1782 | null, null); | ||
| 1783 | } | ||
| 1784 | catch (SecurityException exception) | ||
| 1785 | { | ||
| 1786 | /* A SecurityException can be thrown if Emacs doesn't have | ||
| 1787 | access to uriObject. */ | ||
| 1788 | return -2; | ||
| 1789 | } | ||
| 1790 | catch (UnsupportedOperationException exception) | ||
| 1791 | { | ||
| 1792 | exception.printStackTrace (); | ||
| 1793 | |||
| 1794 | /* Why is this? */ | ||
| 1795 | return -3; | ||
| 1796 | } | ||
| 1797 | |||
| 1798 | if (cursor == null || !cursor.moveToFirst ()) | ||
| 1799 | return -1; | ||
| 1800 | |||
| 1801 | if (!writable) | ||
| 1802 | return 0; | ||
| 1803 | |||
| 1804 | try | ||
| 1805 | { | ||
| 1806 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1807 | if (index < 0) | ||
| 1808 | return -3; | ||
| 1809 | |||
| 1810 | /* Get the type of this file to check if it's a directory. */ | ||
| 1811 | tem1 = cursor.getString (index); | ||
| 1812 | |||
| 1813 | /* Check if this is a directory file. */ | ||
| 1814 | if (tem1.equals (Document.MIME_TYPE_DIR)) | ||
| 1815 | { | ||
| 1816 | /* If so, don't check for FLAG_SUPPORTS_WRITE. | ||
| 1817 | Check for FLAG_DIR_SUPPORTS_CREATE instead. */ | ||
| 1818 | |||
| 1819 | if (!writable) | ||
| 1820 | return 0; | ||
| 1821 | |||
| 1822 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1823 | if (index < 0) | ||
| 1824 | return -3; | ||
| 1825 | |||
| 1826 | tem = cursor.getInt (index); | ||
| 1827 | if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) | ||
| 1828 | return -3; | ||
| 1829 | |||
| 1830 | return 0; | ||
| 1831 | } | ||
| 1832 | |||
| 1833 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1834 | if (index < 0) | ||
| 1835 | return -3; | ||
| 1836 | |||
| 1837 | tem = cursor.getInt (index); | ||
| 1838 | if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) | ||
| 1839 | return -3; | ||
| 1840 | } | ||
| 1841 | catch (Exception exception) | ||
| 1842 | { | ||
| 1843 | /* Whether or not type errors cause exceptions to be signaled | ||
| 1844 | is defined ``by the implementation of Cursor'', whatever | ||
| 1845 | that means. */ | ||
| 1846 | exception.printStackTrace (); | ||
| 1847 | return -3; | ||
| 1848 | } | ||
| 1849 | |||
| 1850 | return 0; | ||
| 1851 | } | ||
| 1852 | |||
| 1853 | /* Open a cursor representing each entry within the directory | ||
| 1854 | designated by the specified DOCUMENTID within the tree URI. | ||
| 1855 | |||
| 1856 | If DOCUMENTID is NULL, use the document ID within URI itself. | ||
| 1857 | Value is NULL upon failure. */ | ||
| 1858 | |||
| 1859 | public Cursor | ||
| 1860 | openDocumentDirectory (String uri, String documentId) | ||
| 1861 | { | ||
| 1862 | Uri uriObject; | ||
| 1863 | Cursor cursor; | ||
| 1864 | String projection[]; | ||
| 1865 | |||
| 1866 | uriObject = Uri.parse (uri); | ||
| 1867 | |||
| 1868 | /* If documentId is not set, use the document ID of the tree URI | ||
| 1869 | itself. */ | ||
| 1870 | |||
| 1871 | if (documentId == null) | ||
| 1872 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1873 | |||
| 1874 | /* Build a URI representing each directory entry within | ||
| 1875 | DOCUMENTID. */ | ||
| 1876 | |||
| 1877 | uriObject | ||
| 1878 | = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, | ||
| 1879 | documentId); | ||
| 1880 | |||
| 1881 | projection = new String [] { | ||
| 1882 | Document.COLUMN_DISPLAY_NAME, | ||
| 1883 | Document.COLUMN_MIME_TYPE, | ||
| 1884 | }; | ||
| 1885 | |||
| 1886 | try | ||
| 1887 | { | ||
| 1888 | cursor = resolver.query (uriObject, projection, null, null, | ||
| 1889 | null); | ||
| 1890 | } | ||
| 1891 | catch (SecurityException exception) | ||
| 1892 | { | ||
| 1893 | /* A SecurityException can be thrown if Emacs doesn't have | ||
| 1894 | access to uriObject. */ | ||
| 1895 | return null; | ||
| 1896 | } | ||
| 1897 | catch (UnsupportedOperationException exception) | ||
| 1898 | { | ||
| 1899 | exception.printStackTrace (); | ||
| 1900 | |||
| 1901 | /* Why is this? */ | ||
| 1902 | return null; | ||
| 1903 | } | ||
| 1904 | |||
| 1905 | /* Return the cursor. */ | ||
| 1906 | return cursor; | ||
| 1907 | } | ||
| 1908 | |||
| 1909 | /* Read a single directory entry from the specified CURSOR. Return | ||
| 1910 | NULL if at the end of the directory stream, and a directory entry | ||
| 1911 | with `d_name' set to NULL if an error occurs. */ | ||
| 1912 | |||
| 1913 | public EmacsDirectoryEntry | ||
| 1914 | readDirectoryEntry (Cursor cursor) | ||
| 1915 | { | ||
| 1916 | EmacsDirectoryEntry entry; | ||
| 1917 | int index; | ||
| 1918 | String name, type; | ||
| 1919 | |||
| 1920 | entry = new EmacsDirectoryEntry (); | ||
| 1921 | |||
| 1922 | while (true) | ||
| 1923 | { | ||
| 1924 | if (!cursor.moveToNext ()) | ||
| 1925 | return null; | ||
| 1926 | |||
| 1927 | /* First, retrieve the display name. */ | ||
| 1928 | index = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 1929 | |||
| 1930 | if (index < 0) | ||
| 1931 | /* Return an invalid directory entry upon failure. */ | ||
| 1932 | return entry; | ||
| 1933 | |||
| 1934 | try | ||
| 1935 | { | ||
| 1936 | name = cursor.getString (index); | ||
| 1937 | } | ||
| 1938 | catch (Exception exception) | ||
| 1939 | { | ||
| 1940 | return entry; | ||
| 1941 | } | ||
| 1942 | |||
| 1943 | /* Skip this entry if its name cannot be represented. */ | ||
| 1944 | |||
| 1945 | if (name.equals ("..") || name.equals (".") || name.contains ("/")) | ||
| 1946 | continue; | ||
| 1947 | |||
| 1948 | /* Now, look for its type. */ | ||
| 1949 | |||
| 1950 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1951 | |||
| 1952 | if (index < 0) | ||
| 1953 | /* Return an invalid directory entry upon failure. */ | ||
| 1954 | return entry; | ||
| 1955 | |||
| 1956 | try | ||
| 1957 | { | ||
| 1958 | type = cursor.getString (index); | ||
| 1959 | } | ||
| 1960 | catch (Exception exception) | ||
| 1961 | { | ||
| 1962 | return entry; | ||
| 1963 | } | ||
| 1964 | |||
| 1965 | if (type.equals (Document.MIME_TYPE_DIR)) | ||
| 1966 | entry.d_type = 1; | ||
| 1967 | entry.d_name = name; | ||
| 1968 | return entry; | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | /* Not reached. */ | ||
| 1972 | } | ||
| 1973 | |||
| 1974 | /* Open a file descriptor for a file document designated by | ||
| 1975 | DOCUMENTID within the document tree identified by URI. If | ||
| 1976 | TRUNCATE and the document already exists, truncate its contents | ||
| 1977 | before returning. | ||
| 1978 | |||
| 1979 | On Android 9.0 and earlier, always open the document in | ||
| 1980 | ``read-write'' mode; this instructs the document provider to | ||
| 1981 | return a seekable file that is stored on disk and returns correct | ||
| 1982 | file status. | ||
| 1983 | |||
| 1984 | Under newer versions of Android, open the document in a | ||
| 1985 | non-writable mode if WRITE is false. This is possible because | ||
| 1986 | these versions allow Emacs to explicitly request a seekable | ||
| 1987 | on-disk file. | ||
| 1988 | |||
| 1989 | Value is NULL upon failure or a parcel file descriptor upon | ||
| 1990 | success. Call `ParcelFileDescriptor.close' on this file | ||
| 1991 | descriptor instead of using the `close' system call. */ | ||
| 1992 | |||
| 1993 | public ParcelFileDescriptor | ||
| 1994 | openDocument (String uri, String documentId, boolean write, | ||
| 1995 | boolean truncate) | ||
| 1996 | { | ||
| 1997 | Uri treeUri, documentUri; | ||
| 1998 | String mode; | ||
| 1999 | ParcelFileDescriptor fileDescriptor; | ||
| 2000 | |||
| 2001 | treeUri = Uri.parse (uri); | ||
| 2002 | |||
| 2003 | /* documentId must be set for this request, since it doesn't make | ||
| 2004 | sense to ``open'' the root of the directory tree. */ | ||
| 2005 | |||
| 2006 | documentUri | ||
| 2007 | = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); | ||
| 2008 | |||
| 2009 | try | ||
| 2010 | { | ||
| 2011 | if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) | ||
| 2012 | { | ||
| 2013 | /* Select the mode used to open the file. `rw' means open | ||
| 2014 | a stat-able file, while `rwt' means that and to | ||
| 2015 | truncate the file as well. */ | ||
| 2016 | |||
| 2017 | if (truncate) | ||
| 2018 | mode = "rwt"; | ||
| 2019 | else | ||
| 2020 | mode = "rw"; | ||
| 2021 | |||
| 2022 | fileDescriptor | ||
| 2023 | = resolver.openFileDescriptor (documentUri, mode, | ||
| 2024 | null); | ||
| 2025 | } | ||
| 2026 | else | ||
| 2027 | { | ||
| 2028 | /* Select the mode used to open the file. `openFile' | ||
| 2029 | below means always open a stat-able file. */ | ||
| 2030 | |||
| 2031 | if (truncate) | ||
| 2032 | /* Invalid mode! */ | ||
| 2033 | return null; | ||
| 2034 | else | ||
| 2035 | mode = "r"; | ||
| 2036 | |||
| 2037 | fileDescriptor = resolver.openFile (documentUri, mode, null); | ||
| 2038 | } | ||
| 2039 | } | ||
| 2040 | catch (Exception exception) | ||
| 2041 | { | ||
| 2042 | return null; | ||
| 2043 | } | ||
| 2044 | |||
| 2045 | return fileDescriptor; | ||
| 2046 | } | ||
| 2047 | |||
| 2048 | /* Create a new document with the given display NAME within the | ||
| 2049 | directory identified by DOCUMENTID inside the document tree | ||
| 2050 | designated by URI. | ||
| 2051 | |||
| 2052 | If DOCUMENTID is NULL, create the document inside the root of | ||
| 2053 | that tree. | ||
| 2054 | |||
| 2055 | Return the document ID of the new file upon success, NULL | ||
| 2056 | otherwise. */ | ||
| 2057 | |||
| 2058 | public String | ||
| 2059 | createDocument (String uri, String documentId, String name) | ||
| 2060 | { | ||
| 2061 | String mimeType, separator, mime, extension; | ||
| 2062 | int index; | ||
| 2063 | MimeTypeMap singleton; | ||
| 2064 | Uri directoryUri, docUri; | ||
| 2065 | |||
| 2066 | /* Try to get the MIME type for this document. | ||
| 2067 | Default to ``application/octet-stream''. */ | ||
| 2068 | |||
| 2069 | mimeType = "application/octet-stream"; | ||
| 2070 | |||
| 2071 | /* Abuse WebView stuff to get the file's MIME type. */ | ||
| 2072 | |||
| 2073 | index = name.lastIndexOf ('.'); | ||
| 2074 | |||
| 2075 | if (index > 0) | ||
| 2076 | { | ||
| 2077 | singleton = MimeTypeMap.getSingleton (); | ||
| 2078 | extension = name.substring (index + 1); | ||
| 2079 | mime = singleton.getMimeTypeFromExtension (extension); | ||
| 2080 | |||
| 2081 | if (mime != null) | ||
| 2082 | mimeType = mime; | ||
| 2083 | } | ||
| 2084 | |||
| 2085 | /* Now parse URI. */ | ||
| 2086 | directoryUri = Uri.parse (uri); | ||
| 2087 | |||
| 2088 | if (documentId == null) | ||
| 2089 | documentId = DocumentsContract.getTreeDocumentId (directoryUri); | ||
| 2090 | |||
| 2091 | /* And build a file URI referring to the directory. */ | ||
| 2092 | |||
| 2093 | directoryUri | ||
| 2094 | = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, | ||
| 2095 | documentId); | ||
| 2096 | |||
| 2097 | try | ||
| 2098 | { | ||
| 2099 | docUri = DocumentsContract.createDocument (resolver, | ||
| 2100 | directoryUri, | ||
| 2101 | mimeType, name); | ||
| 2102 | |||
| 2103 | if (docUri == null) | ||
| 2104 | return null; | ||
| 2105 | |||
| 2106 | /* Return the ID of the new document. */ | ||
| 2107 | return DocumentsContract.getDocumentId (docUri); | ||
| 2108 | } | ||
| 2109 | catch (Exception exception) | ||
| 2110 | { | ||
| 2111 | exception.printStackTrace (); | ||
| 2112 | } | ||
| 2113 | |||
| 2114 | return null; | ||
| 2115 | } | ||
| 1099 | }; | 2116 | }; |