/*
* 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;
}
}