/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.android_scripting.facade; import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.StatFs; import android.os.UserHandle; import android.os.Vibrator; import android.text.InputType; import android.text.method.PasswordTransformationMethod; import android.widget.EditText; import android.widget.Toast; import com.googlecode.android_scripting.BaseApplication; import com.googlecode.android_scripting.FileUtils; import com.googlecode.android_scripting.FutureActivityTaskExecutor; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.NotificationIdFactory; import com.googlecode.android_scripting.future.FutureActivityTask; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.rpc.Rpc; import com.googlecode.android_scripting.rpc.RpcDefault; import com.googlecode.android_scripting.rpc.RpcDeprecated; import com.googlecode.android_scripting.rpc.RpcOptional; import com.googlecode.android_scripting.rpc.RpcParameter; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.concurrent.TimeUnit; /** * Some general purpose Android routines.<br> * <h2>Intents</h2> Intents are returned as a map, in the following form:<br> * <ul> * <li><b>action</b> - action. * <li><b>data</b> - url * <li><b>type</b> - mime type * <li><b>packagename</b> - name of package. If used, requires classname to be useful (optional) * <li><b>classname</b> - name of class. If used, requires packagename to be useful (optional) * <li><b>categories</b> - list of categories * <li><b>extras</b> - map of extras * <li><b>flags</b> - integer flags. * </ul> * <br> * An intent can be built using the {@see #makeIntent} call, but can also be constructed exterally. * */ public class AndroidFacade extends RpcReceiver { /** * An instance of this interface is passed to the facade. From this object, the resource IDs can * be obtained. */ public interface Resources { int getLogo48(); int getStringId(String identifier); } private static final String CHANNEL_ID = "android_facade_channel"; private final Service mService; private final Handler mHandler; private final Intent mIntent; private final FutureActivityTaskExecutor mTaskQueue; private final Vibrator mVibrator; private final NotificationManager mNotificationManager; private final Resources mResources; private ClipboardManager mClipboard = null; @Override public void shutdown() { } public AndroidFacade(FacadeManager manager) { super(manager); mService = manager.getService(); mIntent = manager.getIntent(); BaseApplication application = ((BaseApplication) mService.getApplication()); mTaskQueue = application.getTaskExecutor(); mHandler = new Handler(mService.getMainLooper()); mVibrator = (Vibrator) mService.getSystemService(Context.VIBRATOR_SERVICE); mNotificationManager = (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); mResources = manager.getAndroidFacadeResources(); } ClipboardManager getClipboardManager() { Object clipboard = null; if (mClipboard == null) { try { clipboard = mService.getSystemService(Context.CLIPBOARD_SERVICE); } catch (Exception e) { Looper.prepare(); // Clipboard manager won't work without this on higher SDK levels... clipboard = mService.getSystemService(Context.CLIPBOARD_SERVICE); } mClipboard = (ClipboardManager) clipboard; if (mClipboard == null) { Log.w("Clipboard managed not accessible."); } } return mClipboard; } public Intent startActivityForResult(final Intent intent) { FutureActivityTask<Intent> task = new FutureActivityTask<Intent>() { @Override public void onCreate() { super.onCreate(); try { startActivityForResult(intent, 0); } catch (Exception e) { intent.putExtra("EXCEPTION", e.getMessage()); setResult(intent); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { setResult(data); } }; mTaskQueue.execute(task); try { return task.getResult(); } catch (Exception e) { throw new RuntimeException(e); } finally { task.finish(); } } public int startActivityForResultCodeWithTimeout(final Intent intent, final int request, final int timeout) { FutureActivityTask<Integer> task = new FutureActivityTask<Integer>() { @Override public void onCreate() { super.onCreate(); try { startActivityForResult(intent, request); } catch (Exception e) { intent.putExtra("EXCEPTION", e.getMessage()); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (request == requestCode){ setResult(resultCode); } } }; mTaskQueue.execute(task); try { return task.getResult(timeout, TimeUnit.SECONDS); } catch (Exception e) { throw new RuntimeException(e); } finally { task.finish(); } } // TODO(damonkohler): Pull this out into proper argument deserialization and support // complex/nested types being passed in. public static void putExtrasFromJsonObject(JSONObject extras, Intent intent) throws JSONException { JSONArray names = extras.names(); for (int i = 0; i < names.length(); i++) { String name = names.getString(i); Object data = extras.get(name); if (data == null) { continue; } if (data instanceof Integer) { intent.putExtra(name, (Integer) data); } if (data instanceof Float) { intent.putExtra(name, (Float) data); } if (data instanceof Double) { intent.putExtra(name, (Double) data); } if (data instanceof Long) { intent.putExtra(name, (Long) data); } if (data instanceof String) { intent.putExtra(name, (String) data); } if (data instanceof Boolean) { intent.putExtra(name, (Boolean) data); } // Nested JSONObject if (data instanceof JSONObject) { Bundle nestedBundle = new Bundle(); intent.putExtra(name, nestedBundle); putNestedJSONObject((JSONObject) data, nestedBundle); } // Nested JSONArray. Doesn't support mixed types in single array if (data instanceof JSONArray) { // Empty array. No way to tell what type of data to pass on, so skipping if (((JSONArray) data).length() == 0) { Log.e("Empty array not supported in JSONObject, skipping"); continue; } // Integer if (((JSONArray) data).get(0) instanceof Integer) { Integer[] integerArrayData = new Integer[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { integerArrayData[j] = ((JSONArray) data).getInt(j); } intent.putExtra(name, integerArrayData); } // Double if (((JSONArray) data).get(0) instanceof Double) { Double[] doubleArrayData = new Double[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { doubleArrayData[j] = ((JSONArray) data).getDouble(j); } intent.putExtra(name, doubleArrayData); } // Long if (((JSONArray) data).get(0) instanceof Long) { Long[] longArrayData = new Long[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { longArrayData[j] = ((JSONArray) data).getLong(j); } intent.putExtra(name, longArrayData); } // String if (((JSONArray) data).get(0) instanceof String) { String[] stringArrayData = new String[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { stringArrayData[j] = ((JSONArray) data).getString(j); } intent.putExtra(name, stringArrayData); } // Boolean if (((JSONArray) data).get(0) instanceof Boolean) { Boolean[] booleanArrayData = new Boolean[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { booleanArrayData[j] = ((JSONArray) data).getBoolean(j); } intent.putExtra(name, booleanArrayData); } } } } // Contributed by Emmanuel T // Nested Array handling contributed by Sergey Zelenev private static void putNestedJSONObject(JSONObject jsonObject, Bundle bundle) throws JSONException { JSONArray names = jsonObject.names(); for (int i = 0; i < names.length(); i++) { String name = names.getString(i); Object data = jsonObject.get(name); if (data == null) { continue; } if (data instanceof Integer) { bundle.putInt(name, ((Integer) data).intValue()); } if (data instanceof Float) { bundle.putFloat(name, ((Float) data).floatValue()); } if (data instanceof Double) { bundle.putDouble(name, ((Double) data).doubleValue()); } if (data instanceof Long) { bundle.putLong(name, ((Long) data).longValue()); } if (data instanceof String) { bundle.putString(name, (String) data); } if (data instanceof Boolean) { bundle.putBoolean(name, ((Boolean) data).booleanValue()); } // Nested JSONObject if (data instanceof JSONObject) { Bundle nestedBundle = new Bundle(); bundle.putBundle(name, nestedBundle); putNestedJSONObject((JSONObject) data, nestedBundle); } // Nested JSONArray. Doesn't support mixed types in single array if (data instanceof JSONArray) { // Empty array. No way to tell what type of data to pass on, so skipping if (((JSONArray) data).length() == 0) { Log.e("Empty array not supported in nested JSONObject, skipping"); continue; } // Integer if (((JSONArray) data).get(0) instanceof Integer) { int[] integerArrayData = new int[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { integerArrayData[j] = ((JSONArray) data).getInt(j); } bundle.putIntArray(name, integerArrayData); } // Double if (((JSONArray) data).get(0) instanceof Double) { double[] doubleArrayData = new double[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { doubleArrayData[j] = ((JSONArray) data).getDouble(j); } bundle.putDoubleArray(name, doubleArrayData); } // Long if (((JSONArray) data).get(0) instanceof Long) { long[] longArrayData = new long[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { longArrayData[j] = ((JSONArray) data).getLong(j); } bundle.putLongArray(name, longArrayData); } // String if (((JSONArray) data).get(0) instanceof String) { String[] stringArrayData = new String[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { stringArrayData[j] = ((JSONArray) data).getString(j); } bundle.putStringArray(name, stringArrayData); } // Boolean if (((JSONArray) data).get(0) instanceof Boolean) { boolean[] booleanArrayData = new boolean[((JSONArray) data).length()]; for (int j = 0; j < ((JSONArray) data).length(); ++j) { booleanArrayData[j] = ((JSONArray) data).getBoolean(j); } bundle.putBooleanArray(name, booleanArrayData); } } } } void startActivity(final Intent intent) { try { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mService.startActivity(intent); } catch (Exception e) { Log.e("Failed to launch intent.", e); } } private Intent buildIntent(String action, String uri, String type, JSONObject extras, String packagename, String classname, JSONArray categories) throws JSONException { Intent intent = new Intent(); if (action != null) { intent.setAction(action); } intent.setDataAndType(uri != null ? Uri.parse(uri) : null, type); if (packagename != null && classname != null) { intent.setComponent(new ComponentName(packagename, classname)); } if (extras != null) { putExtrasFromJsonObject(extras, intent); } if (categories != null) { for (int i = 0; i < categories.length(); i++) { intent.addCategory(categories.getString(i)); } } return intent; } // TODO(damonkohler): It's unnecessary to add the complication of choosing between startActivity // and startActivityForResult. It's probably better to just always use the ForResult version. // However, this makes the call always blocking. We'd need to add an extra boolean parameter to // indicate if we should wait for a result. @Rpc(description = "Starts an activity and returns the result.", returns = "A Map representation of the result Intent.") public Intent startActivityForResult( @RpcParameter(name = "action") String action, @RpcParameter(name = "uri") @RpcOptional String uri, @RpcParameter(name = "type", description = "MIME type/subtype of the URI") @RpcOptional String type, @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent") @RpcOptional JSONObject extras, @RpcParameter(name = "packagename", description = "name of package. If used, requires classname to be useful") @RpcOptional String packagename, @RpcParameter(name = "classname", description = "name of class. If used, requires packagename to be useful") @RpcOptional String classname ) throws JSONException { final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null); return startActivityForResult(intent); } @Rpc(description = "Starts an activity and returns the result.", returns = "A Map representation of the result Intent.") public Intent startActivityForResultIntent( @RpcParameter(name = "intent", description = "Intent in the format as returned from makeIntent") Intent intent) { return startActivityForResult(intent); } private void doStartActivity(final Intent intent, Boolean wait) throws Exception { if (wait == null || wait == false) { startActivity(intent); } else { FutureActivityTask<Intent> task = new FutureActivityTask<Intent>() { private boolean mSecondResume = false; @Override public void onCreate() { super.onCreate(); startActivity(intent); } @Override public void onResume() { if (mSecondResume) { finish(); } mSecondResume = true; } @Override public void onDestroy() { setResult(null); } }; mTaskQueue.execute(task); try { task.getResult(); } catch (Exception e) { throw new RuntimeException(e); } } } @Rpc(description = "Put a text string in the clipboard.") public void setTextClip(@RpcParameter(name = "text") String text, @RpcParameter(name = "label") @RpcOptional @RpcDefault(value = "copiedText") String label) { getClipboardManager().setPrimaryClip(ClipData.newPlainText(label, text)); } @Rpc(description = "Get the device serial number.") public String getBuildSerial() { return Build.SERIAL; } @Rpc(description = "Get the name of system bootloader version number.") public String getBuildBootloader() { return android.os.Build.BOOTLOADER; } @Rpc(description = "Get the name of the industrial design.") public String getBuildIndustrialDesignName() { return Build.DEVICE; } @Rpc(description = "Get the build ID string meant for displaying to the user") public String getBuildDisplay() { return Build.DISPLAY; } @Rpc(description = "Get the string that uniquely identifies this build.") public String getBuildFingerprint() { return Build.FINGERPRINT; } @Rpc(description = "Get the name of the hardware (from the kernel command " + "line or /proc)..") public String getBuildHardware() { return Build.HARDWARE; } @Rpc(description = "Get the device host.") public String getBuildHost() { return Build.HOST; } @Rpc(description = "Get Either a changelist number, or a label like." + " \"M4-rc20\".") public String getBuildID() { return android.os.Build.ID; } @Rpc(description = "Returns true if we are running a debug build such" + " as \"user-debug\" or \"eng\".") public boolean getBuildIsDebuggable() { return Build.IS_DEBUGGABLE; } @Rpc(description = "Get the name of the overall product.") public String getBuildProduct() { return android.os.Build.PRODUCT; } @Rpc(description = "Get an ordered list of 32 bit ABIs supported by this " + "device. The most preferred ABI is the first element in the list") public String[] getBuildSupported32BitAbis() { return Build.SUPPORTED_32_BIT_ABIS; } @Rpc(description = "Get an ordered list of 64 bit ABIs supported by this " + "device. The most preferred ABI is the first element in the list") public String[] getBuildSupported64BitAbis() { return Build.SUPPORTED_64_BIT_ABIS; } @Rpc(description = "Get an ordered list of ABIs supported by this " + "device. The most preferred ABI is the first element in the list") public String[] getBuildSupportedBitAbis() { return Build.SUPPORTED_ABIS; } @Rpc(description = "Get comma-separated tags describing the build," + " like \"unsigned,debug\".") public String getBuildTags() { return Build.TAGS; } @Rpc(description = "Get The type of build, like \"user\" or \"eng\".") public String getBuildType() { return Build.TYPE; } @Rpc(description = "Returns the board name.") public String getBuildBoard() { return Build.BOARD; } @Rpc(description = "Returns the brand name.") public String getBuildBrand() { return Build.BRAND; } @Rpc(description = "Returns the manufacturer name.") public String getBuildManufacturer() { return Build.MANUFACTURER; } @Rpc(description = "Returns the model name.") public String getBuildModel() { return Build.MODEL; } @Rpc(description = "Returns the build number.") public String getBuildNumber() { return Build.FINGERPRINT; } @Rpc(description = "Returns the SDK version.") public Integer getBuildSdkVersion() { return Build.VERSION.SDK_INT; } @Rpc(description = "Returns the current device time.") public Long getBuildTime() { return Build.TIME; } @Rpc(description = "Read all text strings copied by setTextClip from the clipboard.") public List<String> getTextClip() { ClipboardManager cm = getClipboardManager(); ArrayList<String> texts = new ArrayList<String>(); if(!cm.hasPrimaryClip()) { return texts; } ClipData cd = cm.getPrimaryClip(); for(int i=0; i<cd.getItemCount(); i++) { texts.add(cd.getItemAt(i).coerceToText(mService).toString()); } return texts; } /** * packagename and classname, if provided, are used in a 'setComponent' call. */ @Rpc(description = "Starts an activity.") public void startActivity( @RpcParameter(name = "action") String action, @RpcParameter(name = "uri") @RpcOptional String uri, @RpcParameter(name = "type", description = "MIME type/subtype of the URI") @RpcOptional String type, @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent") @RpcOptional JSONObject extras, @RpcParameter(name = "wait", description = "block until the user exits the started activity") @RpcOptional Boolean wait, @RpcParameter(name = "packagename", description = "name of package. If used, requires classname to be useful") @RpcOptional String packagename, @RpcParameter(name = "classname", description = "name of class. If used, requires packagename to be useful") @RpcOptional String classname ) throws Exception { final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null); doStartActivity(intent, wait); } @Rpc(description = "Send a broadcast.") public void sendBroadcast( @RpcParameter(name = "action") String action, @RpcParameter(name = "uri") @RpcOptional String uri, @RpcParameter(name = "type", description = "MIME type/subtype of the URI") @RpcOptional String type, @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent") @RpcOptional JSONObject extras, @RpcParameter(name = "packagename", description = "name of package. If used, requires classname to be useful") @RpcOptional String packagename, @RpcParameter(name = "classname", description = "name of class. If used, requires packagename to be useful") @RpcOptional String classname ) throws JSONException { final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null); try { mService.sendBroadcast(intent); } catch (Exception e) { Log.e("Failed to broadcast intent.", e); } } @Rpc(description = "Starts a service.") public void startService( @RpcParameter(name = "uri") @RpcOptional String uri, @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent") @RpcOptional JSONObject extras, @RpcParameter(name = "packagename", description = "name of package. If used, requires classname to be useful") @RpcOptional String packagename, @RpcParameter(name = "classname", description = "name of class. If used, requires packagename to be useful") @RpcOptional String classname ) throws Exception { final Intent intent = buildIntent(null /* action */, uri, null /* type */, extras, packagename, classname, null /* categories */); mService.startService(intent); } @Rpc(description = "Create an Intent.", returns = "An object representing an Intent") public Intent makeIntent( @RpcParameter(name = "action") String action, @RpcParameter(name = "uri") @RpcOptional String uri, @RpcParameter(name = "type", description = "MIME type/subtype of the URI") @RpcOptional String type, @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent") @RpcOptional JSONObject extras, @RpcParameter(name = "categories", description = "a List of categories to add to the Intent") @RpcOptional JSONArray categories, @RpcParameter(name = "packagename", description = "name of package. If used, requires classname to be useful") @RpcOptional String packagename, @RpcParameter(name = "classname", description = "name of class. If used, requires packagename to be useful") @RpcOptional String classname, @RpcParameter(name = "flags", description = "Intent flags") @RpcOptional Integer flags ) throws JSONException { Intent intent = buildIntent(action, uri, type, extras, packagename, classname, categories); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (flags != null) { intent.setFlags(flags); } return intent; } @Rpc(description = "Start Activity using Intent") public void startActivityIntent( @RpcParameter(name = "intent", description = "Intent in the format as returned from makeIntent") Intent intent, @RpcParameter(name = "wait", description = "block until the user exits the started activity") @RpcOptional Boolean wait ) throws Exception { doStartActivity(intent, wait); } @Rpc(description = "Send Broadcast Intent") public void sendBroadcastIntent( @RpcParameter(name = "intent", description = "Intent in the format as returned from makeIntent") Intent intent ) throws Exception { mService.sendBroadcast(intent); } @Rpc(description = "Start Service using Intent") public void startServiceIntent( @RpcParameter(name = "intent", description = "Intent in the format as returned from makeIntent") Intent intent ) throws Exception { mService.startService(intent); } @Rpc(description = "Send Broadcast Intent as system user.") public void sendBroadcastIntentAsUserAll( @RpcParameter(name = "intent", description = "Intent in the format as returned from makeIntent") Intent intent ) throws Exception { mService.sendBroadcastAsUser(intent, UserHandle.ALL); } @Rpc(description = "Vibrates the phone or a specified duration in milliseconds.") public void vibrate( @RpcParameter(name = "duration", description = "duration in milliseconds") @RpcDefault("300") Integer duration) { mVibrator.vibrate(duration); } @Rpc(description = "Displays a short-duration Toast notification.") public void makeToast(@RpcParameter(name = "message") final String message) { mHandler.post(new Runnable() { public void run() { Toast.makeText(mService, message, Toast.LENGTH_SHORT).show(); } }); } private String getInputFromAlertDialog(final String title, final String message, final boolean password) { final FutureActivityTask<String> task = new FutureActivityTask<String>() { @Override public void onCreate() { super.onCreate(); final EditText input = new EditText(getActivity()); if (password) { input.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); input.setTransformationMethod(new PasswordTransformationMethod()); } AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); alert.setTitle(title); alert.setMessage(message); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { dialog.dismiss(); setResult(input.getText().toString()); finish(); } }); alert.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { dialog.dismiss(); setResult(null); finish(); } }); alert.show(); } }; mTaskQueue.execute(task); try { return task.getResult(); } catch (Exception e) { Log.e("Failed to display dialog.", e); throw new RuntimeException(e); } } @Rpc(description = "Queries the user for a text input.") @RpcDeprecated(value = "dialogGetInput", release = "r3") public String getInput( @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("SL4A Input") final String title, @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message) { return getInputFromAlertDialog(title, message, false); } @Rpc(description = "Queries the user for a password.") @RpcDeprecated(value = "dialogGetPassword", release = "r3") public String getPassword( @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("SL4A Password Input") final String title, @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message) { return getInputFromAlertDialog(title, message, true); } private void createNotificationChannel() { CharSequence name = mService.getString(mResources.getStringId("notification_channel_name")); String description = mService.getString(mResources.getStringId("notification_channel_description")); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); channel.setDescription(description); channel.enableLights(false); channel.enableVibration(false); mNotificationManager.createNotificationChannel(channel); } @Rpc(description = "Displays a notification that will be canceled when the user clicks on it.") public void notify(@RpcParameter(name = "title", description = "title") String title, @RpcParameter(name = "message") String message) { createNotificationChannel(); // This contentIntent is a noop. PendingIntent contentIntent = PendingIntent.getService(mService, 0, new Intent(), 0); Notification.Builder builder = new Notification.Builder(mService, CHANNEL_ID); builder.setSmallIcon(mResources.getLogo48()) .setTicker(message) .setWhen(System.currentTimeMillis()) .setContentTitle(title) .setContentText(message) .setContentIntent(contentIntent); Notification notification = builder.build(); notification.flags = Notification.FLAG_AUTO_CANCEL; // Get a unique notification id from the application. final int notificationId = NotificationIdFactory.create(); mNotificationManager.notify(notificationId, notification); } @Rpc(description = "Returns the intent that launched the script.") public Object getIntent() { return mIntent; } @Rpc(description = "Launches an activity that sends an e-mail message to a given recipient.") public void sendEmail( @RpcParameter(name = "to", description = "A comma separated list of recipients.") final String to, @RpcParameter(name = "subject") final String subject, @RpcParameter(name = "body") final String body, @RpcParameter(name = "attachmentUri") @RpcOptional final String attachmentUri) { final Intent intent = new Intent(android.content.Intent.ACTION_SEND); intent.setType("plain/text"); intent.putExtra(android.content.Intent.EXTRA_EMAIL, to.split(",")); intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject); intent.putExtra(android.content.Intent.EXTRA_TEXT, body); if (attachmentUri != null) { intent.putExtra(android.content.Intent.EXTRA_STREAM, Uri.parse(attachmentUri)); } startActivity(intent); } @Rpc(description = "Returns package version code.") public int getPackageVersionCode(@RpcParameter(name = "packageName") final String packageName) { int result = -1; PackageInfo pInfo = null; try { pInfo = mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA); } catch (NameNotFoundException e) { pInfo = null; } if (pInfo != null) { result = pInfo.versionCode; } return result; } @Rpc(description = "Returns package version name.") public String getPackageVersion(@RpcParameter(name = "packageName") final String packageName) { PackageInfo packageInfo = null; try { packageInfo = mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA); } catch (NameNotFoundException e) { return null; } if (packageInfo != null) { return packageInfo.versionName; } return null; } @Rpc(description = "Checks if SL4A's version is >= the specified version.") public boolean requiredVersion( @RpcParameter(name = "requiredVersion") final Integer version) { boolean result = false; int packageVersion = getPackageVersionCode( "com.googlecode.android_scripting"); if (version > -1) { result = (packageVersion >= version); } return result; } @Rpc(description = "Writes message to logcat at verbose level") public void logV( @RpcParameter(name = "message") String message) { android.util.Log.v("SL4A: ", message); } @Rpc(description = "Writes message to logcat at info level") public void logI( @RpcParameter(name = "message") String message) { android.util.Log.i("SL4A: ", message); } @Rpc(description = "Writes message to logcat at debug level") public void logD( @RpcParameter(name = "message") String message) { android.util.Log.d("SL4A: ", message); } @Rpc(description = "Writes message to logcat at warning level") public void logW( @RpcParameter(name = "message") String message) { android.util.Log.w("SL4A: ", message); } @Rpc(description = "Writes message to logcat at error level") public void logE( @RpcParameter(name = "message") String message) { android.util.Log.e("SL4A: ", message); } @Rpc(description = "Writes message to logcat at wtf level") public void logWTF( @RpcParameter(name = "message") String message) { android.util.Log.wtf("SL4A: ", message); } /** * * Map returned: * * <pre> * TZ = Timezone * id = Timezone ID * display = Timezone display name * offset = Offset from UTC (in ms) * SDK = SDK Version * download = default download path * appcache = Location of application cache * sdcard = Space on sdcard * availblocks = Available blocks * blockcount = Total Blocks * blocksize = size of block. * </pre> */ @Rpc(description = "A map of various useful environment details") public Map<String, Object> environment() { Map<String, Object> result = new HashMap<String, Object>(); Map<String, Object> zone = new HashMap<String, Object>(); Map<String, Object> space = new HashMap<String, Object>(); TimeZone tz = TimeZone.getDefault(); zone.put("id", tz.getID()); zone.put("display", tz.getDisplayName()); zone.put("offset", tz.getOffset((new Date()).getTime())); result.put("TZ", zone); result.put("SDK", android.os.Build.VERSION.SDK_INT); result.put("download", FileUtils.getExternalDownload().getAbsolutePath()); result.put("appcache", mService.getCacheDir().getAbsolutePath()); try { StatFs fs = new StatFs("/sdcard"); space.put("availblocks", fs.getAvailableBlocksLong()); space.put("blocksize", fs.getBlockSizeLong()); space.put("blockcount", fs.getBlockCountLong()); } catch (Exception e) { space.put("exception", e.toString()); } result.put("sdcard", space); return result; } @Rpc(description = "Get list of constants (static final fields) for a class") public Bundle getConstants( @RpcParameter(name = "classname", description = "Class to get constants from") String classname) throws Exception { Bundle result = new Bundle(); int flags = Modifier.FINAL | Modifier.PUBLIC | Modifier.STATIC; Class<?> clazz = Class.forName(classname); for (Field field : clazz.getFields()) { if ((field.getModifiers() & flags) == flags) { Class<?> type = field.getType(); String name = field.getName(); if (type == int.class) { result.putInt(name, field.getInt(null)); } else if (type == long.class) { result.putLong(name, field.getLong(null)); } else if (type == double.class) { result.putDouble(name, field.getDouble(null)); } else if (type == char.class) { result.putChar(name, field.getChar(null)); } else if (type instanceof Object) { result.putString(name, field.get(null).toString()); } } } return result; } }