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