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