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