diff options
Diffstat (limited to 'java')
-rw-r--r-- | java/AndroidManifest.xml.in | 137 | ||||
-rw-r--r-- | java/INSTALL | 34 | ||||
-rw-r--r-- | java/Makefile.in | 13 | ||||
-rwxr-xr-x | java/debug.sh | 13 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsActivity.java | 129 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsContextMenu.java | 17 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsDesktopNotification.java | 202 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsNative.java | 22 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsOpenActivity.java | 43 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsPreferencesActivity.java | 5 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsService.java | 254 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsView.java | 33 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsWindow.java | 159 | ||||
-rw-r--r-- | java/org/gnu/emacs/EmacsWindowAttachmentManager.java | 9 |
14 files changed, 859 insertions, 211 deletions
diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index b18446bece0..563914fb02c 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -64,6 +64,132 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.ACCEPT_HANDOVER" /> + <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> + <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" /> + <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> + <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> + <uses-permission android:name="android.permission.BODY_SENSORS" /> + <uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" /> + <uses-permission android:name="android.permission.CALL_PHONE" /> + <uses-permission android:name="android.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD" /> + <uses-permission android:name="android.permission.GET_ACCOUNTS" /> + <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES" /> + <uses-permission android:name="android.permission.LOADER_USAGE_STATS" /> + <uses-permission android:name="android.permission.MANAGE_IPSEC_TUNNELS" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA" /> + <uses-permission android:name="android.permission.MANAGE_ONGOING_CALLS" /> + <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" /> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> + <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> + <uses-permission android:name="android.permission.READ_CALL_LOG" /> + <uses-permission android:name="android.permission.READ_CELL_BROADCASTS" /> + <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> + <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> + <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> + <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> + <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> + <uses-permission android:name="android.permission.READ_SMS" /> + <uses-permission android:name="android.permission.RECEIVE_MMS" /> + <uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" /> + <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> + <uses-permission android:name="android.permission.SMS_FINANCIAL_TRANSACTIONS" /> + <uses-permission android:name="android.permission.TURN_SCREEN_ON" /> + <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> + <uses-permission android:name="android.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER" /> + <uses-permission android:name="android.permission.USE_SIP" /> + <uses-permission android:name="android.permission.UWB_RANGING" /> + <uses-permission android:name="android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS" /> + <uses-permission android:name="android.permission.WRITE_CALL_LOG" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + + <uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" /> + <uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" /> + <uses-permission android:name="android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE" /> + <uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" /> + <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" /> + <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> + <uses-permission android:name="android.permission.BLUETOOTH" /> + <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> + <uses-permission android:name="android.permission.BROADCAST_STICKY" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> + <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> + <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> + <uses-permission android:name="android.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS" /> + <uses-permission android:name="android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS" /> + <uses-permission android:name="android.permission.CREDENTIAL_MANAGER_SET_ORIGIN" /> + <uses-permission android:name="android.permission.DELIVER_COMPANION_MESSAGES" /> + <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" /> + <uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> + <uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" /> + <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> + <uses-permission android:name="android.permission.FLASHLIGHT" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> + <uses-permission android:name="android.permission.GET_PACKAGE_SIZE" /> + <uses-permission android:name="android.permission.GET_TASKS" /> + <uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" /> + <uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" /> + <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" /> + <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> + <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" /> + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> + <uses-permission android:name="android.permission.NFC_PREFERRED_PAYMENT_INFO" /> + <uses-permission android:name="android.permission.NFC_TRANSACTION_EVENT" /> + <uses-permission android:name="android.permission.PERSISTENT_ACTIVITY" /> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" /> + <uses-permission android:name="android.permission.READ_INSTALL_SESSIONS" /> + <uses-permission android:name="android.permission.READ_NEARBY_STREAMING_POLICY" /> + <uses-permission android:name="android.permission.READ_PROFILE" /> + <uses-permission android:name="android.permission.READ_SOCIAL_STREAM" /> + <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> + <uses-permission android:name="android.permission.READ_SYNC_STATS" /> + <uses-permission android:name="android.permission.READ_USER_DICTIONARY" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <uses-permission android:name="android.permission.REORDER_TASKS" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_GLASSES" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" /> + <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> + <uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" /> + <uses-permission android:name="android.permission.REQUEST_PASSWORD_COMPLEXITY" /> + <uses-permission android:name="android.permission.RESTART_PACKAGES" /> + <uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" /> + <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" /> + <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_READ" /> + <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_WRITE" /> + <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> + <uses-permission android:name="android.permission.USE_BIOMETRIC" /> + <uses-permission android:name="android.permission.USE_CREDENTIALS" /> + <uses-permission android:name="android.permission.USE_EXACT_ALARM" /> + <uses-permission android:name="android.permission.USE_FINGERPRINT" /> + <uses-permission android:name="android.permission.WRITE_PROFILE" /> + <uses-permission android:name="android.permission.WRITE_SMS" /> + <uses-permission android:name="android.permission.WRITE_SOCIAL_STREAM" /> + <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" /> + <!-- This is required on Android 11 or later to access /sdcard. --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> @@ -92,6 +218,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. --> <activity android:name="org.gnu.emacs.EmacsActivity" android:launchMode="singleInstance" + android:taskAffinity="emacs.primary_frame" android:windowSoftInputMode="adjustResize" android:exported="true" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|locale|fontScale"> @@ -103,7 +230,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. --> </activity> <activity android:name="org.gnu.emacs.EmacsOpenActivity" - android:taskAffinity="open.dialog" + android:taskAffinity="emacs.open_dialog" android:excludeFromRecents="true" android:exported="true"> @@ -147,6 +274,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. --> </activity> <activity android:name="org.gnu.emacs.EmacsMultitaskActivity" + android:taskAffinity="emacs.secondary_frame" android:windowSoftInputMode="adjustResize" android:exported="true" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|locale|fontScale"/> @@ -190,6 +318,13 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. --> </intent-filter> </provider> + <receiver android:name=".EmacsDesktopNotification$CancellationReceiver" + android:exported="false"> + <intent-filter> + <action android:name="org.gnu.emacs.DISMISSED" /> + </intent-filter> + </receiver> + <service android:name="org.gnu.emacs.EmacsService" android:directBootAware="false" android:enabled="true" diff --git a/java/INSTALL b/java/INSTALL index 175ff2826b2..f1063b40c25 100644 --- a/java/INSTALL +++ b/java/INSTALL @@ -166,25 +166,21 @@ than a compressed package for a newer version of Android. BUILDING C++ DEPENDENCIES -With a new version of the NDK, dependencies containing C++ code should -build without any further configuration. However, older versions -require that you use the ``make_standalone_toolchain.py'' script in -the NDK distribution to create a ``standalone toolchain'', and use -that instead, in order for C++ headers to be found. - -See https://developer.android.com/ndk/guides/standalone_toolchain for -more details; when a ``standalone toolchain'' is specified, the -configure script will try to determine the location of the C++ -compiler based on the C compiler specified. If that automatic -detection does not work, you can specify a C++ compiler yourself, like -so: - - ./configure --with-ndk-cxx=/path/to/toolchain/bin/i686-linux-android-g++ - -Some versions of the NDK have a bug, where GCC fails to locate -``stddef.h'' after being copied to a standalone toolchain. To work -around this problem (which normally exhibits itself when building C++ -code), add: +In normal circumstances, Emacs should automatically detect and configure +one of the C++ standard libraries part of the NDK when such a library is +required to build a dependency specified under `--with-ndk-path'. + +Nevertheless, this process is not infalliable, and with certain versions +of the NDK is liable to fail to locate a C++ compiler, requiring that +you run the `make_standalone_toolchain.py' script in the NDK +distribution to create a ``standalone toolchain'' and substitute the +same for the regular compiler toolchain. See +https://developer.android.com/ndk/guides/standalone_toolchain for +further details. + +Some versions of the NDK that ship GCC 4.9.x exhibit a bug where the +compiler cannot locate `stddef.h' after being copied to a standalone +toolchain. To work around this problem, add: -isystem /path/to/toolchain/include/c++/4.9.x diff --git a/java/Makefile.in b/java/Makefile.in index 60bd2ea086b..c23b52ed44e 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -256,15 +256,15 @@ install_temp/assets/build_info: install_temp emacs.apk-in: install_temp install_temp/assets/directory-tree \ AndroidManifest.xml install_temp/assets/version \ - install_temp/assets/build_info -# Package everything. Specifying the assets on this command line is -# necessary for AAssetManager_getNextFileName to work on old versions -# of Android. Make sure not to generate R.java, as it's already been -# generated. + install_temp/assets/build_info classes.dex +# Package everything. Redirect the generated R.java to install_temp, as +# it must already have been generated as a prerequisite of +# classes.dex's. $(AM_V_AAPT) $(AAPT) p -I "$(ANDROID_JAR)" -F $@ \ -f -M AndroidManifest.xml $(AAPT_ASSET_ARGS) \ -A install_temp/assets \ -S $(top_srcdir)/java/res -J install_temp + $(AM_V_SILENT) $(AAPT) a $@ classes.dex $(AM_V_SILENT) pushd install_temp &> /dev/null; \ $(AAPT) add ../$@ `find lib -type f`; \ popd &> /dev/null @@ -311,10 +311,9 @@ classes.dex: $(CLASS_FILES) .PHONY: clean maintainer-clean -$(APK_NAME): classes.dex emacs.apk-in $(srcdir)/emacs.keystore +$(APK_NAME): emacs.apk-in $(srcdir)/emacs.keystore $(AM_V_GEN) $(AM_V_SILENT) cp -f emacs.apk-in $@.unaligned - $(AM_V_SILENT) $(AAPT) add $@.unaligned classes.dex $(AM_V_SILENT) $(JARSIGNER) $(SIGN_EMACS) $@.unaligned "Emacs keystore" $(AM_V_SILENT) $(ZIPALIGN) -f 4 $@.unaligned $@ # Signing must happen after alignment! diff --git a/java/debug.sh b/java/debug.sh index 8fc03d014cf..c5d40141355 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -104,13 +104,14 @@ if [ -z "$devices" ]; then exit 1 fi -if [ -z $device ]; then - device=$devices +if [ `wc -w <<< "$devices"` -gt 1 ] && [ -z $device ]; then + echo "Multiple devices are available. Please specify one with" + echo "the option --device and try again." + exit 1 fi -if [ `wc -w <<< "$devices"` -gt 1 ] && [ -z device ]; then - echo "Multiple devices are available. Please pick one using" - echo "--device and try again." +if [ -z $device ]; then + device=$devices fi echo "Looking for $package on device $device" @@ -189,6 +190,8 @@ if [ "$attach_existing" != "yes" ]; then package_pids=`awk -f tmp.awk <<< $package_pids` fi +rm tmp.awk + pid=$package_pids num_pids=`wc -w <<< "$package_pids"` diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 3237f650240..e380b7bfc2a 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -20,9 +20,12 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ package org.gnu.emacs; import java.lang.IllegalStateException; + import java.util.List; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + import android.app.Activity; import android.content.ContentResolver; @@ -31,6 +34,7 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.util.Log; @@ -78,13 +82,16 @@ public class EmacsActivity extends Activity /* The last context menu to be closed. */ private static Menu lastClosedMenu; + /* The time of the most recent call to onStop. */ + private static long timeOfLastInteraction; + static { focusedActivities = new ArrayList<EmacsActivity> (); }; public static void - invalidateFocus1 (EmacsWindow window) + invalidateFocus1 (EmacsWindow window, boolean resetWhenChildless) { if (window.view.isFocused ()) focusedWindow = window; @@ -92,12 +99,23 @@ public class EmacsActivity extends Activity synchronized (window.children) { for (EmacsWindow child : window.children) - invalidateFocus1 (child); + invalidateFocus1 (child, false); + + /* If no focused window was previously detected among WINDOW's + children and RESETWHENCHILDLESS is set (implying it is a + toplevel window), request that it be focused, to avoid + creating a situation where no windows exist focused or can be + transferred the input focus by user action. */ + if (focusedWindow == null && resetWhenChildless) + { + window.view.requestFocus (); + focusedWindow = window; + } } } public static void - invalidateFocus () + invalidateFocus (int whence) { EmacsWindow oldFocus; @@ -110,7 +128,7 @@ public class EmacsActivity extends Activity for (EmacsActivity activity : focusedActivities) { if (activity.window != null) - invalidateFocus1 (activity.window); + invalidateFocus1 (activity.window, focusedWindow == null); } /* Send focus in- and out- events to the previous and current @@ -144,7 +162,7 @@ public class EmacsActivity extends Activity layout.removeView (window.view); window = null; - invalidateFocus (); + invalidateFocus (0); } } @@ -172,8 +190,17 @@ public class EmacsActivity extends Activity if (isPaused) window.noticeIconified (); - /* Invalidate the focus. */ - invalidateFocus (); + /* Invalidate the focus. Since attachWindow may be called from + either the main or the UI thread, post this to the UI thread. */ + + runOnUiThread (new Runnable () { + @Override + public void + run () + { + invalidateFocus (1); + } + }); } @Override @@ -238,6 +265,10 @@ public class EmacsActivity extends Activity } super.onCreate (savedInstanceState); + + /* Call `onWindowFocusChanged' to read the focus state, which fails + to be called after an activity is recreated. */ + onWindowFocusChanged (false); } @Override @@ -249,6 +280,50 @@ public class EmacsActivity extends Activity @Override public final void + onStop () + { + timeOfLastInteraction = SystemClock.elapsedRealtime (); + + super.onStop (); + } + + /* Return whether the task is being finished in response to explicit + user action. That is to say, Activity.isFinished, but as + documented. */ + + public final boolean + isReallyFinishing () + { + long atime, dtime; + int hours; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + return isFinishing (); + + /* When the number of tasks retained in the recents list exceeds a + threshold, Android 7 and later so destroy activities in trimming + them from recents on the expiry of a timeout that isFinishing + returns true, in direct contradiction to the documentation. This + timeout is generally 6 hours, but admits of customization by + individual system distributors, so to err on the side of the + caution, the timeout Emacs applies is a more conservative figure + of 4 hours. */ + + if (timeOfLastInteraction == 0) + return isFinishing (); + + atime = timeOfLastInteraction; + + /* Compare atime with the current system time. */ + dtime = SystemClock.elapsedRealtime () - atime; + if (dtime + 1000000 < TimeUnit.HOURS.toMillis (4)) + return isFinishing (); + + return false; + } + + @Override + public final void onDestroy () { EmacsWindowAttachmentManager manager; @@ -259,9 +334,10 @@ public class EmacsActivity extends Activity /* The activity will die shortly hereafter. If there is a window attached, close it now. */ isMultitask = this instanceof EmacsMultitaskActivity; - manager.removeWindowConsumer (this, isMultitask || isFinishing ()); + manager.removeWindowConsumer (this, (isMultitask + || isReallyFinishing ())); focusedActivities.remove (this); - invalidateFocus (); + invalidateFocus (2); /* Remove this activity from the static field, lest it leak. */ if (lastFocusedActivity == this) @@ -274,9 +350,16 @@ public class EmacsActivity extends Activity public final void onWindowFocusChanged (boolean isFocused) { - if (isFocused && !focusedActivities.contains (this)) + /* At times and on certain versions of Android ISFOCUSED does not + reflect whether the window actually holds focus, so replace it + with the value of `hasWindowFocus'. */ + isFocused = hasWindowFocus (); + + if (isFocused) { - focusedActivities.add (this); + if (!focusedActivities.contains (this)) + focusedActivities.add (this); + lastFocusedActivity = this; /* Update the window insets as the focus change may have @@ -291,7 +374,7 @@ public class EmacsActivity extends Activity else focusedActivities.remove (this); - invalidateFocus (); + invalidateFocus (3); } @Override @@ -309,6 +392,7 @@ public class EmacsActivity extends Activity onResume () { isPaused = false; + timeOfLastInteraction = 0; EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); super.onResume (); @@ -433,6 +517,27 @@ public class EmacsActivity extends Activity syncFullscreenWith (window); } + @Override + public final void + onNewIntent (Intent intent) + { + String tag, action; + + /* This function is called when EmacsActivity is relaunched from a + notification. */ + + if (intent == null || EmacsService.SERVICE == null) + return; + + tag = intent.getStringExtra (EmacsDesktopNotification.NOTIFICATION_TAG); + action + = intent.getStringExtra (EmacsDesktopNotification.NOTIFICATION_ACTION); + + if (tag == null || action == null) + return; + + EmacsNative.sendNotificationAction (tag, action); + } @Override diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 17e6033377d..2bbf2a313d6 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -361,8 +361,23 @@ public final class EmacsContextMenu public Boolean call () { + boolean rc; + lastMenuEventSerial = serial; - return display1 (window, xPosition, yPosition); + rc = display1 (window, xPosition, yPosition); + + /* Android 3.0 to Android 7.0 perform duplicate calls to + onContextMenuClosed the second time a context menu is + dismissed. Since the second call after such a dismissal is + otherwise liable to prematurely cancel any context menu + displayed immediately afterwards, ignore calls received + within 150 milliseconds of this menu's being displayed. */ + + if (rc && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB + && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + wasSubmenuSelected = System.currentTimeMillis () - 150; + + return rc; } }); diff --git a/java/org/gnu/emacs/EmacsDesktopNotification.java b/java/org/gnu/emacs/EmacsDesktopNotification.java index fb35e3fea1f..72569631a8c 100644 --- a/java/org/gnu/emacs/EmacsDesktopNotification.java +++ b/java/org/gnu/emacs/EmacsDesktopNotification.java @@ -24,9 +24,12 @@ import android.app.NotificationManager; import android.app.NotificationChannel; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; + import android.os.Build; import android.widget.RemoteViews; @@ -44,6 +47,16 @@ import android.widget.RemoteViews; public final class EmacsDesktopNotification { + /* Intent tag for notification action data. */ + public static final String NOTIFICATION_ACTION = "emacs:notification_action"; + + /* Intent tag for notification IDs. */ + public static final String NOTIFICATION_TAG = "emacs:notification_tag"; + + /* Action ID assigned to the broadcast receiver which should be + notified of any notification's being dismissed. */ + public static final String NOTIFICATION_DISMISSED = "org.gnu.emacs.DISMISSED"; + /* The content of this desktop notification. */ public final String content; @@ -66,10 +79,20 @@ public final class EmacsDesktopNotification /* The importance of this notification's group. */ public final int importance; + /* Array of actions and their user-facing text to be offered by this + notification. */ + public final String[] actions, titles; + + /* Delay in miliseconds after which this notification should be + automatically dismissed. */ + public final long delay; + public EmacsDesktopNotification (String title, String content, String group, String tag, int icon, - int importance) + int importance, + String[] actions, String[] titles, + long delay) { this.content = content; this.title = title; @@ -77,12 +100,69 @@ public final class EmacsDesktopNotification this.tag = tag; this.icon = icon; this.importance = importance; + this.actions = actions; + this.titles = titles; + this.delay = delay; } /* Functions for displaying desktop notifications. */ + /* Insert each action in actions and titles into the notification + builder BUILDER, with pending intents created with CONTEXT holding + suitable metadata. */ + + @SuppressWarnings ("deprecation") + private void + insertActions (Context context, Notification.Builder builder) + { + int i; + PendingIntent pending; + Intent intent; + Notification.Action.Builder action; + + if (actions == null) + return; + + for (i = 0; i < actions.length; ++i) + { + /* Actions named default should not be displayed. */ + if (actions[i].equals ("default")) + continue; + + intent = new Intent (context, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + + /* Pending intents are specific to combinations of class, action + and data, but not information provided as extras. In order + that its target may be invoked with the action and tag set + below, generate a URL from those two elements and specify it + as the intent data, which ensures that the intent allocated + fully reflects the duo. */ + + intent.setData (new Uri.Builder ().scheme ("action") + .appendPath (tag).appendPath (actions[i]) + .build ()); + intent.putExtra (NOTIFICATION_ACTION, actions[i]); + intent.putExtra (NOTIFICATION_TAG, tag); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + pending = PendingIntent.getActivity (context, 0, intent, + PendingIntent.FLAG_IMMUTABLE); + else + pending = PendingIntent.getActivity (context, 0, intent, 0); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + action = new Notification.Action.Builder (0, titles[i], pending); + builder.addAction (action.build ()); + } + else + builder.addAction (0, titles[i], pending); + } + } + /* Internal helper for `display' executed on the main thread. */ @SuppressWarnings ("deprecation") /* Notification.Builder (Context). */ @@ -97,6 +177,7 @@ public final class EmacsDesktopNotification Intent intent; PendingIntent pending; int priority; + Notification.Builder builder; tem = context.getSystemService (Context.NOTIFICATION_SERVICE); manager = (NotificationManager) tem; @@ -108,13 +189,18 @@ public final class EmacsDesktopNotification (such as its importance) will be overridden. */ channel = new NotificationChannel (group, group, importance); manager.createNotificationChannel (channel); + builder = new Notification.Builder (context, group); - /* Create a notification object and display it. */ - notification = (new Notification.Builder (context, group) - .setContentTitle (title) - .setContentText (content) - .setSmallIcon (icon) - .build ()); + /* Create and configure a notification object and display + it. */ + + builder.setContentTitle (title); + builder.setContentText (content); + builder.setSmallIcon (icon); + builder.setTimeoutAfter (delay); + + insertActions (context, builder); + notification = builder.build (); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { @@ -122,31 +208,35 @@ public final class EmacsDesktopNotification distinct categories, but permit an importance to be assigned to each individual notification. */ - switch (importance) + builder = new Notification.Builder (context); + builder.setContentTitle (title); + builder.setContentText (content); + builder.setSmallIcon (icon); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - case 2: /* IMPORTANCE_LOW */ - default: - priority = Notification.PRIORITY_LOW; - break; - - case 3: /* IMPORTANCE_DEFAULT */ - priority = Notification.PRIORITY_DEFAULT; - break; - - case 4: /* IMPORTANCE_HIGH */ - priority = Notification.PRIORITY_HIGH; - break; + switch (importance) + { + case 2: /* IMPORTANCE_LOW */ + default: + priority = Notification.PRIORITY_LOW; + break; + + case 3: /* IMPORTANCE_DEFAULT */ + priority = Notification.PRIORITY_DEFAULT; + break; + + case 4: /* IMPORTANCE_HIGH */ + priority = Notification.PRIORITY_HIGH; + break; + } + + builder.setPriority (priority); + insertActions (context, builder); + notification = builder.build (); } - - notification = (new Notification.Builder (context) - .setContentTitle (title) - .setContentText (content) - .setSmallIcon (icon) - .setPriority (priority) - .build ()); - - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) - notification.priority = priority; + else + notification = builder.getNotification (); } else { @@ -170,6 +260,12 @@ public final class EmacsDesktopNotification intent = new Intent (context, EmacsActivity.class); intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData (new Uri.Builder () + .scheme ("action") + .appendPath (tag) + .build ()); + intent.putExtra (NOTIFICATION_ACTION, "default"); + intent.putExtra (NOTIFICATION_TAG, tag); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) pending = PendingIntent.getActivity (context, 0, intent, @@ -179,6 +275,25 @@ public final class EmacsDesktopNotification notification.contentIntent = pending; + /* Provide a cancellation intent to respond to notification + dismissals. */ + + intent = new Intent (context, CancellationReceiver.class); + intent.setAction (NOTIFICATION_DISMISSED); + intent.setPackage ("org.gnu.emacs"); + intent.setData (new Uri.Builder () + .scheme ("action") + .appendPath (tag) + .build ()); + intent.putExtra (NOTIFICATION_TAG, tag); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + pending = PendingIntent.getBroadcast (context, 0, intent, + PendingIntent.FLAG_IMMUTABLE); + else + pending = PendingIntent.getBroadcast (context, 0, intent, 0); + + notification.deleteIntent = pending; manager.notify (tag, 2, notification); } @@ -199,4 +314,31 @@ public final class EmacsDesktopNotification } }); } + + + + /* Broadcast receiver. This is something of a system-wide callback + arranged to be invoked whenever a notification posted by Emacs is + dismissed, in order to relay news of its dismissal to + androidselect.c and run or remove callbacks as appropriate. */ + + public static class CancellationReceiver extends BroadcastReceiver + { + @Override + public void + onReceive (Context context, Intent intent) + { + String tag, action; + + if (intent == null || EmacsService.SERVICE == null) + return; + + tag = intent.getStringExtra (NOTIFICATION_TAG); + + if (tag == null) + return; + + EmacsNative.sendNotificationDeleted (tag); + } + }; }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cd0e70923d1..654e94b1a7d 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -196,6 +196,12 @@ public final class EmacsNative public static native long sendDndText (short window, int x, int y, String text); + /* Send an ANDROID_NOTIFICATION_CANCELED event. */ + public static native void sendNotificationDeleted (String tag); + + /* Send an ANDROID_NOTIFICATION_ACTION event. */ + public static native void sendNotificationAction (String tag, String action); + /* Return the file name associated with the specified file descriptor, or NULL if there is none. */ public static native byte[] getProcName (int fd); @@ -275,7 +281,7 @@ public final class EmacsNative public static native int[] getSelection (short window); - /* Graphics functions used as a replacement for potentially buggy + /* Graphics functions used as replacements for potentially buggy Android APIs. */ public static native void blitRect (Bitmap src, Bitmap dest, int x1, @@ -283,7 +289,6 @@ public final class EmacsNative /* Increment the generation ID of the specified BITMAP, forcing its texture to be re-uploaded to the GPU. */ - public static native void notifyPixelsChanged (Bitmap bitmap); @@ -307,6 +312,13 @@ public final class EmacsNative in the process. */ public static native boolean ftruncate (int fd); + + /* Functions that assist in generating content file names. */ + + /* Calculate an 8 digit checksum for the byte array DISPLAYNAME + suitable for inclusion in a content file name. */ + public static native String displayNameHash (byte[] displayName); + static { /* Older versions of Android cannot link correctly with shared @@ -317,7 +329,9 @@ public final class EmacsNative Every time you add a new shared library dependency to Emacs, please add it here as well. */ - libraryDeps = new String[] { "png_emacs", "selinux_emacs", + libraryDeps = new String[] { "c++_shared", "gnustl_shared", + "stlport_shared", "gabi++_shared", + "png_emacs", "selinux_emacs", "crypto_emacs", "pcre_emacs", "packagelistparser_emacs", "gnutls_emacs", "gmp_emacs", @@ -325,7 +339,7 @@ public final class EmacsNative "tasn1_emacs", "hogweed_emacs", "jansson_emacs", "jpeg_emacs", "tiff_emacs", "xml2_emacs", - "icuuc_emacs", + "icuuc_emacs", "harfbuzz_emacs", "tree-sitter_emacs", }; for (String dependency : libraryDeps) diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 9ae1bf353dd..327a53bc417 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -252,7 +252,7 @@ public final class EmacsOpenActivity extends Activity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - content = EmacsService.buildContentName (uri); + content = EmacsService.buildContentName (uri, getContentResolver ()); return content; } @@ -423,6 +423,7 @@ public final class EmacsOpenActivity extends Activity /* Obtain the intent that started Emacs. */ intent = getIntent (); action = intent.getAction (); + resolver = getContentResolver (); if (action == null) { @@ -534,9 +535,19 @@ public final class EmacsOpenActivity extends Activity uri = intent.getParcelableExtra (Intent.EXTRA_STREAM); if ((scheme = uri.getScheme ()) != null - && scheme.equals ("content")) + && scheme.equals ("content") + && (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.KITKAT)) { - tem1 = EmacsService.buildContentName (uri); + tem1 = EmacsService.buildContentName (uri, resolver); + attachmentString = ("'(\"" + (tem1.replace ("\\", "\\\\") + .replace ("\"", "\\\"") + .replace ("$", "\\$")) + + "\")"); + } + else if (scheme != null && scheme.equals ("file")) + { + tem1 = uri.getPath (); attachmentString = ("'(\"" + (tem1.replace ("\\", "\\\\") .replace ("\"", "\\\"") .replace ("$", "\\$")) @@ -566,9 +577,22 @@ public final class EmacsOpenActivity extends Activity if (uri != null && (scheme = uri.getScheme ()) != null - && scheme.equals ("content")) + && scheme.equals ("content") + && (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.KITKAT)) + { + tem1 + = EmacsService.buildContentName (uri, resolver); + builder.append ("\""); + builder.append (tem1.replace ("\\", "\\\\") + .replace ("\"", "\\\"") + .replace ("$", "\\$")); + builder.append ("\""); + } + else if (scheme != null + && scheme.equals ("file")) { - tem1 = EmacsService.buildContentName (uri); + tem1 = uri.getPath (); builder.append ("\""); builder.append (tem1.replace ("\\", "\\\\") .replace ("\"", "\\\"") @@ -602,14 +626,19 @@ public final class EmacsOpenActivity extends Activity { fileName = null; - if (scheme.equals ("content")) + if (scheme.equals ("content") + /* Retrieving the native file descriptor of a + ParcelFileDescriptor requires Honeycomb, and + proceeding without this capability is pointless on + systems before KitKat, since Emacs doesn't support + opening content files on those. */ + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { /* This is one of the annoying Android ``content'' URIs. Most of the time, there is actually an underlying file, but it cannot be found without opening the file and doing readlink on its file descriptor in /proc/self/fd. */ - resolver = getContentResolver (); fd = null; try diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 330adbea223..766e2e11d46 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -38,8 +38,9 @@ import android.preference.*; option, which would not be possible otherwise, as there is no command line on Android. - Android provides a preferences activity, but it is deprecated. - Unfortunately, there is no alternative that looks the same way. */ + This file extends a deprecated preferences activity, but no suitable + alternative exists that is identical in appearance to system settings + forms. */ @SuppressWarnings ("deprecation") public class EmacsPreferencesActivity extends PreferenceActivity diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 5cb1ceca0aa..446cd26a3dd 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ package org.gnu.emacs; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -45,9 +46,11 @@ import android.view.KeyEvent; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ExtractedText; +import android.app.AlarmManager; import android.app.Notification; -import android.app.NotificationManager; import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.ClipboardManager; @@ -60,6 +63,7 @@ import android.content.UriPermission; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.content.res.Configuration; import android.hardware.input.InputManager; @@ -78,6 +82,7 @@ import android.os.VibrationEffect; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; +import android.provider.OpenableColumns; import android.provider.Settings; import android.util.Log; @@ -109,9 +114,10 @@ public final class EmacsService extends Service private ContentResolver resolver; /* Keep this in synch with androidgui.h. */ - public static final int IC_MODE_NULL = 0; - public static final int IC_MODE_ACTION = 1; - public static final int IC_MODE_TEXT = 2; + public static final int IC_MODE_NULL = 0; + public static final int IC_MODE_ACTION = 1; + public static final int IC_MODE_TEXT = 2; + public static final int IC_MODE_PASSWORD = 3; /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -135,6 +141,10 @@ public final class EmacsService extends Service been created yet. */ private EmacsSafThread storageThread; + /* The Thread object representing the Android user interface + thread. */ + private Thread mainThread; + static { servicingQuery = new AtomicInteger (); @@ -235,6 +245,7 @@ public final class EmacsService extends Service / metrics.density) * pixelDensityX); resolver = getContentResolver (); + mainThread = Thread.currentThread (); /* If the density used to compute the text size is lesser than 160, there's likely a bug with display density computation. @@ -383,7 +394,13 @@ public final class EmacsService extends Service { if (DEBUG_THREADS) { - if (Thread.currentThread () instanceof EmacsThread) + /* When SERVICE is NULL, Emacs is being executed non-interactively. */ + if (SERVICE == null + /* It was previously assumed that only instances of + `EmacsThread' were valid for graphics calls, but this is + no longer true now that Lisp threads can be attached to + the JVM. */ + || (Thread.currentThread () != SERVICE.mainThread)) return; throw new RuntimeException ("Emacs thread function" @@ -437,21 +454,6 @@ public final class EmacsService extends Service EmacsDrawPoint.perform (drawable, gc, x, y); } - public void - clearWindow (EmacsWindow window) - { - checkEmacsThread (); - window.clearWindow (); - } - - public void - clearArea (EmacsWindow window, int x, int y, int width, - int height) - { - checkEmacsThread (); - window.clearArea (x, y, width, height); - } - @SuppressWarnings ("deprecation") public void ringBell (int duration) @@ -581,6 +583,15 @@ public final class EmacsService extends Service return false; } + public boolean + detectKeyboard () + { + Configuration configuration; + + configuration = getResources ().getConfiguration (); + return configuration.keyboard != Configuration.KEYBOARD_NOKEYS; + } + public String nameKeysym (int keysym) { @@ -716,11 +727,29 @@ public final class EmacsService extends Service restartEmacs () { Intent intent; + PendingIntent pending; + AlarmManager manager; intent = new Intent (this, EmacsActivity.class); intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity (intent); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + startActivity (intent); + else + { + /* Experimentation has established that Android 4.3 and earlier + versions do not attempt to recreate a process when it crashes + immediately after requesting that an intent for itself be + started. Schedule an intent to start some time after Emacs + exits instead. */ + + pending = PendingIntent.getActivity (this, 0, intent, 0); + manager = (AlarmManager) getSystemService (Context.ALARM_SERVICE); + manager.set (AlarmManager.RTC, System.currentTimeMillis () + 100, + pending); + } + System.exit (0); } @@ -905,48 +934,6 @@ public final class EmacsService extends Service /* Content provider functions. */ - /* Return a ContentResolver capable of accessing as many files as - possible, namely the content resolver of the last selected - activity if available: only they posses the rights to access drag - and drop files. */ - - public ContentResolver - getUsefulContentResolver () - { - EmacsActivity activity; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - /* Since the system predates drag and drop, return this resolver - to avoid any unforeseen difficulties. */ - return resolver; - - activity = EmacsActivity.lastFocusedActivity; - if (activity == null) - return resolver; - - return activity.getContentResolver (); - } - - /* Return a context whose ContentResolver is granted access to most - files, as in `getUsefulContentResolver'. */ - - public Context - getContentResolverContext () - { - EmacsActivity activity; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - /* Since the system predates drag and drop, return this resolver - to avoid any unforeseen difficulties. */ - return this; - - activity = EmacsActivity.lastFocusedActivity; - if (activity == null) - return this; - - return activity; - } - /* Open a content URI described by the bytes BYTES, a non-terminated string; make it writable if WRITABLE, and readable if READABLE. Truncate the file if TRUNCATE. @@ -960,9 +947,6 @@ public final class EmacsService extends Service String name, mode; ParcelFileDescriptor fd; int i; - ContentResolver resolver; - - resolver = getUsefulContentResolver (); /* Figure out the file access mode. */ @@ -1024,12 +1008,8 @@ public final class EmacsService extends Service ParcelFileDescriptor fd; Uri uri; int rc, flags; - Context context; - ContentResolver resolver; ParcelFileDescriptor descriptor; - context = getContentResolverContext (); - uri = Uri.parse (name); flags = 0; @@ -1039,7 +1019,7 @@ public final class EmacsService extends Service if (writable) flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - rc = context.checkCallingUriPermission (uri, flags); + rc = checkCallingUriPermission (uri, flags); if (rc == PackageManager.PERMISSION_GRANTED) return true; @@ -1053,7 +1033,6 @@ public final class EmacsService extends Service try { - resolver = context.getContentResolver (); descriptor = resolver.openFileDescriptor (uri, "r"); return true; } @@ -1077,22 +1056,114 @@ public final class EmacsService extends Service return false; } + /* Return a 8 character checksum for the string STRING, after encoding + as UTF-8 data. */ + + public static String + getDisplayNameHash (String string) + { + byte[] encoded; + ByteArrayOutputStream stream; + int i, ch; + + /* Much of the VFS code expects file names to be encoded as modified + UTF-8 data, but Android's JNI implementation produces (while not + accepting!) regular UTF-8 sequences for all characters, even + non-Emoji ones. With no documentation to this effect, save for + two comments nestled in the source code of the Java virtual + machine, it is not sound to assume that this behavior will not be + revised in future or modified releases of Android, and as such, + encode STRING into modified UTF-8 by hand, to protect against + future changes in this respect. */ + + stream = new ByteArrayOutputStream (); + + for (i = 0; i < string.length (); ++i) + { + ch = string.charAt (i); + + if (ch != 0 && ch <= 127) + stream.write (ch); + else if (ch <= 2047) + { + stream.write (0xc0 | (0x1f & (ch >> 6))); + stream.write (0x80 | (0x3f & ch)); + } + else + { + stream.write (0xe0 | (0x0f & (ch >> 12))); + stream.write (0x80 | (0x3f & (ch >> 6))); + stream.write (0x80 | (0x3f & ch)); + } + } + + encoded = stream.toByteArray (); + + /* Closing a ByteArrayOutputStream has no effect. + encoded.close (); */ + + return EmacsNative.displayNameHash (encoded); + } + /* Build a content file name for URI. Return a file name within the /contents/by-authority pseudo-directory that `android_get_content_name' can then transform back into an encoded URI. + If a display name can be requested from URI (using the resolver + RESOLVER), append it to this file name. + A content name consists of any number of unencoded path segments separated by `/' characters, possibly followed by a question mark and an encoded query string. */ public static String - buildContentName (Uri uri) + buildContentName (Uri uri, ContentResolver resolver) { StringBuilder builder; + String displayName; + Cursor cursor; + int column; + + displayName = null; + cursor = null; + + try + { + cursor = resolver.query (uri, null, null, null, null); + + if (cursor != null) + { + cursor.moveToFirst (); + column + = cursor.getColumnIndexOrThrow (OpenableColumns.DISPLAY_NAME); + displayName + = cursor.getString (column); + + /* Verify that the display name is valid, i.e. it + contains no characters unsuitable for a file name and + is nonempty. */ + if (displayName.isEmpty () || displayName.contains ("/")) + displayName = null; + } + } + catch (Exception e) + { + /* Ignored. */ + } + finally + { + if (cursor != null) + cursor.close (); + } + + /* If a display name is available, at this point it should be the + value of displayName. */ - builder = new StringBuilder ("/content/by-authority/"); + builder = new StringBuilder (displayName != null + ? "/content/by-authority-named/" + : "/content/by-authority/"); builder.append (uri.getAuthority ()); /* First, append each path segment. */ @@ -1109,6 +1180,16 @@ public final class EmacsService extends Service if (uri.getEncodedQuery () != null) builder.append ('?').append (uri.getEncodedQuery ()); + /* Append the display name. */ + + if (displayName != null) + { + builder.append ('/'); + builder.append (getDisplayNameHash (displayName)); + builder.append ('/'); + builder.append (displayName); + } + return builder.toString (); } @@ -2011,4 +2092,29 @@ public final class EmacsService extends Service else requestStorageAccess30 (); } + + + + /* Notification miscellany. */ + + /* Cancel any notification displayed with the tag TAG. */ + + public void + cancelNotification (final String string) + { + Object tem; + final NotificationManager manager; + + tem = getSystemService (Context.NOTIFICATION_SERVICE); + manager = (NotificationManager) tem; + + runOnUiThread (new Runnable () { + @Override + public void + run () + { + manager.cancel (string, 2); + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 136d8abc713..109208b2518 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -456,7 +456,6 @@ public final class EmacsView extends ViewGroup { Canvas canvas; Rect damageRect; - Bitmap bitmap; /* Make sure this function is called only from the Emacs thread. */ @@ -474,11 +473,12 @@ public final class EmacsView extends ViewGroup damageRect = damageRegion.getBounds (); damageRegion.setEmpty (); - bitmap = getBitmap (); - - /* Transfer the bitmap to the surface view, then invalidate - it. */ - surfaceView.setBitmap (bitmap, damageRect); + synchronized (this) + { + /* Transfer the bitmap to the surface view, then invalidate + it. */ + surfaceView.setBitmap (bitmap, damageRect); + } } @Override @@ -724,17 +724,20 @@ public final class EmacsView extends ViewGroup public synchronized void onDetachedFromWindow () { - isAttachedToWindow = false; - - /* Recycle the bitmap and call GC. */ - - if (bitmap != null) - bitmap.recycle (); + Bitmap savedBitmap; + savedBitmap = bitmap; + isAttachedToWindow = false; bitmap = null; canvas = null; + surfaceView.setBitmap (null, null); + /* Recycle the bitmap and call GC. */ + + if (savedBitmap != null) + savedBitmap.recycle (); + /* Collect the bitmap storage; it could be large. */ Runtime.getRuntime ().gc (); @@ -835,9 +838,13 @@ public final class EmacsView extends ViewGroup EmacsNative.requestSelectionUpdate (window.handle); } - if (mode == EmacsService.IC_MODE_ACTION) + if (mode == EmacsService.IC_MODE_ACTION + || mode == EmacsService.IC_MODE_PASSWORD) info.imeOptions |= EditorInfo.IME_ACTION_DONE; + if (mode == EmacsService.IC_MODE_PASSWORD) + info.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; + /* Set the initial selection fields. */ info.initialSelStart = selection[0]; info.initialSelEnd = selection[1]; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 207bd22c538..2baede1d2d0 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -23,12 +23,14 @@ import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import android.app.Activity; + import android.content.ClipData; import android.content.ClipDescription; +import android.content.ContentResolver; import android.content.Context; import android.graphics.Rect; @@ -47,6 +49,7 @@ import android.view.View; import android.view.ViewManager; import android.view.WindowManager; +import android.util.SparseArray; import android.util.Log; import android.os.Build; @@ -106,7 +109,7 @@ public final class EmacsWindow extends EmacsHandleObject /* Map between pointer identifiers and last known position. Used to compute which pointer changed upon a touch event. */ - private HashMap<Integer, Coordinate> pointerMap; + private SparseArray<Coordinate> pointerMap; /* The window consumer currently attached, if it exists. */ private EmacsWindowAttachmentManager.WindowConsumer attached; @@ -163,7 +166,7 @@ public final class EmacsWindow extends EmacsHandleObject super (handle); rect = new Rect (x, y, x + width, y + height); - pointerMap = new HashMap<Integer, Coordinate> (); + pointerMap = new SparseArray<Coordinate> (); /* Create the view from the context's UI thread. The window is unmapped, so the view is GONE. */ @@ -240,7 +243,7 @@ public final class EmacsWindow extends EmacsHandleObject } } - EmacsActivity.invalidateFocus (); + EmacsActivity.invalidateFocus (4); if (!children.isEmpty ()) throw new IllegalStateException ("Trying to destroy window with " @@ -362,6 +365,9 @@ public final class EmacsWindow extends EmacsHandleObject requestViewLayout (); } + /* Return WM layout parameters for an override redirect window with + the geometry provided here. */ + private WindowManager.LayoutParams getWindowLayoutParams () { @@ -384,15 +390,15 @@ public final class EmacsWindow extends EmacsHandleObject return params; } - private Context + private Activity findSuitableActivityContext () { /* Find a recently focused activity. */ if (!EmacsActivity.focusedActivities.isEmpty ()) return EmacsActivity.focusedActivities.get (0); - /* Return the service context, which probably won't work. */ - return EmacsService.SERVICE; + /* Resort to the last activity to be focused. */ + return EmacsActivity.lastFocusedActivity; } public synchronized void @@ -416,7 +422,7 @@ public final class EmacsWindow extends EmacsHandleObject { EmacsWindowAttachmentManager manager; WindowManager windowManager; - Context ctx; + Activity ctx; Object tem; WindowManager.LayoutParams params; @@ -447,11 +453,23 @@ public final class EmacsWindow extends EmacsHandleObject activity using the system window manager. */ ctx = findSuitableActivityContext (); + + if (ctx == null) + { + Log.w (TAG, "failed to attach override-redirect window" + + " for want of activity"); + return; + } + tem = ctx.getSystemService (Context.WINDOW_SERVICE); windowManager = (WindowManager) tem; - /* Calculate layout parameters. */ + /* Calculate layout parameters and propagate the + activity's token into it. */ + params = getWindowLayoutParams (); + params.token = (ctx.findViewById (android.R.id.content) + .getWindowToken ()); view.setLayoutParams (params); /* Attach the view. */ @@ -644,7 +662,7 @@ public final class EmacsWindow extends EmacsHandleObject public void onKeyDown (int keyCode, KeyEvent event) { - int state, state_1, num_lock_flag; + int state, state_1, extra_ignored; long serial; String characters; @@ -665,23 +683,37 @@ public final class EmacsWindow extends EmacsHandleObject state = eventModifiers (event); - /* Num Lock and Scroll Lock aren't supported by systems older than - Android 3.0. */ + /* Num Lock, Scroll Lock and Meta aren't supported by systems older + than Android 3.0. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) - num_lock_flag = (KeyEvent.META_NUM_LOCK_ON - | KeyEvent.META_SCROLL_LOCK_ON); + extra_ignored = (KeyEvent.META_NUM_LOCK_ON + | KeyEvent.META_SCROLL_LOCK_ON + | KeyEvent.META_META_MASK); else - num_lock_flag = 0; + extra_ignored = 0; /* Ignore meta-state understood by Emacs for now, or key presses - such as Ctrl+C and Meta+C will not be recognized as an ASCII - key press event. */ + such as Ctrl+C and Meta+C will not be recognized as ASCII key + press events. */ state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK - | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK - | num_lock_flag); + | KeyEvent.META_SYM_ON | extra_ignored); + + /* There's no distinction between Right Alt and Alt Gr on Android, + so restore META_ALT_RIGHT_ON if set in state to enable composing + characters. (bug#69321) */ + + if ((state & KeyEvent.META_ALT_RIGHT_ON) != 0) + { + state_1 |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON; + + /* If Alt is also not depressed, remove its bit from the mask + reported to Emacs. */ + if ((state & KeyEvent.META_ALT_LEFT_ON) == 0) + state &= ~KeyEvent.META_ALT_MASK; + } synchronized (eventStrings) { @@ -702,29 +734,43 @@ public final class EmacsWindow extends EmacsHandleObject public void onKeyUp (int keyCode, KeyEvent event) { - int state, state_1, unicode_char, num_lock_flag; + int state, state_1, unicode_char, extra_ignored; long time; /* Compute the event's modifier mask. */ state = eventModifiers (event); - /* Num Lock and Scroll Lock aren't supported by systems older than - Android 3.0. */ + /* Num Lock, Scroll Lock and Meta aren't supported by systems older + than Android 3.0. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) - num_lock_flag = (KeyEvent.META_NUM_LOCK_ON - | KeyEvent.META_SCROLL_LOCK_ON); + extra_ignored = (KeyEvent.META_NUM_LOCK_ON + | KeyEvent.META_SCROLL_LOCK_ON + | KeyEvent.META_META_MASK); else - num_lock_flag = 0; + extra_ignored = 0; /* Ignore meta-state understood by Emacs for now, or key presses - such as Ctrl+C and Meta+C will not be recognized as an ASCII - key press event. */ + such as Ctrl+C and Meta+C will not be recognized as ASCII key + press events. */ state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK - | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK - | num_lock_flag); + | KeyEvent.META_SYM_ON | extra_ignored); + + /* There's no distinction between Right Alt and Alt Gr on Android, + so restore META_ALT_RIGHT_ON if set in state to enable composing + characters. */ + + if ((state & KeyEvent.META_ALT_RIGHT_ON) != 0) + { + state_1 |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON; + + /* If Alt is also not depressed, remove its bit from the mask + reported to Emacs. */ + if ((state & KeyEvent.META_ALT_LEFT_ON) == 0) + state &= ~KeyEvent.META_ALT_MASK; + } unicode_char = getEventUnicodeChar (event, state_1); @@ -760,7 +806,7 @@ public final class EmacsWindow extends EmacsHandleObject public void onFocusChanged (boolean gainFocus) { - EmacsActivity.invalidateFocus (); + EmacsActivity.invalidateFocus (gainFocus ? 6 : 5); } /* Notice that the activity has been detached or destroyed. @@ -955,7 +1001,8 @@ public final class EmacsWindow extends EmacsHandleObject case MotionEvent.ACTION_CANCEL: /* Primary pointer released with index 0. */ pointerID = event.getPointerId (0); - coordinate = pointerMap.remove (pointerID); + coordinate = pointerMap.get (pointerID); + pointerMap.delete (pointerID); break; case MotionEvent.ACTION_POINTER_DOWN: @@ -974,7 +1021,8 @@ public final class EmacsWindow extends EmacsHandleObject /* Pointer removed. Remove it from the map. */ pointerIndex = event.getActionIndex (); pointerID = event.getPointerId (pointerIndex); - coordinate = pointerMap.remove (pointerID); + coordinate = pointerMap.get (pointerID); + pointerMap.delete (pointerID); break; default: @@ -1654,10 +1702,11 @@ public final class EmacsWindow extends EmacsHandleObject ClipData data; ClipDescription description; int i, j, x, y, itemCount; - String type; + String type, uriString; Uri uri; EmacsActivity activity; StringBuilder builder; + ContentResolver resolver; x = (int) event.getX (); y = (int) event.getY (); @@ -1746,7 +1795,7 @@ public final class EmacsWindow extends EmacsHandleObject /* Attempt to acquire permissions for this URI; failing which, insert it as text instead. */ - + if (uri != null && uri.getScheme () != null && uri.getScheme ().equals ("content") @@ -1754,6 +1803,20 @@ public final class EmacsWindow extends EmacsHandleObject { if ((activity.requestDragAndDropPermissions (event) == null)) uri = null; + else + { + resolver = activity.getContentResolver (); + + /* Substitute a content file name for the URI, if + possible. */ + uriString = EmacsService.buildContentName (uri, resolver); + + if (uriString != null) + { + builder.append (uriString).append ("\n"); + continue; + } + } } if (uri != null) @@ -1784,4 +1847,32 @@ public final class EmacsWindow extends EmacsHandleObject return true; } + + + + /* Miscellaneous functions for debugging graphics code. */ + + /* Recreate the activity to which this window is attached, if any. + This is nonfunctional on Android 2.3.7 and earlier. */ + + public void + recreateActivity () + { + final EmacsWindowAttachmentManager.WindowConsumer attached; + + attached = this.attached; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) + return; + + view.post (new Runnable () { + @Override + public void + run () + { + if (attached instanceof EmacsActivity) + ((EmacsActivity) attached).recreate (); + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 18bdb6dbf60..aae4e2ee49b 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -124,10 +124,15 @@ public final class EmacsWindowAttachmentManager intent = new Intent (EmacsService.SERVICE, EmacsMultitaskActivity.class); - intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT - | Intent.FLAG_ACTIVITY_NEW_TASK + + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + /* Intent.FLAG_ACTIVITY_NEW_DOCUMENT is lamentably unavailable on + older systems than Lolipop. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) EmacsService.SERVICE.startActivity (intent); else |