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