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