Home | History | Annotate | Download | only in facade
      1 /*
      2  * Copyright (C) 2016 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.googlecode.android_scripting.facade;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Notification;
     22 import android.app.NotificationManager;
     23 import android.app.PendingIntent;
     24 import android.app.Service;
     25 import android.content.ClipData;
     26 import android.content.ComponentName;
     27 import android.content.Context;
     28 import android.content.DialogInterface;
     29 import android.content.Intent;
     30 import android.content.pm.PackageInfo;
     31 import android.content.pm.PackageManager;
     32 import android.content.pm.PackageManager.NameNotFoundException;
     33 import android.net.Uri;
     34 import android.os.Build;
     35 import android.os.Bundle;
     36 import android.os.Handler;
     37 import android.os.Looper;
     38 import android.os.StatFs;
     39 import android.os.UserHandle;
     40 import android.os.Vibrator;
     41 import android.content.ClipboardManager;
     42 import android.text.InputType;
     43 import android.text.method.PasswordTransformationMethod;
     44 import android.widget.EditText;
     45 import android.widget.Toast;
     46 
     47 import com.googlecode.android_scripting.BaseApplication;
     48 import com.googlecode.android_scripting.FileUtils;
     49 import com.googlecode.android_scripting.FutureActivityTaskExecutor;
     50 import com.googlecode.android_scripting.Log;
     51 import com.googlecode.android_scripting.NotificationIdFactory;
     52 import com.googlecode.android_scripting.future.FutureActivityTask;
     53 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
     54 import com.googlecode.android_scripting.rpc.Rpc;
     55 import com.googlecode.android_scripting.rpc.RpcDefault;
     56 import com.googlecode.android_scripting.rpc.RpcDeprecated;
     57 import com.googlecode.android_scripting.rpc.RpcOptional;
     58 import com.googlecode.android_scripting.rpc.RpcParameter;
     59 
     60 import java.lang.reflect.Field;
     61 import java.lang.reflect.Modifier;
     62 import java.util.ArrayList;
     63 import java.util.Date;
     64 import java.util.HashMap;
     65 import java.util.List;
     66 import java.util.Map;
     67 import java.util.TimeZone;
     68 import java.util.concurrent.TimeUnit;
     69 
     70 import org.json.JSONArray;
     71 import org.json.JSONException;
     72 import org.json.JSONObject;
     73 
     74 /**
     75  * Some general purpose Android routines.<br>
     76  * <h2>Intents</h2> Intents are returned as a map, in the following form:<br>
     77  * <ul>
     78  * <li><b>action</b> - action.
     79  * <li><b>data</b> - url
     80  * <li><b>type</b> - mime type
     81  * <li><b>packagename</b> - name of package. If used, requires classname to be useful (optional)
     82  * <li><b>classname</b> - name of class. If used, requires packagename to be useful (optional)
     83  * <li><b>categories</b> - list of categories
     84  * <li><b>extras</b> - map of extras
     85  * <li><b>flags</b> - integer flags.
     86  * </ul>
     87  * <br>
     88  * An intent can be built using the {@see #makeIntent} call, but can also be constructed exterally.
     89  *
     90  */
     91 public class AndroidFacade extends RpcReceiver {
     92   /**
     93    * An instance of this interface is passed to the facade. From this object, the resource IDs can
     94    * be obtained.
     95    */
     96 
     97   public interface Resources {
     98     int getLogo48();
     99   }
    100 
    101   private final Service mService;
    102   private final Handler mHandler;
    103   private final Intent mIntent;
    104   private final FutureActivityTaskExecutor mTaskQueue;
    105 
    106   private final Vibrator mVibrator;
    107   private final NotificationManager mNotificationManager;
    108 
    109   private final Resources mResources;
    110   private ClipboardManager mClipboard = null;
    111 
    112   @Override
    113   public void shutdown() {
    114   }
    115 
    116   public AndroidFacade(FacadeManager manager) {
    117     super(manager);
    118     mService = manager.getService();
    119     mIntent = manager.getIntent();
    120     BaseApplication application = ((BaseApplication) mService.getApplication());
    121     mTaskQueue = application.getTaskExecutor();
    122     mHandler = new Handler(mService.getMainLooper());
    123     mVibrator = (Vibrator) mService.getSystemService(Context.VIBRATOR_SERVICE);
    124     mNotificationManager =
    125         (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
    126     mResources = manager.getAndroidFacadeResources();
    127   }
    128 
    129   ClipboardManager getClipboardManager() {
    130     Object clipboard = null;
    131     if (mClipboard == null) {
    132       try {
    133         clipboard = mService.getSystemService(Context.CLIPBOARD_SERVICE);
    134       } catch (Exception e) {
    135         Looper.prepare(); // Clipboard manager won't work without this on higher SDK levels...
    136         clipboard = mService.getSystemService(Context.CLIPBOARD_SERVICE);
    137       }
    138       mClipboard = (ClipboardManager) clipboard;
    139       if (mClipboard == null) {
    140         Log.w("Clipboard managed not accessible.");
    141       }
    142     }
    143     return mClipboard;
    144   }
    145 
    146   public Intent startActivityForResult(final Intent intent) {
    147     FutureActivityTask<Intent> task = new FutureActivityTask<Intent>() {
    148       @Override
    149       public void onCreate() {
    150         super.onCreate();
    151         try {
    152           startActivityForResult(intent, 0);
    153         } catch (Exception e) {
    154           intent.putExtra("EXCEPTION", e.getMessage());
    155           setResult(intent);
    156         }
    157       }
    158 
    159       @Override
    160       public void onActivityResult(int requestCode, int resultCode, Intent data) {
    161         setResult(data);
    162       }
    163     };
    164     mTaskQueue.execute(task);
    165 
    166     try {
    167       return task.getResult();
    168     } catch (Exception e) {
    169       throw new RuntimeException(e);
    170     } finally {
    171       task.finish();
    172     }
    173   }
    174 
    175   public int startActivityForResultCodeWithTimeout(final Intent intent,
    176     final int request, final int timeout) {
    177     FutureActivityTask<Integer> task = new FutureActivityTask<Integer>() {
    178       @Override
    179       public void onCreate() {
    180         super.onCreate();
    181         try {
    182           startActivityForResult(intent, request);
    183         } catch (Exception e) {
    184           intent.putExtra("EXCEPTION", e.getMessage());
    185         }
    186       }
    187 
    188       @Override
    189       public void onActivityResult(int requestCode, int resultCode, Intent data) {
    190         if (request == requestCode){
    191             setResult(resultCode);
    192         }
    193       }
    194     };
    195     mTaskQueue.execute(task);
    196 
    197     try {
    198       return task.getResult(timeout, TimeUnit.SECONDS);
    199     } catch (Exception e) {
    200       throw new RuntimeException(e);
    201     } finally {
    202       task.finish();
    203     }
    204   }
    205 
    206   // TODO(damonkohler): Pull this out into proper argument deserialization and support
    207   // complex/nested types being passed in.
    208   public static void putExtrasFromJsonObject(JSONObject extras,
    209                                              Intent intent) throws JSONException {
    210     JSONArray names = extras.names();
    211     for (int i = 0; i < names.length(); i++) {
    212       String name = names.getString(i);
    213       Object data = extras.get(name);
    214       if (data == null) {
    215         continue;
    216       }
    217       if (data instanceof Integer) {
    218         intent.putExtra(name, (Integer) data);
    219       }
    220       if (data instanceof Float) {
    221         intent.putExtra(name, (Float) data);
    222       }
    223       if (data instanceof Double) {
    224         intent.putExtra(name, (Double) data);
    225       }
    226       if (data instanceof Long) {
    227         intent.putExtra(name, (Long) data);
    228       }
    229       if (data instanceof String) {
    230         intent.putExtra(name, (String) data);
    231       }
    232       if (data instanceof Boolean) {
    233         intent.putExtra(name, (Boolean) data);
    234       }
    235       // Nested JSONObject
    236       if (data instanceof JSONObject) {
    237         Bundle nestedBundle = new Bundle();
    238         intent.putExtra(name, nestedBundle);
    239         putNestedJSONObject((JSONObject) data, nestedBundle);
    240       }
    241       // Nested JSONArray. Doesn't support mixed types in single array
    242       if (data instanceof JSONArray) {
    243         // Empty array. No way to tell what type of data to pass on, so skipping
    244         if (((JSONArray) data).length() == 0) {
    245           Log.e("Empty array not supported in JSONObject, skipping");
    246           continue;
    247         }
    248         // Integer
    249         if (((JSONArray) data).get(0) instanceof Integer) {
    250           Integer[] integerArrayData = new Integer[((JSONArray) data).length()];
    251           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    252             integerArrayData[j] = ((JSONArray) data).getInt(j);
    253           }
    254           intent.putExtra(name, integerArrayData);
    255         }
    256         // Double
    257         if (((JSONArray) data).get(0) instanceof Double) {
    258           Double[] doubleArrayData = new Double[((JSONArray) data).length()];
    259           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    260             doubleArrayData[j] = ((JSONArray) data).getDouble(j);
    261           }
    262           intent.putExtra(name, doubleArrayData);
    263         }
    264         // Long
    265         if (((JSONArray) data).get(0) instanceof Long) {
    266           Long[] longArrayData = new Long[((JSONArray) data).length()];
    267           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    268             longArrayData[j] = ((JSONArray) data).getLong(j);
    269           }
    270           intent.putExtra(name, longArrayData);
    271         }
    272         // String
    273         if (((JSONArray) data).get(0) instanceof String) {
    274           String[] stringArrayData = new String[((JSONArray) data).length()];
    275           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    276             stringArrayData[j] = ((JSONArray) data).getString(j);
    277           }
    278           intent.putExtra(name, stringArrayData);
    279         }
    280         // Boolean
    281         if (((JSONArray) data).get(0) instanceof Boolean) {
    282           Boolean[] booleanArrayData = new Boolean[((JSONArray) data).length()];
    283           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    284             booleanArrayData[j] = ((JSONArray) data).getBoolean(j);
    285           }
    286           intent.putExtra(name, booleanArrayData);
    287         }
    288       }
    289     }
    290   }
    291 
    292   // Contributed by Emmanuel T
    293   // Nested Array handling contributed by Sergey Zelenev
    294   private static void putNestedJSONObject(JSONObject jsonObject, Bundle bundle)
    295       throws JSONException {
    296     JSONArray names = jsonObject.names();
    297     for (int i = 0; i < names.length(); i++) {
    298       String name = names.getString(i);
    299       Object data = jsonObject.get(name);
    300       if (data == null) {
    301         continue;
    302       }
    303       if (data instanceof Integer) {
    304         bundle.putInt(name, ((Integer) data).intValue());
    305       }
    306       if (data instanceof Float) {
    307         bundle.putFloat(name, ((Float) data).floatValue());
    308       }
    309       if (data instanceof Double) {
    310         bundle.putDouble(name, ((Double) data).doubleValue());
    311       }
    312       if (data instanceof Long) {
    313         bundle.putLong(name, ((Long) data).longValue());
    314       }
    315       if (data instanceof String) {
    316         bundle.putString(name, (String) data);
    317       }
    318       if (data instanceof Boolean) {
    319         bundle.putBoolean(name, ((Boolean) data).booleanValue());
    320       }
    321       // Nested JSONObject
    322       if (data instanceof JSONObject) {
    323         Bundle nestedBundle = new Bundle();
    324         bundle.putBundle(name, nestedBundle);
    325         putNestedJSONObject((JSONObject) data, nestedBundle);
    326       }
    327       // Nested JSONArray. Doesn't support mixed types in single array
    328       if (data instanceof JSONArray) {
    329         // Empty array. No way to tell what type of data to pass on, so skipping
    330         if (((JSONArray) data).length() == 0) {
    331           Log.e("Empty array not supported in nested JSONObject, skipping");
    332           continue;
    333         }
    334         // Integer
    335         if (((JSONArray) data).get(0) instanceof Integer) {
    336           int[] integerArrayData = new int[((JSONArray) data).length()];
    337           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    338             integerArrayData[j] = ((JSONArray) data).getInt(j);
    339           }
    340           bundle.putIntArray(name, integerArrayData);
    341         }
    342         // Double
    343         if (((JSONArray) data).get(0) instanceof Double) {
    344           double[] doubleArrayData = new double[((JSONArray) data).length()];
    345           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    346             doubleArrayData[j] = ((JSONArray) data).getDouble(j);
    347           }
    348           bundle.putDoubleArray(name, doubleArrayData);
    349         }
    350         // Long
    351         if (((JSONArray) data).get(0) instanceof Long) {
    352           long[] longArrayData = new long[((JSONArray) data).length()];
    353           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    354             longArrayData[j] = ((JSONArray) data).getLong(j);
    355           }
    356           bundle.putLongArray(name, longArrayData);
    357         }
    358         // String
    359         if (((JSONArray) data).get(0) instanceof String) {
    360           String[] stringArrayData = new String[((JSONArray) data).length()];
    361           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    362             stringArrayData[j] = ((JSONArray) data).getString(j);
    363           }
    364           bundle.putStringArray(name, stringArrayData);
    365         }
    366         // Boolean
    367         if (((JSONArray) data).get(0) instanceof Boolean) {
    368           boolean[] booleanArrayData = new boolean[((JSONArray) data).length()];
    369           for (int j = 0; j < ((JSONArray) data).length(); ++j) {
    370             booleanArrayData[j] = ((JSONArray) data).getBoolean(j);
    371           }
    372           bundle.putBooleanArray(name, booleanArrayData);
    373         }
    374       }
    375     }
    376   }
    377 
    378   void startActivity(final Intent intent) {
    379     try {
    380       intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    381       mService.startActivity(intent);
    382     } catch (Exception e) {
    383       Log.e("Failed to launch intent.", e);
    384     }
    385   }
    386 
    387   private Intent buildIntent(String action, String uri, String type, JSONObject extras,
    388       String packagename, String classname, JSONArray categories) throws JSONException {
    389     Intent intent = new Intent();
    390     if (action != null) {
    391       intent.setAction(action);
    392     }
    393     intent.setDataAndType(uri != null ? Uri.parse(uri) : null, type);
    394     if (packagename != null && classname != null) {
    395       intent.setComponent(new ComponentName(packagename, classname));
    396     }
    397     if (extras != null) {
    398       putExtrasFromJsonObject(extras, intent);
    399     }
    400     if (categories != null) {
    401       for (int i = 0; i < categories.length(); i++) {
    402         intent.addCategory(categories.getString(i));
    403       }
    404     }
    405     return intent;
    406   }
    407 
    408   // TODO(damonkohler): It's unnecessary to add the complication of choosing between startActivity
    409   // and startActivityForResult. It's probably better to just always use the ForResult version.
    410   // However, this makes the call always blocking. We'd need to add an extra boolean parameter to
    411   // indicate if we should wait for a result.
    412   @Rpc(description = "Starts an activity and returns the result.",
    413        returns = "A Map representation of the result Intent.")
    414   public Intent startActivityForResult(
    415       @RpcParameter(name = "action")
    416       String action,
    417       @RpcParameter(name = "uri")
    418       @RpcOptional String uri,
    419       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
    420       @RpcOptional String type,
    421       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
    422       @RpcOptional JSONObject extras,
    423       @RpcParameter(name = "packagename",
    424                     description = "name of package. If used, requires classname to be useful")
    425       @RpcOptional String packagename,
    426       @RpcParameter(name = "classname",
    427                     description = "name of class. If used, requires packagename to be useful")
    428       @RpcOptional String classname
    429       ) throws JSONException {
    430     final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
    431     return startActivityForResult(intent);
    432   }
    433 
    434   @Rpc(description = "Starts an activity and returns the result.",
    435        returns = "A Map representation of the result Intent.")
    436   public Intent startActivityForResultIntent(
    437       @RpcParameter(name = "intent",
    438                     description = "Intent in the format as returned from makeIntent")
    439       Intent intent) {
    440     return startActivityForResult(intent);
    441   }
    442 
    443   private void doStartActivity(final Intent intent, Boolean wait) throws Exception {
    444     if (wait == null || wait == false) {
    445       startActivity(intent);
    446     } else {
    447       FutureActivityTask<Intent> task = new FutureActivityTask<Intent>() {
    448         private boolean mSecondResume = false;
    449 
    450         @Override
    451         public void onCreate() {
    452           super.onCreate();
    453           startActivity(intent);
    454         }
    455 
    456         @Override
    457         public void onResume() {
    458           if (mSecondResume) {
    459             finish();
    460           }
    461           mSecondResume = true;
    462         }
    463 
    464         @Override
    465         public void onDestroy() {
    466           setResult(null);
    467         }
    468 
    469       };
    470       mTaskQueue.execute(task);
    471 
    472       try {
    473         task.getResult();
    474       } catch (Exception e) {
    475         throw new RuntimeException(e);
    476       }
    477     }
    478   }
    479 
    480   /**
    481    * Creates a new AndroidFacade that simplifies the interface to various Android APIs.
    482    *
    483    * @param service
    484    *          is the {@link Context} the APIs will run under
    485    */
    486 
    487   @Rpc(description = "Put a text string in the clipboard.")
    488   public void setTextClip(@RpcParameter(name = "text")
    489                           String text,
    490                           @RpcParameter(name = "label")
    491                           @RpcOptional @RpcDefault(value = "copiedText")
    492                           String label) {
    493     getClipboardManager().setPrimaryClip(ClipData.newPlainText(label, text));
    494   }
    495 
    496   @Rpc(description = "Get the device serial number.")
    497   public String getBuildSerial() {
    498       return Build.SERIAL;
    499   }
    500 
    501   @Rpc(description = "Get the name of system bootloader version number.")
    502   public String getBuildBootloader() {
    503     return android.os.Build.BOOTLOADER;
    504   }
    505 
    506   @Rpc(description = "Get the name of the industrial design.")
    507   public String getBuildIndustrialDesignName() {
    508     return Build.DEVICE;
    509   }
    510 
    511   @Rpc(description = "Get the build ID string meant for displaying to the user")
    512   public String getBuildDisplay() {
    513     return Build.DISPLAY;
    514   }
    515 
    516   @Rpc(description = "Get the string that uniquely identifies this build.")
    517   public String getBuildFingerprint() {
    518     return Build.FINGERPRINT;
    519   }
    520 
    521   @Rpc(description = "Get the name of the hardware (from the kernel command "
    522       + "line or /proc)..")
    523   public String getBuildHardware() {
    524     return Build.HARDWARE;
    525   }
    526 
    527   @Rpc(description = "Get the device host.")
    528   public String getBuildHost() {
    529     return Build.HOST;
    530   }
    531 
    532   @Rpc(description = "Get Either a changelist number, or a label like."
    533       + " \"M4-rc20\".")
    534   public String getBuildID() {
    535     return android.os.Build.ID;
    536   }
    537 
    538   @Rpc(description = "Returns true if we are running a debug build such"
    539       + " as \"user-debug\" or \"eng\".")
    540   public boolean getBuildIsDebuggable() {
    541     return Build.IS_DEBUGGABLE;
    542   }
    543 
    544   @Rpc(description = "Get the name of the overall product.")
    545   public String getBuildProduct() {
    546     return android.os.Build.PRODUCT;
    547   }
    548 
    549   @Rpc(description = "Get an ordered list of 32 bit ABIs supported by this "
    550       + "device. The most preferred ABI is the first element in the list")
    551   public String[] getBuildSupported32BitAbis() {
    552     return Build.SUPPORTED_32_BIT_ABIS;
    553   }
    554 
    555   @Rpc(description = "Get an ordered list of 64 bit ABIs supported by this "
    556       + "device. The most preferred ABI is the first element in the list")
    557   public String[] getBuildSupported64BitAbis() {
    558     return Build.SUPPORTED_64_BIT_ABIS;
    559   }
    560 
    561   @Rpc(description = "Get an ordered list of ABIs supported by this "
    562       + "device. The most preferred ABI is the first element in the list")
    563   public String[] getBuildSupportedBitAbis() {
    564     return Build.SUPPORTED_ABIS;
    565   }
    566 
    567   @Rpc(description = "Get comma-separated tags describing the build,"
    568       + " like \"unsigned,debug\".")
    569   public String getBuildTags() {
    570     return Build.TAGS;
    571   }
    572 
    573   @Rpc(description = "Get The type of build, like \"user\" or \"eng\".")
    574   public String getBuildType() {
    575     return Build.TYPE;
    576   }
    577   @Rpc(description = "Returns the board name.")
    578   public String getBuildBoard() {
    579     return Build.BOARD;
    580   }
    581 
    582   @Rpc(description = "Returns the brand name.")
    583   public String getBuildBrand() {
    584     return Build.BRAND;
    585   }
    586 
    587   @Rpc(description = "Returns the manufacturer name.")
    588   public String getBuildManufacturer() {
    589     return Build.MANUFACTURER;
    590   }
    591 
    592   @Rpc(description = "Returns the model name.")
    593   public String getBuildModel() {
    594     return Build.MODEL;
    595   }
    596 
    597   @Rpc(description = "Returns the build number.")
    598   public String getBuildNumber() {
    599     return Build.FINGERPRINT;
    600   }
    601 
    602   @Rpc(description = "Returns the SDK version.")
    603   public Integer getBuildSdkVersion() {
    604     return Build.VERSION.SDK_INT;
    605   }
    606 
    607   @Rpc(description = "Returns the current device time.")
    608   public Long getBuildTime() {
    609     return Build.TIME;
    610   }
    611 
    612   @Rpc(description = "Read all text strings copied by setTextClip from the clipboard.")
    613   public List<String> getTextClip() {
    614     ClipboardManager cm = getClipboardManager();
    615     ArrayList<String> texts = new ArrayList<String>();
    616     if(!cm.hasPrimaryClip()) {
    617       return texts;
    618     }
    619     ClipData cd = cm.getPrimaryClip();
    620     for(int i=0; i<cd.getItemCount(); i++) {
    621       texts.add(cd.getItemAt(i).coerceToText(mService).toString());
    622     }
    623     return texts;
    624   }
    625 
    626   /**
    627    * packagename and classname, if provided, are used in a 'setComponent' call.
    628    */
    629   @Rpc(description = "Starts an activity.")
    630   public void startActivity(
    631       @RpcParameter(name = "action")
    632       String action,
    633       @RpcParameter(name = "uri")
    634       @RpcOptional String uri,
    635       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
    636       @RpcOptional String type,
    637       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
    638       @RpcOptional JSONObject extras,
    639       @RpcParameter(name = "wait", description = "block until the user exits the started activity")
    640       @RpcOptional Boolean wait,
    641       @RpcParameter(name = "packagename",
    642                     description = "name of package. If used, requires classname to be useful")
    643       @RpcOptional String packagename,
    644       @RpcParameter(name = "classname",
    645                     description = "name of class. If used, requires packagename to be useful")
    646       @RpcOptional String classname
    647       ) throws Exception {
    648     final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
    649     doStartActivity(intent, wait);
    650   }
    651 
    652   @Rpc(description = "Send a broadcast.")
    653   public void sendBroadcast(
    654       @RpcParameter(name = "action")
    655       String action,
    656       @RpcParameter(name = "uri")
    657       @RpcOptional String uri,
    658       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
    659       @RpcOptional String type,
    660       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
    661       @RpcOptional JSONObject extras,
    662       @RpcParameter(name = "packagename",
    663                     description = "name of package. If used, requires classname to be useful")
    664       @RpcOptional String packagename,
    665       @RpcParameter(name = "classname",
    666                     description = "name of class. If used, requires packagename to be useful")
    667       @RpcOptional String classname
    668       ) throws JSONException {
    669     final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
    670     try {
    671       mService.sendBroadcast(intent);
    672     } catch (Exception e) {
    673       Log.e("Failed to broadcast intent.", e);
    674     }
    675   }
    676 
    677   @Rpc(description = "Starts a service.")
    678   public void startService(
    679       @RpcParameter(name = "uri")
    680       @RpcOptional String uri,
    681       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
    682       @RpcOptional JSONObject extras,
    683       @RpcParameter(name = "packagename",
    684                     description = "name of package. If used, requires classname to be useful")
    685       @RpcOptional String packagename,
    686       @RpcParameter(name = "classname",
    687                     description = "name of class. If used, requires packagename to be useful")
    688       @RpcOptional String classname
    689       ) throws Exception {
    690     final Intent intent = buildIntent(null /* action */, uri, null /* type */, extras, packagename,
    691                                       classname, null /* categories */);
    692     mService.startService(intent);
    693   }
    694 
    695   @Rpc(description = "Create an Intent.", returns = "An object representing an Intent")
    696   public Intent makeIntent(
    697       @RpcParameter(name = "action")
    698       String action,
    699       @RpcParameter(name = "uri")
    700       @RpcOptional String uri,
    701       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
    702       @RpcOptional String type,
    703       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
    704       @RpcOptional JSONObject extras,
    705       @RpcParameter(name = "categories", description = "a List of categories to add to the Intent")
    706       @RpcOptional JSONArray categories,
    707       @RpcParameter(name = "packagename",
    708                     description = "name of package. If used, requires classname to be useful")
    709       @RpcOptional String packagename,
    710       @RpcParameter(name = "classname",
    711                     description = "name of class. If used, requires packagename to be useful")
    712       @RpcOptional String classname,
    713       @RpcParameter(name = "flags", description = "Intent flags")
    714       @RpcOptional Integer flags
    715       ) throws JSONException {
    716     Intent intent = buildIntent(action, uri, type, extras, packagename, classname, categories);
    717     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    718     if (flags != null) {
    719       intent.setFlags(flags);
    720     }
    721     return intent;
    722   }
    723 
    724   @Rpc(description = "Start Activity using Intent")
    725   public void startActivityIntent(
    726       @RpcParameter(name = "intent",
    727                     description = "Intent in the format as returned from makeIntent")
    728       Intent intent,
    729       @RpcParameter(name = "wait",
    730                     description = "block until the user exits the started activity")
    731       @RpcOptional Boolean wait
    732       ) throws Exception {
    733     doStartActivity(intent, wait);
    734   }
    735 
    736   @Rpc(description = "Send Broadcast Intent")
    737   public void sendBroadcastIntent(
    738       @RpcParameter(name = "intent",
    739                     description = "Intent in the format as returned from makeIntent")
    740       Intent intent
    741       ) throws Exception {
    742     mService.sendBroadcast(intent);
    743   }
    744 
    745   @Rpc(description = "Start Service using Intent")
    746   public void startServiceIntent(
    747       @RpcParameter(name = "intent",
    748                     description = "Intent in the format as returned from makeIntent")
    749       Intent intent
    750       ) throws Exception {
    751     mService.startService(intent);
    752   }
    753 
    754   @Rpc(description = "Send Broadcast Intent as system user.")
    755   public void sendBroadcastIntentAsUserAll(
    756       @RpcParameter(name = "intent",
    757                     description = "Intent in the format as returned from makeIntent")
    758       Intent intent
    759       ) throws Exception {
    760     mService.sendBroadcastAsUser(intent, UserHandle.ALL);
    761   }
    762 
    763   @Rpc(description = "Vibrates the phone or a specified duration in milliseconds.")
    764   public void vibrate(
    765       @RpcParameter(name = "duration", description = "duration in milliseconds")
    766       @RpcDefault("300")
    767       Integer duration) {
    768     mVibrator.vibrate(duration);
    769   }
    770 
    771   @Rpc(description = "Displays a short-duration Toast notification.")
    772   public void makeToast(@RpcParameter(name = "message") final String message) {
    773     mHandler.post(new Runnable() {
    774       public void run() {
    775         Toast.makeText(mService, message, Toast.LENGTH_SHORT).show();
    776       }
    777     });
    778   }
    779 
    780   private String getInputFromAlertDialog(final String title, final String message,
    781       final boolean password) {
    782     final FutureActivityTask<String> task = new FutureActivityTask<String>() {
    783       @Override
    784       public void onCreate() {
    785         super.onCreate();
    786         final EditText input = new EditText(getActivity());
    787         if (password) {
    788           input.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
    789           input.setTransformationMethod(new PasswordTransformationMethod());
    790         }
    791         AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
    792         alert.setTitle(title);
    793         alert.setMessage(message);
    794         alert.setView(input);
    795         alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
    796           @Override
    797           public void onClick(DialogInterface dialog, int whichButton) {
    798             dialog.dismiss();
    799             setResult(input.getText().toString());
    800             finish();
    801           }
    802         });
    803         alert.setOnCancelListener(new DialogInterface.OnCancelListener() {
    804           @Override
    805           public void onCancel(DialogInterface dialog) {
    806             dialog.dismiss();
    807             setResult(null);
    808             finish();
    809           }
    810         });
    811         alert.show();
    812       }
    813     };
    814     mTaskQueue.execute(task);
    815 
    816     try {
    817       return task.getResult();
    818     } catch (Exception e) {
    819       Log.e("Failed to display dialog.", e);
    820       throw new RuntimeException(e);
    821     }
    822   }
    823 
    824   @Rpc(description = "Queries the user for a text input.")
    825   @RpcDeprecated(value = "dialogGetInput", release = "r3")
    826   public String getInput(
    827       @RpcParameter(name = "title", description = "title of the input box")
    828       @RpcDefault("SL4A Input")
    829       final String title,
    830       @RpcParameter(name = "message", description = "message to display above the input box")
    831       @RpcDefault("Please enter value:")
    832       final String message) {
    833     return getInputFromAlertDialog(title, message, false);
    834   }
    835 
    836   @Rpc(description = "Queries the user for a password.")
    837   @RpcDeprecated(value = "dialogGetPassword", release = "r3")
    838   public String getPassword(
    839       @RpcParameter(name = "title", description = "title of the input box")
    840       @RpcDefault("SL4A Password Input")
    841       final String title,
    842       @RpcParameter(name = "message", description = "message to display above the input box")
    843       @RpcDefault("Please enter password:")
    844       final String message) {
    845     return getInputFromAlertDialog(title, message, true);
    846   }
    847 
    848   @Rpc(description = "Displays a notification that will be canceled when the user clicks on it.")
    849   public void notify(@RpcParameter(name = "title", description = "title") String title,
    850       @RpcParameter(name = "message") String message) {
    851     // This contentIntent is a noop.
    852     PendingIntent contentIntent = PendingIntent.getService(mService, 0, new Intent(), 0);
    853     Notification.Builder builder = new Notification.Builder(mService);
    854     builder.setSmallIcon(mResources.getLogo48())
    855            .setTicker(message)
    856            .setWhen(System.currentTimeMillis())
    857            .setContentTitle(title)
    858            .setContentText(message)
    859            .setContentIntent(contentIntent);
    860     Notification notification = builder.build();
    861     notification.flags = Notification.FLAG_AUTO_CANCEL;
    862     // Get a unique notification id from the application.
    863     final int notificationId = NotificationIdFactory.create();
    864     mNotificationManager.notify(notificationId, notification);
    865   }
    866 
    867   @Rpc(description = "Returns the intent that launched the script.")
    868   public Object getIntent() {
    869     return mIntent;
    870   }
    871 
    872   @Rpc(description = "Launches an activity that sends an e-mail message to a given recipient.")
    873   public void sendEmail(
    874       @RpcParameter(name = "to", description = "A comma separated list of recipients.")
    875       final String to,
    876       @RpcParameter(name = "subject") final String subject,
    877       @RpcParameter(name = "body") final String body,
    878       @RpcParameter(name = "attachmentUri")
    879       @RpcOptional final String attachmentUri) {
    880     final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
    881     intent.setType("plain/text");
    882     intent.putExtra(android.content.Intent.EXTRA_EMAIL, to.split(","));
    883     intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
    884     intent.putExtra(android.content.Intent.EXTRA_TEXT, body);
    885     if (attachmentUri != null) {
    886       intent.putExtra(android.content.Intent.EXTRA_STREAM, Uri.parse(attachmentUri));
    887     }
    888     startActivity(intent);
    889   }
    890 
    891   @Rpc(description = "Returns package version code.")
    892   public int getPackageVersionCode(@RpcParameter(name = "packageName") final String packageName) {
    893     int result = -1;
    894     PackageInfo pInfo = null;
    895     try {
    896       pInfo =
    897           mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
    898     } catch (NameNotFoundException e) {
    899       pInfo = null;
    900     }
    901     if (pInfo != null) {
    902       result = pInfo.versionCode;
    903     }
    904     return result;
    905   }
    906 
    907   @Rpc(description = "Returns package version name.")
    908   public String getPackageVersion(@RpcParameter(name = "packageName") final String packageName) {
    909     PackageInfo packageInfo = null;
    910     try {
    911       packageInfo =
    912           mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
    913     } catch (NameNotFoundException e) {
    914       return null;
    915     }
    916     if (packageInfo != null) {
    917       return packageInfo.versionName;
    918     }
    919     return null;
    920   }
    921 
    922   @Rpc(description = "Checks if SL4A's version is >= the specified version.")
    923   public boolean requiredVersion(
    924           @RpcParameter(name = "requiredVersion") final Integer version) {
    925     boolean result = false;
    926     int packageVersion = getPackageVersionCode(
    927             "com.googlecode.android_scripting");
    928     if (version > -1) {
    929       result = (packageVersion >= version);
    930     }
    931     return result;
    932   }
    933 
    934   @Rpc(description = "Writes message to logcat at verbose level")
    935   public void logV(
    936           @RpcParameter(name = "message")
    937           String message) {
    938       android.util.Log.v("SL4A: ", message);
    939   }
    940 
    941   @Rpc(description = "Writes message to logcat at info level")
    942   public void logI(
    943           @RpcParameter(name = "message")
    944           String message) {
    945       android.util.Log.i("SL4A: ", message);
    946   }
    947 
    948   @Rpc(description = "Writes message to logcat at debug level")
    949   public void logD(
    950           @RpcParameter(name = "message")
    951           String message) {
    952       android.util.Log.d("SL4A: ", message);
    953   }
    954 
    955   @Rpc(description = "Writes message to logcat at warning level")
    956   public void logW(
    957           @RpcParameter(name = "message")
    958           String message) {
    959       android.util.Log.w("SL4A: ", message);
    960   }
    961 
    962   @Rpc(description = "Writes message to logcat at error level")
    963   public void logE(
    964           @RpcParameter(name = "message")
    965           String message) {
    966       android.util.Log.e("SL4A: ", message);
    967   }
    968 
    969   @Rpc(description = "Writes message to logcat at wtf level")
    970   public void logWTF(
    971           @RpcParameter(name = "message")
    972           String message) {
    973       android.util.Log.wtf("SL4A: ", message);
    974   }
    975 
    976   /**
    977    *
    978    * Map returned:
    979    *
    980    * <pre>
    981    *   TZ = Timezone
    982    *     id = Timezone ID
    983    *     display = Timezone display name
    984    *     offset = Offset from UTC (in ms)
    985    *   SDK = SDK Version
    986    *   download = default download path
    987    *   appcache = Location of application cache
    988    *   sdcard = Space on sdcard
    989    *     availblocks = Available blocks
    990    *     blockcount = Total Blocks
    991    *     blocksize = size of block.
    992    * </pre>
    993    */
    994   @Rpc(description = "A map of various useful environment details")
    995   public Map<String, Object> environment() {
    996     Map<String, Object> result = new HashMap<String, Object>();
    997     Map<String, Object> zone = new HashMap<String, Object>();
    998     Map<String, Object> space = new HashMap<String, Object>();
    999     TimeZone tz = TimeZone.getDefault();
   1000     zone.put("id", tz.getID());
   1001     zone.put("display", tz.getDisplayName());
   1002     zone.put("offset", tz.getOffset((new Date()).getTime()));
   1003     result.put("TZ", zone);
   1004     result.put("SDK", android.os.Build.VERSION.SDK_INT);
   1005     result.put("download", FileUtils.getExternalDownload().getAbsolutePath());
   1006     result.put("appcache", mService.getCacheDir().getAbsolutePath());
   1007     try {
   1008       StatFs fs = new StatFs("/sdcard");
   1009       space.put("availblocks", fs.getAvailableBlocksLong());
   1010       space.put("blocksize", fs.getBlockSizeLong());
   1011       space.put("blockcount", fs.getBlockCountLong());
   1012     } catch (Exception e) {
   1013       space.put("exception", e.toString());
   1014     }
   1015     result.put("sdcard", space);
   1016     return result;
   1017   }
   1018 
   1019   @Rpc(description = "Get list of constants (static final fields) for a class")
   1020   public Bundle getConstants(
   1021       @RpcParameter(name = "classname", description = "Class to get constants from")
   1022       String classname)
   1023       throws Exception {
   1024     Bundle result = new Bundle();
   1025     int flags = Modifier.FINAL | Modifier.PUBLIC | Modifier.STATIC;
   1026     Class<?> clazz = Class.forName(classname);
   1027     for (Field field : clazz.getFields()) {
   1028       if ((field.getModifiers() & flags) == flags) {
   1029         Class<?> type = field.getType();
   1030         String name = field.getName();
   1031         if (type == int.class) {
   1032           result.putInt(name, field.getInt(null));
   1033         } else if (type == long.class) {
   1034           result.putLong(name, field.getLong(null));
   1035         } else if (type == double.class) {
   1036           result.putDouble(name, field.getDouble(null));
   1037         } else if (type == char.class) {
   1038           result.putChar(name, field.getChar(null));
   1039         } else if (type instanceof Object) {
   1040           result.putString(name, field.get(null).toString());
   1041         }
   1042       }
   1043     }
   1044     return result;
   1045   }
   1046 
   1047 }
   1048