• 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 whether the device is running SDK at least T")
isSdkAtLeastT()616   public boolean isSdkAtLeastT() {
617     return SdkLevel.isAtLeastT();
618   }
619 
620   @Rpc(description = "Returns the current device time.")
getBuildTime()621   public Long getBuildTime() {
622     return Build.TIME;
623   }
624 
625   @Rpc(description = "Read all text strings copied by setTextClip from the clipboard.")
getTextClip()626   public List<String> getTextClip() {
627     ClipboardManager cm = getClipboardManager();
628     ArrayList<String> texts = new ArrayList<String>();
629     if(!cm.hasPrimaryClip()) {
630       return texts;
631     }
632     ClipData cd = cm.getPrimaryClip();
633     for(int i=0; i<cd.getItemCount(); i++) {
634       texts.add(cd.getItemAt(i).coerceToText(mService).toString());
635     }
636     return texts;
637   }
638 
639   /**
640    * packagename and classname, if provided, are used in a 'setComponent' call.
641    */
642   @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 )643   public void startActivity(
644       @RpcParameter(name = "action")
645       String action,
646       @RpcParameter(name = "uri")
647       @RpcOptional String uri,
648       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
649       @RpcOptional String type,
650       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
651       @RpcOptional JSONObject extras,
652       @RpcParameter(name = "wait", description = "block until the user exits the started activity")
653       @RpcOptional Boolean wait,
654       @RpcParameter(name = "packagename",
655                     description = "name of package. If used, requires classname to be useful")
656       @RpcOptional String packagename,
657       @RpcParameter(name = "classname",
658                     description = "name of class. If used, requires packagename to be useful")
659       @RpcOptional String classname
660       ) throws Exception {
661     final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
662     doStartActivity(intent, wait);
663   }
664 
665   @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 )666   public void sendBroadcast(
667       @RpcParameter(name = "action")
668       String action,
669       @RpcParameter(name = "uri")
670       @RpcOptional String uri,
671       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
672       @RpcOptional String type,
673       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
674       @RpcOptional JSONObject extras,
675       @RpcParameter(name = "packagename",
676                     description = "name of package. If used, requires classname to be useful")
677       @RpcOptional String packagename,
678       @RpcParameter(name = "classname",
679                     description = "name of class. If used, requires packagename to be useful")
680       @RpcOptional String classname
681       ) throws JSONException {
682     final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
683     try {
684       mService.sendBroadcast(intent);
685     } catch (Exception e) {
686       Log.e("Failed to broadcast intent.", e);
687     }
688   }
689 
690   @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 )691   public void startService(
692       @RpcParameter(name = "uri")
693       @RpcOptional String uri,
694       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
695       @RpcOptional JSONObject extras,
696       @RpcParameter(name = "packagename",
697                     description = "name of package. If used, requires classname to be useful")
698       @RpcOptional String packagename,
699       @RpcParameter(name = "classname",
700                     description = "name of class. If used, requires packagename to be useful")
701       @RpcOptional String classname
702       ) throws Exception {
703     final Intent intent = buildIntent(null /* action */, uri, null /* type */, extras, packagename,
704                                       classname, null /* categories */);
705     mService.startService(intent);
706   }
707 
708   @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 )709   public Intent makeIntent(
710       @RpcParameter(name = "action")
711       String action,
712       @RpcParameter(name = "uri")
713       @RpcOptional String uri,
714       @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
715       @RpcOptional String type,
716       @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
717       @RpcOptional JSONObject extras,
718       @RpcParameter(name = "categories", description = "a List of categories to add to the Intent")
719       @RpcOptional JSONArray categories,
720       @RpcParameter(name = "packagename",
721                     description = "name of package. If used, requires classname to be useful")
722       @RpcOptional String packagename,
723       @RpcParameter(name = "classname",
724                     description = "name of class. If used, requires packagename to be useful")
725       @RpcOptional String classname,
726       @RpcParameter(name = "flags", description = "Intent flags")
727       @RpcOptional Integer flags
728       ) throws JSONException {
729     Intent intent = buildIntent(action, uri, type, extras, packagename, classname, categories);
730     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
731     if (flags != null) {
732       intent.setFlags(flags);
733     }
734     return intent;
735   }
736 
737   @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 )738   public void startActivityIntent(
739       @RpcParameter(name = "intent",
740                     description = "Intent in the format as returned from makeIntent")
741       Intent intent,
742       @RpcParameter(name = "wait",
743                     description = "block until the user exits the started activity")
744       @RpcOptional Boolean wait
745       ) throws Exception {
746     doStartActivity(intent, wait);
747   }
748 
749   @Rpc(description = "Send Broadcast Intent")
sendBroadcastIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent intent )750   public void sendBroadcastIntent(
751       @RpcParameter(name = "intent",
752                     description = "Intent in the format as returned from makeIntent")
753       Intent intent
754       ) throws Exception {
755     mService.sendBroadcast(intent);
756   }
757 
758   @Rpc(description = "Start Service using Intent")
startServiceIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent intent )759   public void startServiceIntent(
760       @RpcParameter(name = "intent",
761                     description = "Intent in the format as returned from makeIntent")
762       Intent intent
763       ) throws Exception {
764     mService.startService(intent);
765   }
766 
767   @Rpc(description = "Send Broadcast Intent as system user.")
sendBroadcastIntentAsUserAll( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent intent )768   public void sendBroadcastIntentAsUserAll(
769       @RpcParameter(name = "intent",
770                     description = "Intent in the format as returned from makeIntent")
771       Intent intent
772       ) throws Exception {
773     mService.sendBroadcastAsUser(intent, UserHandle.ALL);
774   }
775 
776   @Rpc(description = "Vibrates the phone or a specified duration in milliseconds.")
vibrate( @pcParametername = "duration", description = "duration in milliseconds") @pcDefault"300") Integer duration)777   public void vibrate(
778       @RpcParameter(name = "duration", description = "duration in milliseconds")
779       @RpcDefault("300")
780       Integer duration) {
781     mVibrator.vibrate(duration);
782   }
783 
784   @Rpc(description = "Displays a short-duration Toast notification.")
makeToast(@pcParametername = "message") final String message)785   public void makeToast(@RpcParameter(name = "message") final String message) {
786     mHandler.post(new Runnable() {
787       public void run() {
788         Toast.makeText(mService, message, Toast.LENGTH_SHORT).show();
789       }
790     });
791   }
792 
getInputFromAlertDialog(final String title, final String message, final boolean password)793   private String getInputFromAlertDialog(final String title, final String message,
794       final boolean password) {
795     final FutureActivityTask<String> task = new FutureActivityTask<String>() {
796       @Override
797       public void onCreate() {
798         super.onCreate();
799         final EditText input = new EditText(getActivity());
800         if (password) {
801           input.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
802           input.setTransformationMethod(new PasswordTransformationMethod());
803         }
804         AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
805         alert.setTitle(title);
806         alert.setMessage(message);
807         alert.setView(input);
808         alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
809           @Override
810           public void onClick(DialogInterface dialog, int whichButton) {
811             dialog.dismiss();
812             setResult(input.getText().toString());
813             finish();
814           }
815         });
816         alert.setOnCancelListener(new DialogInterface.OnCancelListener() {
817           @Override
818           public void onCancel(DialogInterface dialog) {
819             dialog.dismiss();
820             setResult(null);
821             finish();
822           }
823         });
824         alert.show();
825       }
826     };
827     mTaskQueue.execute(task);
828 
829     try {
830       return task.getResult();
831     } catch (Exception e) {
832       Log.e("Failed to display dialog.", e);
833       throw new RuntimeException(e);
834     }
835   }
836 
837   @Rpc(description = "Queries the user for a text input.")
838   @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)839   public String getInput(
840       @RpcParameter(name = "title", description = "title of the input box")
841       @RpcDefault("SL4A Input")
842       final String title,
843       @RpcParameter(name = "message", description = "message to display above the input box")
844       @RpcDefault("Please enter value:")
845       final String message) {
846     return getInputFromAlertDialog(title, message, false);
847   }
848 
849   @Rpc(description = "Queries the user for a password.")
850   @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)851   public String getPassword(
852       @RpcParameter(name = "title", description = "title of the input box")
853       @RpcDefault("SL4A Password Input")
854       final String title,
855       @RpcParameter(name = "message", description = "message to display above the input box")
856       @RpcDefault("Please enter password:")
857       final String message) {
858     return getInputFromAlertDialog(title, message, true);
859   }
860 
createNotificationChannel()861   private void createNotificationChannel() {
862     CharSequence name = mService.getString(mResources.getStringId("notification_channel_name"));
863     String description = mService.getString(mResources.getStringId("notification_channel_description"));
864     int importance = NotificationManager.IMPORTANCE_DEFAULT;
865     NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
866     channel.setDescription(description);
867     channel.enableLights(false);
868     channel.enableVibration(false);
869     mNotificationManager.createNotificationChannel(channel);
870   }
871 
872   @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)873   public void notify(@RpcParameter(name = "title", description = "title") String title,
874       @RpcParameter(name = "message") String message) {
875     createNotificationChannel();
876     // This contentIntent is a noop.
877     PendingIntent contentIntent = PendingIntent.getService(mService, 0, new Intent(),
878             PendingIntent.FLAG_IMMUTABLE);
879     Notification.Builder builder = new Notification.Builder(mService, CHANNEL_ID);
880     builder.setSmallIcon(mResources.getLogo48())
881            .setTicker(message)
882            .setWhen(System.currentTimeMillis())
883            .setContentTitle(title)
884            .setContentText(message)
885            .setContentIntent(contentIntent);
886     Notification notification = builder.build();
887     notification.flags = Notification.FLAG_AUTO_CANCEL;
888     // Get a unique notification id from the application.
889     final int notificationId = NotificationIdFactory.create();
890     mNotificationManager.notify(notificationId, notification);
891   }
892 
893   @Rpc(description = "Returns the intent that launched the script.")
getIntent()894   public Object getIntent() {
895     return mIntent;
896   }
897 
898   @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)899   public void sendEmail(
900       @RpcParameter(name = "to", description = "A comma separated list of recipients.")
901       final String to,
902       @RpcParameter(name = "subject") final String subject,
903       @RpcParameter(name = "body") final String body,
904       @RpcParameter(name = "attachmentUri")
905       @RpcOptional final String attachmentUri) {
906     final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
907     intent.setType("plain/text");
908     intent.putExtra(android.content.Intent.EXTRA_EMAIL, to.split(","));
909     intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
910     intent.putExtra(android.content.Intent.EXTRA_TEXT, body);
911     if (attachmentUri != null) {
912       intent.putExtra(android.content.Intent.EXTRA_STREAM, Uri.parse(attachmentUri));
913     }
914     startActivity(intent);
915   }
916 
917   @Rpc(description = "Returns package version code.")
getPackageVersionCode(@pcParametername = "packageName") final String packageName)918   public int getPackageVersionCode(@RpcParameter(name = "packageName") final String packageName) {
919     int result = -1;
920     PackageInfo pInfo = null;
921     try {
922       pInfo =
923           mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
924     } catch (NameNotFoundException e) {
925       pInfo = null;
926     }
927     if (pInfo != null) {
928       result = pInfo.versionCode;
929     }
930     return result;
931   }
932 
933   @Rpc(description = "Returns package version name.")
getPackageVersion(@pcParametername = "packageName") final String packageName)934   public String getPackageVersion(@RpcParameter(name = "packageName") final String packageName) {
935     PackageInfo packageInfo = null;
936     try {
937       packageInfo =
938           mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
939     } catch (NameNotFoundException e) {
940       return null;
941     }
942     if (packageInfo != null) {
943       return packageInfo.versionName;
944     }
945     return null;
946   }
947 
948   @Rpc(description = "Checks if SL4A's version is >= the specified version.")
requiredVersion( @pcParametername = "requiredVersion") final Integer version)949   public boolean requiredVersion(
950           @RpcParameter(name = "requiredVersion") final Integer version) {
951     boolean result = false;
952     int packageVersion = getPackageVersionCode(
953             "com.googlecode.android_scripting");
954     if (version > -1) {
955       result = (packageVersion >= version);
956     }
957     return result;
958   }
959 
960   @Rpc(description = "Writes message to logcat at verbose level")
logV( @pcParametername = "message") String message)961   public void logV(
962           @RpcParameter(name = "message")
963           String message) {
964       android.util.Log.v("SL4A: ", message);
965   }
966 
967   @Rpc(description = "Writes message to logcat at info level")
logI( @pcParametername = "message") String message)968   public void logI(
969           @RpcParameter(name = "message")
970           String message) {
971       android.util.Log.i("SL4A: ", message);
972   }
973 
974   @Rpc(description = "Writes message to logcat at debug level")
logD( @pcParametername = "message") String message)975   public void logD(
976           @RpcParameter(name = "message")
977           String message) {
978       android.util.Log.d("SL4A: ", message);
979   }
980 
981   @Rpc(description = "Writes message to logcat at warning level")
logW( @pcParametername = "message") String message)982   public void logW(
983           @RpcParameter(name = "message")
984           String message) {
985       android.util.Log.w("SL4A: ", message);
986   }
987 
988   @Rpc(description = "Writes message to logcat at error level")
logE( @pcParametername = "message") String message)989   public void logE(
990           @RpcParameter(name = "message")
991           String message) {
992       android.util.Log.e("SL4A: ", message);
993   }
994 
995   @Rpc(description = "Writes message to logcat at wtf level")
logWTF( @pcParametername = "message") String message)996   public void logWTF(
997           @RpcParameter(name = "message")
998           String message) {
999       android.util.Log.wtf("SL4A: ", message);
1000   }
1001 
1002   /**
1003    *
1004    * Map returned:
1005    *
1006    * <pre>
1007    *   TZ = Timezone
1008    *     id = Timezone ID
1009    *     display = Timezone display name
1010    *     offset = Offset from UTC (in ms)
1011    *   SDK = SDK Version
1012    *   download = default download path
1013    *   appcache = Location of application cache
1014    *   sdcard = Space on sdcard
1015    *     availblocks = Available blocks
1016    *     blockcount = Total Blocks
1017    *     blocksize = size of block.
1018    * </pre>
1019    */
1020   @Rpc(description = "A map of various useful environment details")
environment()1021   public Map<String, Object> environment() {
1022     Map<String, Object> result = new HashMap<String, Object>();
1023     Map<String, Object> zone = new HashMap<String, Object>();
1024     Map<String, Object> space = new HashMap<String, Object>();
1025     TimeZone tz = TimeZone.getDefault();
1026     zone.put("id", tz.getID());
1027     zone.put("display", tz.getDisplayName());
1028     zone.put("offset", tz.getOffset((new Date()).getTime()));
1029     result.put("TZ", zone);
1030     result.put("SDK", android.os.Build.VERSION.SDK_INT);
1031     result.put("download", FileUtils.getExternalDownload().getAbsolutePath());
1032     result.put("appcache", mService.getCacheDir().getAbsolutePath());
1033     try {
1034       StatFs fs = new StatFs("/sdcard");
1035       space.put("availblocks", fs.getAvailableBlocksLong());
1036       space.put("blocksize", fs.getBlockSizeLong());
1037       space.put("blockcount", fs.getBlockCountLong());
1038     } catch (Exception e) {
1039       space.put("exception", e.toString());
1040     }
1041     result.put("sdcard", space);
1042     return result;
1043   }
1044 
1045   @Rpc(description = "Get list of constants (static final fields) for a class")
getConstants( @pcParametername = "classname", description = "Class to get constants from") String classname)1046   public Bundle getConstants(
1047       @RpcParameter(name = "classname", description = "Class to get constants from")
1048       String classname)
1049       throws Exception {
1050     Bundle result = new Bundle();
1051     int flags = Modifier.FINAL | Modifier.PUBLIC | Modifier.STATIC;
1052     Class<?> clazz = Class.forName(classname);
1053     for (Field field : clazz.getFields()) {
1054       if ((field.getModifiers() & flags) == flags) {
1055         Class<?> type = field.getType();
1056         String name = field.getName();
1057         if (type == int.class) {
1058           result.putInt(name, field.getInt(null));
1059         } else if (type == long.class) {
1060           result.putLong(name, field.getLong(null));
1061         } else if (type == double.class) {
1062           result.putDouble(name, field.getDouble(null));
1063         } else if (type == char.class) {
1064           result.putChar(name, field.getChar(null));
1065         } else if (type instanceof Object) {
1066           result.putString(name, field.get(null).toString());
1067         }
1068       }
1069     }
1070     return result;
1071   }
1072 
1073 }
1074