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