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 { getLogo48()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 shutdown()113 public void shutdown() { 114 } 115 AndroidFacade(FacadeManager manager)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 getClipboardManager()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 startActivityForResult(final Intent intent)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 startActivityForResultCodeWithTimeout(final Intent intent, final int request, final int timeout)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. putExtrasFromJsonObject(JSONObject extras, Intent intent)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 putNestedJSONObject(JSONObject jsonObject, Bundle bundle)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 startActivity(final Intent intent)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 buildIntent(String action, String uri, String type, JSONObject extras, String packagename, String classname, JSONArray categories)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.") 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 )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.") startActivityForResultIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent 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 doStartActivity(final Intent intent, Boolean wait)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.") setTextClip(@pcParametername = "text") String text, @RpcParameter(name = "label") @RpcOptional @RpcDefault(value = "copiedText") String label)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.") getBuildSerial()497 public String getBuildSerial() { 498 return Build.SERIAL; 499 } 500 501 @Rpc(description = "Get the name of system bootloader version number.") getBuildBootloader()502 public String getBuildBootloader() { 503 return android.os.Build.BOOTLOADER; 504 } 505 506 @Rpc(description = "Get the name of the industrial design.") getBuildIndustrialDesignName()507 public String getBuildIndustrialDesignName() { 508 return Build.DEVICE; 509 } 510 511 @Rpc(description = "Get the build ID string meant for displaying to the user") getBuildDisplay()512 public String getBuildDisplay() { 513 return Build.DISPLAY; 514 } 515 516 @Rpc(description = "Get the string that uniquely identifies this build.") getBuildFingerprint()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)..") getBuildHardware()523 public String getBuildHardware() { 524 return Build.HARDWARE; 525 } 526 527 @Rpc(description = "Get the device host.") getBuildHost()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\".") getBuildID()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\".") getBuildIsDebuggable()540 public boolean getBuildIsDebuggable() { 541 return Build.IS_DEBUGGABLE; 542 } 543 544 @Rpc(description = "Get the name of the overall product.") getBuildProduct()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") getBuildSupported32BitAbis()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") getBuildSupported64BitAbis()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") getBuildSupportedBitAbis()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\".") getBuildTags()569 public String getBuildTags() { 570 return Build.TAGS; 571 } 572 573 @Rpc(description = "Get The type of build, like \"user\" or \"eng\".") getBuildType()574 public String getBuildType() { 575 return Build.TYPE; 576 } 577 @Rpc(description = "Returns the board name.") getBuildBoard()578 public String getBuildBoard() { 579 return Build.BOARD; 580 } 581 582 @Rpc(description = "Returns the brand name.") getBuildBrand()583 public String getBuildBrand() { 584 return Build.BRAND; 585 } 586 587 @Rpc(description = "Returns the manufacturer name.") getBuildManufacturer()588 public String getBuildManufacturer() { 589 return Build.MANUFACTURER; 590 } 591 592 @Rpc(description = "Returns the model name.") getBuildModel()593 public String getBuildModel() { 594 return Build.MODEL; 595 } 596 597 @Rpc(description = "Returns the build number.") getBuildNumber()598 public String getBuildNumber() { 599 return Build.FINGERPRINT; 600 } 601 602 @Rpc(description = "Returns the SDK version.") getBuildSdkVersion()603 public Integer getBuildSdkVersion() { 604 return Build.VERSION.SDK_INT; 605 } 606 607 @Rpc(description = "Returns the current device time.") getBuildTime()608 public Long getBuildTime() { 609 return Build.TIME; 610 } 611 612 @Rpc(description = "Read all text strings copied by setTextClip from the clipboard.") getTextClip()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.") 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 )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.") 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 )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.") 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 )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") 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 )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") 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 )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") sendBroadcastIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent 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") startServiceIntent( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent 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.") sendBroadcastIntentAsUserAll( @pcParametername = "intent", description = "Intent in the format as returned from makeIntent") Intent intent )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.") vibrate( @pcParametername = "duration", description = "duration in milliseconds") @pcDefault"300") Integer duration)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.") makeToast(@pcParametername = "message") final String message)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 getInputFromAlertDialog(final String title, final String message, final boolean password)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") 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)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") 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)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.") notify(@pcParametername = "title", description = "title") String title, @RpcParameter(name = "message") String message)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.") getIntent()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.") 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)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.") getPackageVersionCode(@pcParametername = "packageName") final String packageName)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.") getPackageVersion(@pcParametername = "packageName") final String packageName)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.") requiredVersion( @pcParametername = "requiredVersion") final Integer 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") logV( @pcParametername = "message") String message)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") logI( @pcParametername = "message") String message)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") logD( @pcParametername = "message") String message)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") logW( @pcParametername = "message") String message)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") logE( @pcParametername = "message") String message)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") logWTF( @pcParametername = "message") String message)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") environment()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") getConstants( @pcParametername = "classname", description = "Class to get constants from") String classname)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