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 {
getLogo48()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
shutdown()113   public void shutdown() {
114   }
115 
AndroidFacade(FacadeManager manager)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 
getClipboardManager()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 
startActivityForResult(final Intent intent)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 
startActivityForResultCodeWithTimeout(final Intent intent, final int request, final int timeout)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.
putExtrasFromJsonObject(JSONObject extras, Intent intent)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
putNestedJSONObject(JSONObject jsonObject, Bundle bundle)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 
startActivity(final Intent intent)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 
buildIntent(String action, String uri, String type, JSONObject extras, String packagename, String classname, JSONArray categories)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.")
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 )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.")
startActivityForResultIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent 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 
doStartActivity(final Intent intent, Boolean wait)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.")
setTextClip(@pcParametername = "text") String text, @RpcParameter(name = "label") @RpcOptional @RpcDefault(value = "copiedText") String label)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.")
getBuildSerial()497   public String getBuildSerial() {
498       return Build.SERIAL;
499   }
500 
501   @Rpc(description = "Get the name of system bootloader version number.")
getBuildBootloader()502   public String getBuildBootloader() {
503     return android.os.Build.BOOTLOADER;
504   }
505 
506   @Rpc(description = "Get the name of the industrial design.")
getBuildIndustrialDesignName()507   public String getBuildIndustrialDesignName() {
508     return Build.DEVICE;
509   }
510 
511   @Rpc(description = "Get the build ID string meant for displaying to the user")
getBuildDisplay()512   public String getBuildDisplay() {
513     return Build.DISPLAY;
514   }
515 
516   @Rpc(description = "Get the string that uniquely identifies this build.")
getBuildFingerprint()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)..")
getBuildHardware()523   public String getBuildHardware() {
524     return Build.HARDWARE;
525   }
526 
527   @Rpc(description = "Get the device host.")
getBuildHost()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\".")
getBuildID()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\".")
getBuildIsDebuggable()540   public boolean getBuildIsDebuggable() {
541     return Build.IS_DEBUGGABLE;
542   }
543 
544   @Rpc(description = "Get the name of the overall product.")
getBuildProduct()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")
getBuildSupported32BitAbis()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")
getBuildSupported64BitAbis()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")
getBuildSupportedBitAbis()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\".")
getBuildTags()569   public String getBuildTags() {
570     return Build.TAGS;
571   }
572 
573   @Rpc(description = "Get The type of build, like \"user\" or \"eng\".")
getBuildType()574   public String getBuildType() {
575     return Build.TYPE;
576   }
577   @Rpc(description = "Returns the board name.")
getBuildBoard()578   public String getBuildBoard() {
579     return Build.BOARD;
580   }
581 
582   @Rpc(description = "Returns the brand name.")
getBuildBrand()583   public String getBuildBrand() {
584     return Build.BRAND;
585   }
586 
587   @Rpc(description = "Returns the manufacturer name.")
getBuildManufacturer()588   public String getBuildManufacturer() {
589     return Build.MANUFACTURER;
590   }
591 
592   @Rpc(description = "Returns the model name.")
getBuildModel()593   public String getBuildModel() {
594     return Build.MODEL;
595   }
596 
597   @Rpc(description = "Returns the build number.")
getBuildNumber()598   public String getBuildNumber() {
599     return Build.FINGERPRINT;
600   }
601 
602   @Rpc(description = "Returns the SDK version.")
getBuildSdkVersion()603   public Integer getBuildSdkVersion() {
604     return Build.VERSION.SDK_INT;
605   }
606 
607   @Rpc(description = "Returns the current device time.")
getBuildTime()608   public Long getBuildTime() {
609     return Build.TIME;
610   }
611 
612   @Rpc(description = "Read all text strings copied by setTextClip from the clipboard.")
getTextClip()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.")
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 )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.")
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 )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.")
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 )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")
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 )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")
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 )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")
sendBroadcastIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent 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")
startServiceIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent 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.")
sendBroadcastIntentAsUserAll( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent intent )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.")
vibrate( @pcParametername = "duration", description = "duration in milliseconds") @pcDefault"300") Integer duration)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.")
makeToast(@pcParametername = "message") final String message)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 
getInputFromAlertDialog(final String title, final String message, final boolean password)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")
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)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")
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)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.")
notify(@pcParametername = "title", description = "title") String title, @RpcParameter(name = "message") String message)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.")
getIntent()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.")
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)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.")
getPackageVersionCode(@pcParametername = "packageName") final String packageName)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.")
getPackageVersion(@pcParametername = "packageName") final String packageName)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.")
requiredVersion( @pcParametername = "requiredVersion") final Integer 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")
logV( @pcParametername = "message") String message)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")
logI( @pcParametername = "message") String message)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")
logD( @pcParametername = "message") String message)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")
logW( @pcParametername = "message") String message)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")
logE( @pcParametername = "message") String message)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")
logWTF( @pcParametername = "message") String message)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")
environment()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")
getConstants( @pcParametername = "classname", description = "Class to get constants from") String classname)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