Java tutorial
/* * Copyright (C) 2009 Google Inc. * * 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.NotificationManager; import android.app.PendingIntent; import android.app.Service; 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.Bundle; import android.os.Handler; import android.os.Looper; import android.os.StatFs; import android.os.Vibrator; import android.text.ClipboardManager; 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 java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * 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(); } 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; } /** * Creates a new AndroidFacade that simplifies the interface to various Android APIs. * * @param service * is the {@link Context} the APIs will run under */ @Rpc(description = "Put text in the clipboard.") public void setClipboard(@RpcParameter(name = "text") String text) { getClipboardManager().setText(text); } @Rpc(description = "Read text from the clipboard.", returns = "The text in the clipboard.") public String getClipboard() { CharSequence text = getClipboardManager().getText(); return text == null ? null : text.toString(); } 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(); } } // 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(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); } } } /** * 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 = "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 = "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() { public void onClick(DialogInterface dialog, int whichButton) { dialog.dismiss(); setResult(input.getText().toString()); finish(); } }); alert.setOnCancelListener(new DialogInterface.OnCancelListener() { 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); } @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) { Notification notification = new Notification(mResources.getLogo48(), message, System.currentTimeMillis()); // This contentIntent is a noop. PendingIntent contentIntent = PendingIntent.getService(mService, 0, new Intent(), 0); notification.setLatestEventInfo(mService, title, message, contentIntent); 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 version of SL4A is greater than or equal to 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.") public void log(@RpcParameter(name = "message") String message) { android.util.Log.v("SCRIPT", 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); result.put("download", FileUtils.getExternalDownload().getAbsolutePath()); result.put("appcache", mService.getCacheDir().getAbsolutePath()); try { StatFs fs = new StatFs("/sdcard"); space.put("availblocks", fs.getAvailableBlocks()); space.put("blocksize", fs.getBlockSize()); space.put("blockcount", fs.getBlockCount()); } 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; } }