com.googlecode.android_scripting.facade.AndroidFacade.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.android_scripting.facade.AndroidFacade.java

Source

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