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