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