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