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