1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2025-12-24 14:30:43 -08:00
emacs/java/org/gnu/emacs/EmacsService.java
Po Lu 43cc92d6e4 Minor adjustments to Android port
* java/org/gnu/emacs/EmacsService.java (readDirectoryEntry):
Also refrain from returning NULL or file names containing
non-representable NULL bytes.
* src/callproc.c (get_current_directory): Clean up by employing
android_is_special_directory.
2023-08-11 14:55:21 +08:00

1825 lines
47 KiB
Java
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
Copyright (C) 2023 Free Software Foundation, Inc.
This file is part of GNU Emacs.
GNU Emacs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
GNU Emacs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
package org.gnu.emacs;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import android.database.Cursor;
import android.graphics.Matrix;
import android.graphics.Point;
import android.webkit.MimeTypeMap;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.ExtractedText;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.NotificationChannel;
import android.app.Service;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.UriPermission;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Looper;
import android.os.IBinder;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.Vibrator;
import android.os.VibratorManager;
import android.os.VibrationEffect;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import android.util.DisplayMetrics;
import android.widget.Toast;
/* EmacsService is the service that starts the thread running Emacs
and handles requests by that Emacs instance. */
public final class EmacsService extends Service
{
public static final String TAG = "EmacsService";
/* The started Emacs service object. */
public static EmacsService SERVICE;
/* If non-NULL, an extra argument to pass to
`android_emacs_init'. */
public static String extraStartupArgument;
/* The thread running Emacs C code. */
private EmacsThread thread;
/* Handler used to run tasks on the main thread. */
private Handler handler;
/* Content resolver used to access URIs. */
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;
/* Display metrics used by font backends. */
public DisplayMetrics metrics;
/* Flag that says whether or not to print verbose debugging
information when responding to an input method. */
public static final boolean DEBUG_IC = false;
/* Flag that says whether or not to stringently check that only the
Emacs thread is performing drawing calls. */
private static final boolean DEBUG_THREADS = false;
/* Atomic integer used for synchronization between
icBeginSynchronous/icEndSynchronous and viewGetSelection.
Value is 0 if no query is in progress, 1 if viewGetSelection is
being called, and 2 if icBeginSynchronous was called. */
public static final AtomicInteger servicingQuery;
/* Thread used to query document providers, or null if it hasn't
been created yet. */
private EmacsSafThread storageThread;
static
{
servicingQuery = new AtomicInteger ();
};
/* Return the directory leading to the directory in which native
library files are stored on behalf of CONTEXT. */
public static String
getLibraryDirectory (Context context)
{
int apiLevel;
apiLevel = Build.VERSION.SDK_INT;
if (apiLevel >= Build.VERSION_CODES.GINGERBREAD)
return context.getApplicationInfo ().nativeLibraryDir;
return context.getApplicationInfo ().dataDir + "/lib";
}
@Override
public int
onStartCommand (Intent intent, int flags, int startId)
{
Notification notification;
NotificationManager manager;
NotificationChannel channel;
String infoBlurb;
Object tem;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
tem = getSystemService (Context.NOTIFICATION_SERVICE);
manager = (NotificationManager) tem;
infoBlurb = ("This notification is displayed to keep Emacs"
+ " running while it is in the background. You"
+ " may disable it if you want;"
+ " see (emacs)Android Environment.");
channel
= new NotificationChannel ("emacs", "Emacs persistent notification",
NotificationManager.IMPORTANCE_DEFAULT);
manager.createNotificationChannel (channel);
notification = (new Notification.Builder (this, "emacs")
.setContentTitle ("Emacs")
.setContentText (infoBlurb)
.setSmallIcon (android.R.drawable.sym_def_app_icon)
.build ());
manager.notify (1, notification);
startForeground (1, notification);
}
return START_NOT_STICKY;
}
@Override
public IBinder
onBind (Intent intent)
{
return null;
}
@SuppressWarnings ("deprecation")
private String
getApkFile ()
{
PackageManager manager;
ApplicationInfo info;
manager = getPackageManager ();
try
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
info = manager.getApplicationInfo ("org.gnu.emacs", 0);
else
info = manager.getApplicationInfo ("org.gnu.emacs",
ApplicationInfoFlags.of (0));
/* Return an empty string upon failure. */
if (info.sourceDir != null)
return info.sourceDir;
return "";
}
catch (Exception e)
{
return "";
}
}
@Override
public void
onCreate ()
{
final AssetManager manager;
Context app_context;
final String filesDir, libDir, cacheDir, classPath;
final double pixelDensityX;
final double pixelDensityY;
final double scaledDensity;
double tempScaledDensity;
SERVICE = this;
handler = new Handler (Looper.getMainLooper ());
manager = getAssets ();
app_context = getApplicationContext ();
metrics = getResources ().getDisplayMetrics ();
pixelDensityX = metrics.xdpi;
pixelDensityY = metrics.ydpi;
tempScaledDensity = ((metrics.scaledDensity
/ metrics.density)
* pixelDensityX);
resolver = getContentResolver ();
/* If the density used to compute the text size is lesser than
160, there's likely a bug with display density computation.
Reset it to 160 in that case.
Note that Android uses 160 ``dpi'' as the density where 1 point
corresponds to 1 pixel, not 72 or 96 as used elsewhere. This
difference is codified in PT_PER_INCH defined in font.h. */
if (tempScaledDensity < 160)
tempScaledDensity = 160;
/* scaledDensity is const as required to refer to it from within
the nested function below. */
scaledDensity = tempScaledDensity;
try
{
/* Configure Emacs with the asset manager and other necessary
parameters. */
filesDir = app_context.getFilesDir ().getCanonicalPath ();
libDir = getLibraryDirectory (this);
cacheDir = app_context.getCacheDir ().getCanonicalPath ();
/* Now provide this application's apk file, so a recursive
invocation of app_process (through android-emacs) can
find EmacsNoninteractive. */
classPath = getApkFile ();
Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir
+ ", libDir = " + libDir + ", and classPath = " + classPath
+ "; fileToOpen = " + EmacsOpenActivity.fileToOpen
+ "; display density: " + pixelDensityX + " by "
+ pixelDensityY + " scaled to " + scaledDensity);
/* Start the thread that runs Emacs. */
thread = new EmacsThread (this, new Runnable () {
@Override
public void
run ()
{
EmacsNative.setEmacsParams (manager, filesDir, libDir,
cacheDir, (float) pixelDensityX,
(float) pixelDensityY,
(float) scaledDensity,
classPath, EmacsService.this,
Build.VERSION.SDK_INT);
}
}, extraStartupArgument,
/* If any file needs to be opened, open it now. */
EmacsOpenActivity.fileToOpen);
thread.start ();
}
catch (IOException exception)
{
EmacsNative.emacsAbort ();
return;
}
}
/* Functions from here on must only be called from the Emacs
thread. */
public void
runOnUiThread (Runnable runnable)
{
handler.post (runnable);
}
public EmacsView
getEmacsView (final EmacsWindow window, final int visibility,
final boolean isFocusedByDefault)
{
Runnable runnable;
final EmacsHolder<EmacsView> view;
view = new EmacsHolder<EmacsView> ();
runnable = new Runnable () {
@Override
public void
run ()
{
synchronized (this)
{
view.thing = new EmacsView (window);
view.thing.setVisibility (visibility);
/* The following function is only present on Android 26
or later. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
view.thing.setFocusedByDefault (isFocusedByDefault);
notify ();
}
}
};
syncRunnable (runnable);
return view.thing;
}
public void
getLocationOnScreen (final EmacsView view, final int[] coordinates)
{
Runnable runnable;
runnable = new Runnable () {
public void
run ()
{
synchronized (this)
{
view.getLocationOnScreen (coordinates);
notify ();
}
}
};
syncRunnable (runnable);
}
public static void
checkEmacsThread ()
{
if (DEBUG_THREADS)
{
if (Thread.currentThread () instanceof EmacsThread)
return;
throw new RuntimeException ("Emacs thread function"
+ " called from other thread!");
}
}
/* These drawing functions must only be called from the Emacs
thread. */
public void
fillRectangle (EmacsDrawable drawable, EmacsGC gc,
int x, int y, int width, int height)
{
checkEmacsThread ();
EmacsFillRectangle.perform (drawable, gc, x, y,
width, height);
}
public void
fillPolygon (EmacsDrawable drawable, EmacsGC gc,
Point points[])
{
checkEmacsThread ();
EmacsFillPolygon.perform (drawable, gc, points);
}
public void
drawRectangle (EmacsDrawable drawable, EmacsGC gc,
int x, int y, int width, int height)
{
checkEmacsThread ();
EmacsDrawRectangle.perform (drawable, gc, x, y,
width, height);
}
public void
drawLine (EmacsDrawable drawable, EmacsGC gc,
int x, int y, int x2, int y2)
{
checkEmacsThread ();
EmacsDrawLine.perform (drawable, gc, x, y,
x2, y2);
}
public void
drawPoint (EmacsDrawable drawable, EmacsGC gc,
int x, int y)
{
checkEmacsThread ();
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 ()
{
Vibrator vibrator;
VibrationEffect effect;
VibratorManager vibratorManager;
Object tem;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
{
tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE);
vibratorManager = (VibratorManager) tem;
vibrator = vibratorManager.getDefaultVibrator ();
}
else
vibrator
= (Vibrator) getSystemService (Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
effect
= VibrationEffect.createOneShot (50,
VibrationEffect.DEFAULT_AMPLITUDE);
vibrator.vibrate (effect);
}
else
vibrator.vibrate (50);
}
public short[]
queryTree (EmacsWindow window)
{
short[] array;
List<EmacsWindow> windowList;
int i;
if (window == null)
/* Just return all the windows without a parent. */
windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows ();
else
windowList = window.children;
array = new short[windowList.size () + 1];
i = 1;
array[0] = (window == null
? 0 : (window.parent != null
? window.parent.handle : 0));
for (EmacsWindow treeWindow : windowList)
array[i++] = treeWindow.handle;
return array;
}
public int
getScreenWidth (boolean mmWise)
{
DisplayMetrics metrics;
metrics = getResources ().getDisplayMetrics ();
if (!mmWise)
return metrics.widthPixels;
else
return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0);
}
public int
getScreenHeight (boolean mmWise)
{
DisplayMetrics metrics;
metrics = getResources ().getDisplayMetrics ();
if (!mmWise)
return metrics.heightPixels;
else
return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0);
}
public boolean
detectMouse ()
{
InputManager manager;
InputDevice device;
int[] ids;
int i;
if (Build.VERSION.SDK_INT
/* Android 4.0 and earlier don't support mouse input events at
all. */
< Build.VERSION_CODES.JELLY_BEAN)
return false;
manager = (InputManager) getSystemService (Context.INPUT_SERVICE);
ids = manager.getInputDeviceIds ();
for (i = 0; i < ids.length; ++i)
{
device = manager.getInputDevice (ids[i]);
if (device == null)
continue;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
if (device.supportsSource (InputDevice.SOURCE_MOUSE))
return true;
}
else
{
/* `supportsSource' is only present on API level 21 and
later, but earlier versions provide a bit mask
containing each supported source. */
if ((device.getSources () & InputDevice.SOURCE_MOUSE) != 0)
return true;
}
}
return false;
}
public String
nameKeysym (int keysym)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
return KeyEvent.keyCodeToString (keysym);
return String.valueOf (keysym);
}
/* Start the Emacs service if necessary. On Android 26 and up,
start Emacs as a foreground service with a notification, to avoid
it being killed by the system.
On older systems, simply start it as a normal background
service. */
public static void
startEmacsService (Context context)
{
if (EmacsService.SERVICE == null)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
/* Start the Emacs service now. */
context.startService (new Intent (context,
EmacsService.class));
else
/* Display the permanant notification and start Emacs as a
foreground service. */
context.startForegroundService (new Intent (context,
EmacsService.class));
}
}
/* Ask the system to open the specified URL in an application that
understands how to open it.
If SEND, tell the system to also open applications that can
``send'' the URL (through mail, for example), instead of only
those that can view the URL.
Value is NULL upon success, or a string describing the error
upon failure. */
public String
browseUrl (String url, boolean send)
{
Intent intent;
Uri uri;
try
{
/* Parse the URI. */
if (!send)
{
uri = Uri.parse (url);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
/* On Android 4.4 and later, check if URI is actually
a file name. If so, rewrite it into a content
provider URI, so that it can be accessed by other
programs. */
if (uri.getScheme ().equals ("file")
&& uri.getPath () != null)
uri
= DocumentsContract.buildDocumentUri ("org.gnu.emacs",
uri.getPath ());
}
Log.d (TAG, ("browseUri: browsing " + url
+ " --> " + uri.getPath ()
+ " --> " + uri));
intent = new Intent (Intent.ACTION_VIEW, uri);
intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
else
{
intent = new Intent (Intent.ACTION_SEND);
intent.setType ("text/plain");
intent.putExtra (Intent.EXTRA_SUBJECT, "Sharing link");
intent.putExtra (Intent.EXTRA_TEXT, url);
/* Display a list of programs able to send this URL. */
intent = Intent.createChooser (intent, "Send");
/* Apparently flags need to be set after a choser is
created. */
intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity (intent);
}
catch (Exception e)
{
return e.toString ();
}
return null;
}
/* Get a SDK 11 ClipboardManager.
Android 4.0.x requires that this be called from the main
thread. */
public ClipboardManager
getClipboardManager ()
{
final EmacsHolder<ClipboardManager> manager;
Runnable runnable;
manager = new EmacsHolder<ClipboardManager> ();
runnable = new Runnable () {
public void
run ()
{
Object tem;
synchronized (this)
{
tem = getSystemService (Context.CLIPBOARD_SERVICE);
manager.thing = (ClipboardManager) tem;
notify ();
}
}
};
syncRunnable (runnable);
return manager.thing;
}
public void
restartEmacs ()
{
Intent intent;
intent = new Intent (this, EmacsActivity.class);
intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity (intent);
System.exit (0);
}
/* Wait synchronously for the specified RUNNABLE to complete in the
UI thread. Must be called from the Emacs thread. */
public static void
syncRunnable (Runnable runnable)
{
EmacsNative.beginSynchronous ();
synchronized (runnable)
{
SERVICE.runOnUiThread (runnable);
while (true)
{
try
{
runnable.wait ();
break;
}
catch (InterruptedException e)
{
continue;
}
}
}
EmacsNative.endSynchronous ();
}
/* IMM functions such as `updateSelection' holds an internal lock
that is also taken before `onCreateInputConnection' (in
EmacsView.java) is called; when that then asks the UI thread for
the current selection, a dead lock results. To remedy this,
reply to any synchronous queries now -- and prohibit more queries
for the duration of `updateSelection' -- if EmacsView may have
been asking for the value of the region. */
public static void
icBeginSynchronous ()
{
/* Set servicingQuery to 2, so viewGetSelection knows it shouldn't
proceed. */
if (servicingQuery.getAndSet (2) == 1)
/* But if viewGetSelection is already in progress, answer it
first. */
EmacsNative.answerQuerySpin ();
}
public static void
icEndSynchronous ()
{
if (servicingQuery.getAndSet (0) != 2)
throw new RuntimeException ("incorrect value of `servicingQuery': "
+ "likely 1");
}
public static int[]
viewGetSelection (short window)
{
int[] selection;
/* See if a query is already in progress from the other
direction. */
if (!servicingQuery.compareAndSet (0, 1))
return null;
/* Now call the regular getSelection. Note that this can't race
with answerQuerySpin, as `android_servicing_query' can never be
2 when icBeginSynchronous is called, so a query will always be
started. */
selection = EmacsNative.getSelection (window);
/* Finally, clear servicingQuery if its value is still 1. If a
query has started from the other side, it ought to be 2. */
servicingQuery.compareAndSet (1, 0);
return selection;
}
public void
updateIC (EmacsWindow window, int newSelectionStart,
int newSelectionEnd, int composingRegionStart,
int composingRegionEnd)
{
if (DEBUG_IC)
Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart
+ " " + newSelectionEnd + " "
+ composingRegionStart + " "
+ composingRegionEnd));
icBeginSynchronous ();
window.view.imManager.updateSelection (window.view,
newSelectionStart,
newSelectionEnd,
composingRegionStart,
composingRegionEnd);
icEndSynchronous ();
}
public void
resetIC (EmacsWindow window, int icMode)
{
int oldMode;
if (DEBUG_IC)
Log.d (TAG, "resetIC: " + window + ", " + icMode);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& (oldMode = window.view.getICMode ()) == icMode
/* Don't do this if there is currently no input
connection. */
&& oldMode != IC_MODE_NULL)
{
if (DEBUG_IC)
Log.d (TAG, "resetIC: calling invalidateInput");
/* Android 33 and later allow the IM reset to be optimized out
and replaced by a call to `invalidateInput', which is much
faster, as it does not involve resetting the input
connection. */
icBeginSynchronous ();
window.view.imManager.invalidateInput (window.view);
icEndSynchronous ();
return;
}
window.view.setICMode (icMode);
icBeginSynchronous ();
window.view.icGeneration++;
window.view.imManager.restartInput (window.view);
icEndSynchronous ();
}
public void
updateCursorAnchorInfo (EmacsWindow window, float x,
float y, float yBaseline,
float yBottom)
{
CursorAnchorInfo info;
CursorAnchorInfo.Builder builder;
Matrix matrix;
int[] offsets;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
return;
offsets = new int[2];
builder = new CursorAnchorInfo.Builder ();
matrix = new Matrix (window.view.getMatrix ());
window.view.getLocationOnScreen (offsets);
matrix.postTranslate (offsets[0], offsets[1]);
builder.setMatrix (matrix);
builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom,
0);
info = builder.build ();
if (DEBUG_IC)
Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y
+ " " + yBaseline + "-" + yBottom));
icBeginSynchronous ();
window.view.imManager.updateCursorAnchorInfo (window.view, info);
icEndSynchronous ();
}
/* Content provider functions. */
/* 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.
Value is the resulting file descriptor or -1 upon failure. */
public int
openContentUri (byte[] bytes, boolean writable, boolean readable,
boolean truncate)
{
String name, mode;
ParcelFileDescriptor fd;
int i;
/* Figure out the file access mode. */
mode = "";
if (readable)
mode += "r";
if (writable)
mode += "w";
if (truncate)
mode += "t";
/* Try to open an associated ParcelFileDescriptor. */
try
{
/* The usual file name encoding question rears its ugly head
again. */
name = new String (bytes, "UTF-8");
fd = resolver.openFileDescriptor (Uri.parse (name), mode);
/* Use detachFd on newer versions of Android or plain old
dup. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
{
i = fd.detachFd ();
fd.close ();
return i;
}
else
{
i = EmacsNative.dup (fd.getFd ());
fd.close ();
return i;
}
}
catch (Exception exception)
{
return -1;
}
}
public boolean
checkContentUri (byte[] string, boolean readable, boolean writable)
{
String mode, name;
ParcelFileDescriptor fd;
/* Decode this into a URI. */
try
{
/* The usual file name encoding question rears its ugly head
again. */
name = new String (string, "UTF-8");
}
catch (UnsupportedEncodingException exception)
{
name = null;
throw new RuntimeException (exception);
}
mode = "r";
if (writable)
mode += "w";
try
{
fd = resolver.openFileDescriptor (Uri.parse (name), mode);
fd.close ();
return true;
}
catch (Exception exception)
{
/* Fall through. */
}
return false;
}
/* 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.
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)
{
StringBuilder builder;
builder = new StringBuilder ("/content/by-authority/");
builder.append (uri.getAuthority ());
/* First, append each path segment. */
for (String segment : uri.getPathSegments ())
{
/* FIXME: what if segment contains a slash character? */
builder.append ('/');
builder.append (uri.encode (segment));
}
/* Now, append the query string if necessary. */
if (uri.getEncodedQuery () != null)
builder.append ('?').append (uri.getEncodedQuery ());
return builder.toString ();
}
private long[]
queryBattery19 ()
{
IntentFilter filter;
Intent battery;
long capacity, chargeCounter, currentAvg, currentNow;
long status, remaining, plugged, temp;
filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED);
battery = registerReceiver (null, filter);
if (battery == null)
return null;
capacity = battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 0);
chargeCounter
= (battery.getIntExtra (BatteryManager.EXTRA_SCALE, 0)
/ battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 100) * 100);
currentAvg = 0;
currentNow = 0;
status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0);
remaining = -1;
plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0);
temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0);
return new long[] { capacity, chargeCounter, currentAvg,
currentNow, remaining, status, plugged,
temp, };
}
/* Return the status of the battery. See struct
android_battery_status for the order of the elements
returned.
Value may be null upon failure. */
public long[]
queryBattery ()
{
Object tem;
BatteryManager manager;
long capacity, chargeCounter, currentAvg, currentNow;
long status, remaining, plugged, temp;
int prop;
IntentFilter filter;
Intent battery;
/* Android 4.4 or earlier require applications to use a different
API to query the battery status. */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
return queryBattery19 ();
tem = getSystemService (Context.BATTERY_SERVICE);
manager = (BatteryManager) tem;
remaining = -1;
prop = BatteryManager.BATTERY_PROPERTY_CAPACITY;
capacity = manager.getLongProperty (prop);
prop = BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER;
chargeCounter = manager.getLongProperty (prop);
prop = BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE;
currentAvg = manager.getLongProperty (prop);
prop = BatteryManager.BATTERY_PROPERTY_CURRENT_NOW;
currentNow = manager.getLongProperty (prop);
/* Return the battery status. N.B. that Android 7.1 and earlier
only return ``charging'' or ``discharging''. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
status
= manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS);
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
status = (manager.isCharging ()
? BatteryManager.BATTERY_STATUS_CHARGING
: BatteryManager.BATTERY_STATUS_DISCHARGING);
else
status = (currentNow > 0
? BatteryManager.BATTERY_STATUS_CHARGING
: BatteryManager.BATTERY_STATUS_DISCHARGING);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
remaining = manager.computeChargeTimeRemaining ();
plugged = -1;
temp = -1;
/* Now obtain additional information from the battery manager. */
filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED);
battery = registerReceiver (null, filter);
if (battery != null)
{
plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0);
temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0);
/* Make status more reliable. */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0);
}
return new long[] { capacity, chargeCounter, currentAvg,
currentNow, remaining, status, plugged,
temp, };
}
public void
updateExtractedText (EmacsWindow window, ExtractedText text,
int token)
{
if (DEBUG_IC)
Log.d (TAG, "updateExtractedText: @" + token + ", " + text);
window.view.imManager.updateExtractedText (window.view,
token, text);
}
/* Document tree management functions. These functions shouldn't be
called before Android 5.0. */
/* Return an array of each document authority providing at least one
tree URI that Emacs holds the rights to persistently access. */
public String[]
getDocumentAuthorities ()
{
List<UriPermission> permissions;
HashSet<String> allProviders;
Uri uri;
permissions = resolver.getPersistedUriPermissions ();
allProviders = new HashSet<String> ();
for (UriPermission permission : permissions)
{
uri = permission.getUri ();
if (DocumentsContract.isTreeUri (uri)
&& permission.isReadPermission ())
allProviders.add (uri.getAuthority ());
}
return allProviders.toArray (new String[0]);
}
/* Start a file chooser activity to request access to a directory
tree.
Value is 1 if the activity couldn't be started for some reason,
and 0 in any other case. */
public int
requestDirectoryAccess ()
{
Runnable runnable;
final EmacsHolder<Integer> rc;
/* Return 1 if Android is too old to support this feature. */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
return 1;
rc = new EmacsHolder<Integer> ();
rc.thing = Integer.valueOf (1);
runnable = new Runnable () {
@Override
public void
run ()
{
EmacsActivity activity;
Intent intent;
int id;
synchronized (this)
{
/* Try to obtain an activity that will receive the
response from the file chooser dialog. */
if (EmacsActivity.focusedActivities.isEmpty ())
{
/* If focusedActivities is empty then this dialog
may have been displayed immediately after another
popup dialog was dismissed. Try the
EmacsActivity to be focused. */
activity = EmacsActivity.lastFocusedActivity;
if (activity == null)
{
/* Still no luck. Return failure. */
notify ();
return;
}
}
else
activity = EmacsActivity.focusedActivities.get (0);
/* Now create the intent. */
intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE);
try
{
id = EmacsActivity.ACCEPT_DOCUMENT_TREE;
activity.startActivityForResult (intent, id, null);
rc.thing = Integer.valueOf (0);
}
catch (Exception e)
{
e.printStackTrace ();
}
notify ();
}
}
};
syncRunnable (runnable);
return rc.thing;
}
/* Return an array of each tree provided by the document PROVIDER
that Emacs has permission to access.
Value is an array if the provider really does exist, NULL
otherwise. */
public String[]
getDocumentTrees (byte provider[])
{
String providerName;
List<String> treeList;
List<UriPermission> permissions;
Uri uri;
try
{
providerName = new String (provider, "US-ASCII");
}
catch (UnsupportedEncodingException exception)
{
return null;
}
permissions = resolver.getPersistedUriPermissions ();
treeList = new ArrayList<String> ();
for (UriPermission permission : permissions)
{
uri = permission.getUri ();
if (DocumentsContract.isTreeUri (uri)
&& uri.getAuthority ().equals (providerName)
&& permission.isReadPermission ())
/* Make sure the tree document ID is encoded. Refrain from
encoding characters such as +:&?#, since they don't
conflict with file name separators or other special
characters. */
treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri),
" +:&?#"));
}
return treeList.toArray (new String[0]);
}
/* Find the document ID of the file within TREE_URI designated by
NAME.
NAME is a ``file name'' comprised of the display names of
individual files. Each constituent component prior to the last
must name a directory file within TREE_URI.
Upon success, return 0 or 1 (contingent upon whether or not the
last component within NAME is a directory) and place the document
ID of the named file in ID_RETURN[0].
If the designated file can't be located, but each component of
NAME up to the last component can and is a directory, return -2
and the ID of the last component located in ID_RETURN[0].
If the designated file can't be located, return -1, or signal one
of OperationCanceledException, SecurityException,
FileNotFoundException, or UnsupportedOperationException. */
private int
documentIdFromName (String tree_uri, String name, String[] id_return)
{
/* Start the thread used to run SAF requests if it isn't already
running. */
if (storageThread == null)
{
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return storageThread.documentIdFromName (tree_uri, name,
id_return);
}
/* Return an encoded document URI representing a tree with the
specified IDENTIFIER supplied by the authority AUTHORITY.
Return null instead if Emacs does not have permanent access
to the specified document tree recorded on disk. */
public String
getTreeUri (String tree, String authority)
{
Uri uri, grantedUri;
List<UriPermission> permissions;
/* First, build the URI. */
tree = Uri.decode (tree);
uri = DocumentsContract.buildTreeDocumentUri (authority, tree);
/* Now, search for it within the list of persisted URI
permissions. */
permissions = resolver.getPersistedUriPermissions ();
for (UriPermission permission : permissions)
{
/* If the permission doesn't entitle Emacs to read access,
skip it. */
if (!permission.isReadPermission ())
continue;
grantedUri = permission.getUri ();
if (grantedUri.equals (uri))
return uri.toString ();
}
/* Emacs doesn't have permission to access this tree URI. */
return null;
}
/* Return file status for the document designated by the given
DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
ID in URI itself.
Value is null upon failure, or an array of longs [MODE, SIZE,
MTIM] upon success, where MODE contains the file type and access
modes of the file as in `struct stat', SIZE is the size of the
file in BYTES or -1 if not known, and MTIM is the time of the
last modification to this file in milliseconds since 00:00,
January 1st, 1970.
If NOCACHE, refrain from placing the file status within the
status cache.
OperationCanceledException and other typical exceptions may be
signaled upon receiving async input or other errors. */
public long[]
statDocument (String uri, String documentId, boolean noCache)
{
/* Start the thread used to run SAF requests if it isn't already
running. */
if (storageThread == null)
{
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return storageThread.statDocument (uri, documentId, noCache);
}
/* Find out whether Emacs has access to the document designated by
the specified DOCUMENTID within the tree URI. If DOCUMENTID is
NULL, use the document ID in URI itself.
If WRITABLE, also check that the file is writable, which is true
if it is either a directory or its flags contains
FLAG_SUPPORTS_WRITE.
Value is 0 if the file is accessible, and one of the following if
not:
-1, if the file does not exist.
-2, if WRITABLE and the file is not writable.
-3, upon any other error.
In addition, arbitrary runtime exceptions (such as
SecurityException or UnsupportedOperationException) may be
thrown. */
public int
accessDocument (String uri, String documentId, boolean writable)
{
/* Start the thread used to run SAF requests if it isn't already
running. */
if (storageThread == null)
{
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return storageThread.accessDocument (uri, documentId, writable);
}
/* Open a cursor representing each entry within the directory
designated by the specified DOCUMENTID within the tree URI.
If DOCUMENTID is NULL, use the document ID within URI itself.
Value is NULL upon failure.
In addition, arbitrary runtime exceptions (such as
SecurityException or UnsupportedOperationException) may be
thrown. */
public Cursor
openDocumentDirectory (String uri, String documentId)
{
/* Start the thread used to run SAF requests if it isn't already
running. */
if (storageThread == null)
{
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return storageThread.openDocumentDirectory (uri, documentId);
}
/* Read a single directory entry from the specified CURSOR. Return
NULL if at the end of the directory stream, and a directory entry
with `d_name' set to NULL if an error occurs. */
public EmacsDirectoryEntry
readDirectoryEntry (Cursor cursor)
{
EmacsDirectoryEntry entry;
int index;
String name, type;
entry = new EmacsDirectoryEntry ();
while (true)
{
if (!cursor.moveToNext ())
return null;
/* First, retrieve the display name. */
index = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
if (index < 0)
/* Return an invalid directory entry upon failure. */
return entry;
try
{
name = cursor.getString (index);
}
catch (Exception exception)
{
return entry;
}
/* Skip this entry if its name cannot be represented. NAME
can still be null here, since some Cursors are permitted to
return NULL if INDEX is not a string. */
if (name == null || name.equals ("..")
|| name.equals (".") || name.contains ("/")
|| name.contains ("\0"))
continue;
/* Now, look for its type. */
index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
if (index < 0)
/* Return an invalid directory entry upon failure. */
return entry;
try
{
type = cursor.getString (index);
}
catch (Exception exception)
{
return entry;
}
if (type != null
&& type.equals (Document.MIME_TYPE_DIR))
entry.d_type = 1;
entry.d_name = name;
return entry;
}
/* Not reached. */
}
/* Open a file descriptor for a file document designated by
DOCUMENTID within the document tree identified by URI. If
TRUNCATE and the document already exists, truncate its contents
before returning.
If READ && WRITE, open the file under either the `rw' or `rwt'
access mode, which implies that the value must be a seekable
on-disk file. If TRUNC && WRITE, also truncate the file after it
is opened.
If only READ or WRITE is set, value may be a non-seekable FIFO or
one end of a socket pair.
Value is NULL upon failure or a parcel file descriptor upon
success. Call `ParcelFileDescriptor.close' on this file
descriptor instead of using the `close' system call.
FileNotFoundException and/or SecurityException and
UnsupportedOperationException may be thrown upon failure. */
public ParcelFileDescriptor
openDocument (String uri, String documentId,
boolean read, boolean write, boolean truncate)
{
/* Start the thread used to run SAF requests if it isn't already
running. */
if (storageThread == null)
{
storageThread = new EmacsSafThread (resolver);
storageThread.start ();
}
return storageThread.openDocument (uri, documentId, read, write,
truncate);
}
/* Create a new document with the given display NAME within the
directory identified by DOCUMENTID inside the document tree
designated by URI.
If DOCUMENTID is NULL, create the document inside the root of
that tree.
Either FileNotFoundException, SecurityException or
UnsupportedOperationException may be thrown upon failure.
Return the document ID of the new file upon success, NULL
otherwise. */
public String
createDocument (String uri, String documentId, String name)
throws FileNotFoundException
{
String mimeType, separator, mime, extension;
int index;
MimeTypeMap singleton;
Uri treeUri, directoryUri, docUri;
/* Try to get the MIME type for this document.
Default to ``application/octet-stream''. */
mimeType = "application/octet-stream";
/* Abuse WebView stuff to get the file's MIME type. */
index = name.lastIndexOf ('.');
if (index > 0)
{
singleton = MimeTypeMap.getSingleton ();
extension = name.substring (index + 1);
mime = singleton.getMimeTypeFromExtension (extension);
if (mime != null)
mimeType = mime;
}
/* Now parse URI. */
treeUri = Uri.parse (uri);
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (treeUri);
/* And build a file URI referring to the directory. */
directoryUri
= DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
documentId);
docUri = DocumentsContract.createDocument (resolver,
directoryUri,
mimeType, name);
if (docUri == null)
return null;
/* Invalidate the file status of the containing directory. */
if (storageThread != null)
storageThread.postInvalidateStat (treeUri, documentId);
/* Return the ID of the new document. */
return DocumentsContract.getDocumentId (docUri);
}
/* Like `createDocument', but create a directory instead of an
ordinary document. */
public String
createDirectory (String uri, String documentId, String name)
throws FileNotFoundException
{
int index;
Uri treeUri, directoryUri, docUri;
/* Now parse URI. */
treeUri = Uri.parse (uri);
if (documentId == null)
documentId = DocumentsContract.getTreeDocumentId (treeUri);
/* And build a file URI referring to the directory. */
directoryUri
= DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
documentId);
/* If name ends with a directory separator character, delete
it. */
if (name.endsWith ("/"))
name = name.substring (0, name.length () - 1);
/* From Android's perspective, directories are just ordinary
documents with the `MIME_TYPE_DIR' type. */
docUri = DocumentsContract.createDocument (resolver,
directoryUri,
Document.MIME_TYPE_DIR,
name);
if (docUri == null)
return null;
/* Return the ID of the new document, but first invalidate the
state of the containing directory. */
if (storageThread != null)
storageThread.postInvalidateStat (treeUri, documentId);
return DocumentsContract.getDocumentId (docUri);
}
/* Delete the document identified by ID from the document tree
identified by URI. Return 0 upon success and -1 upon
failure.
NAME should be the name of the document being deleted, and is
used to invalidate the cache. */
public int
deleteDocument (String uri, String id, String name)
throws FileNotFoundException
{
Uri uriObject, tree;
tree = Uri.parse (uri);
uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id);
if (DocumentsContract.deleteDocument (resolver, uriObject))
{
if (storageThread != null)
storageThread.postInvalidateCache (tree, id, name);
return 0;
}
return -1;
}
/* Rename the document designated by DOCID inside the directory tree
identified by URI, which should be within the directory
designated by DIR, to NAME. If the file can't be renamed because
it doesn't support renaming, return -1, 0 otherwise. */
public int
renameDocument (String uri, String docId, String dir, String name)
throws FileNotFoundException
{
Uri tree, uriObject;
tree = Uri.parse (uri);
uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId);
if (DocumentsContract.renameDocument (resolver, uriObject,
name)
!= null)
{
/* Invalidate the cache. */
if (storageThread != null)
storageThread.postInvalidateCacheDir (tree, docId,
name);
return 0;
}
/* Handle errors specially, so `android_saf_rename_document' can
return ENXDEV. */
return -1;
}
/* Move the document designated by DOCID from the directory under
DIR_NAME designated by SRCID to the directory designated by
DSTID. If the ID of the document being moved changes as a
consequence of the movement, return the new ID, else NULL.
URI is the document tree containing all three documents. */
public String
moveDocument (String uri, String docId, String dirName,
String dstId, String srcId)
throws FileNotFoundException
{
Uri uri1, docId1, dstId1, srcId1;
Uri name;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
throw new UnsupportedOperationException ("Documents aren't capable"
+ " of being moved on Android"
+ " versions before 7.0.");
uri1 = Uri.parse (uri);
docId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, docId);
dstId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, dstId);
srcId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, srcId);
/* Move the document; this function returns the new ID of the
document should it change. */
name = DocumentsContract.moveDocument (resolver, docId1,
srcId1, dstId1);
/* Now invalidate the caches for both DIRNAME and DOCID. */
if (storageThread != null)
{
storageThread.postInvalidateCacheDir (uri1, docId, dirName);
/* Invalidate the stat cache entries for both the source and
destination directories, since their contents have
changed. */
storageThread.postInvalidateStat (uri1, dstId);
storageThread.postInvalidateStat (uri1, srcId);
}
return (name != null
? DocumentsContract.getDocumentId (name)
: null);
}
/* Return if there is a content provider by the name of AUTHORITY
supplying at least one tree URI Emacs retains persistent rights
to access. */
public boolean
validAuthority (String authority)
{
List<UriPermission> permissions;
Uri uri;
permissions = resolver.getPersistedUriPermissions ();
for (UriPermission permission : permissions)
{
uri = permission.getUri ();
if (DocumentsContract.isTreeUri (uri)
&& permission.isReadPermission ()
&& uri.getAuthority ().equals (authority))
return true;
}
return false;
}
};