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