+ * Used in EditActivity, before setResult(). + * + * @param intentToHost the intent being returned to the host + * @param variableNames array of relevant variable names + */ + public static void addRelevantVariableList(Intent intentToHost, String[] variableNames) { + intentToHost.putExtra(BUNDLE_KEY_RELEVANT_VARIABLES, variableNames); + } + + /** + * Validate a variable name. + *
+ * The basic requirement for variables from a plugin is that they must be all lower-case. + * + * @param varName name to check + */ + public static boolean variableNameValid(String varName) { + + boolean validFlag = false; + + if (varName == null) + Log.d(TAG, "variableNameValid: null name"); + else { + if (VARIABLE_NAME_MATCH_PATTERN == null) + VARIABLE_NAME_MATCH_PATTERN = Pattern.compile(VARIABLE_NAME_MATCH_EXPRESSION, 0); + + if (VARIABLE_NAME_MATCH_PATTERN.matcher(varName).matches()) { + + if (variableNameIsLocal(varName)) + validFlag = true; + else + Log.d(TAG, "variableNameValid: name not local: " + varName); + } else + Log.d(TAG, "variableNameValid: invalid name: " + varName); + } + + return validFlag; + } + + /** + * Allows the plugin/host to indicate to each other a set of variables which they are referencing. + * The host may use this to e.g. show a variable selection list in it's UI. + * The host should use this if it previously indicated to the plugin that it supports relevant vars + * + * @param fromHostIntentExtras usually from getIntent().getExtras() + * @return variableNames an array of relevant variable names + */ + public static String[] getRelevantVariableList(Bundle fromHostIntentExtras) { + + String[] relevantVars = (String[]) getBundleValueSafe(fromHostIntentExtras, BUNDLE_KEY_RELEVANT_VARIABLES, String[].class, "getRelevantVariableList"); + + if (relevantVars == null) + relevantVars = new String[0]; + + return relevantVars; + } + + // ----------------------------- SETTING PLUGIN ONLY --------------------------------- // + + /** + * Used by: plugin QueryReceiver, FireReceiver + *
+ * Add a bundle of variable name/value pairs. + *
+ * Names must be valid Tasker local variable names.
+ * Values must be String, String [] or ArrayList
+ * Specify the encoding for a set of bundle keys.
+ *
+ * This is completely optional and currently only necessary if using Setting#setVariableReplaceKeys
+ * where the corresponding values of some of the keys specified are JSON encoded.
+ *
+ * @param resultBundleToHost the bundle being returned to the host
+ * @param keys the keys being returned to the host which are encoded in some way
+ * @param encoding the encoding of the values corresponding to the specified keys
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ * @see #hostSupportsKeyEncoding(Bundle, Encoding)
+ */
+ public static void setKeyEncoding(Bundle resultBundleToHost, String[] keys, Encoding encoding) {
+ if (Encoding.JSON.equals(encoding))
+ addStringArrayToBundleAsString(
+ keys, resultBundleToHost, BUNDLE_KEY_ENCODING_JSON_KEYS, "setValueEncoding"
+ );
+ else
+ Log.e(TAG, "unknown encoding: " + encoding);
+ }
+
+ // ----------------------------- EVENT PLUGIN ONLY --------------------------------- //
+
+ private static Object getBundleValueSafe(Bundle b, String key, Class> expectedClass, String funcName) {
+ Object value = null;
+
+ if (b != null) {
+ if (b.containsKey(key)) {
+ Object obj = b.get(key);
+ if (obj == null)
+ Log.w(TAG, funcName + ": " + key + ": null value");
+ else if (obj.getClass() != expectedClass)
+ Log.w(TAG, funcName + ": " + key + ": expected " + expectedClass.getClass().getName() + ", got " + obj.getClass().getName());
+ else
+ value = obj;
+ }
+ }
+ return value;
+ }
+ // ---------------------------------- HOST ----------------------------------------- //
+
+ private static Object getExtraValueSafe(Intent i, String key, Class> expectedClass, String funcName) {
+ return (i.hasExtra(key)) ?
+ getBundleValueSafe(i.getExtras(), key, expectedClass, funcName) :
+ null;
+ }
+
+ // ---------------------------------- HELPER FUNCTIONS -------------------------------- //
+
+ private static boolean hostSupports(Bundle extrasFromHost, int capabilityFlag) {
+ Integer flags = (Integer) getBundleValueSafe(extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports");
+ return
+ (flags != null) &&
+ ((flags & capabilityFlag) > 0)
+ ;
+ }
+
+ public static int getPackageVersionCode(PackageManager pm, String packageName) {
+
+ int code = -1;
+
+ if (pm != null) {
+ try {
+ PackageInfo pi = pm.getPackageInfo(packageName, 0);
+ if (pi != null)
+ code = pi.versionCode;
+ } catch (Exception e) {
+ Log.e(TAG, "getPackageVersionCode: exception getting package info");
+ }
+ }
+
+ return code;
+ }
+
+ private static boolean variableNameIsLocal(String varName) {
+
+ int digitCount = 0;
+ int length = varName.length();
+
+ for (int x = 0; x < length; x++) {
+ char ch = varName.charAt(x);
+
+ if (Character.isUpperCase(ch))
+ return false;
+ else if (Character.isDigit(ch))
+ digitCount++;
+ }
+
+ if (digitCount == (varName.length() - 1))
+ return false;
+
+ return true;
+ }
+
+ private static String[] getStringArrayFromBundleString(Bundle bundle, String key, String funcName) {
+
+ String spec = (String) getBundleValueSafe(bundle, key, String.class, funcName);
+
+ String[] toReturn = null;
+
+ if (spec != null)
+ toReturn = spec.split(" ");
+
+ return toReturn;
+ }
+
+ private static void addStringArrayToBundleAsString(String[] toAdd, Bundle bundle, String key, String callerName) {
+
+ StringBuilder builder = new StringBuilder();
+
+ if (toAdd != null) {
+
+ for (String keyName : toAdd) {
+
+ if (keyName.contains(" "))
+ Log.w(TAG, callerName + ": ignoring bad keyName containing space: " + keyName);
+ else {
+ if (builder.length() > 0)
+ builder.append(' ');
+
+ builder.append(keyName);
+ }
+
+ if (builder.length() > 0)
+ bundle.putString(key, builder.toString());
+ }
+ }
+ }
+
+ /**
+ * Generate a sequence of secure random positive integers which is guaranteed not to repeat
+ * in the last 100 calls to this function.
+ *
+ * @return a random positive integer
+ */
+ public static int getPositiveNonRepeatingRandomInteger() {
+
+ // initialize on first call
+ if (sr == null) {
+ sr = new SecureRandom();
+ lastRandomsSeen = new int[RANDOM_HISTORY_SIZE];
+
+ for (int x = 0; x < lastRandomsSeen.length; x++)
+ lastRandomsSeen[x] = -1;
+ }
+
+ int toReturn;
+ do {
+ // pick a number
+ toReturn = sr.nextInt(Integer.MAX_VALUE);
+
+ // check we havn't see it recently
+ for (int seen : lastRandomsSeen) {
+ if (seen == toReturn) {
+ toReturn = -1;
+ break;
+ }
+ }
+ }
+ while (toReturn == -1);
+
+ // update history
+ lastRandomsSeen[randomInsertPointer] = toReturn;
+ randomInsertPointer = (randomInsertPointer + 1) % lastRandomsSeen.length;
+
+ return toReturn;
+ }
+
+ /**
+ * Possible encodings of text in bundle values
+ *
+ * @see #setKeyEncoding(Bundle, String[], Encoding)
+ */
+ public enum Encoding {
+ JSON
+ }
+
+ public static class Setting {
+
+ /**
+ * Variable name into which a description of any error that occurred can be placed
+ * for the user to process.
+ *
+ * Should *only* be set when the BroadcastReceiver result code indicates a failure.
+ *
+ * Note that the user needs to have configured the task to continue after failure of the plugin
+ * action otherwise they will not be able to make use of the error message.
+ *
+ * For use with #addRelevantVariableList(Intent, String[]) and #addVariableBundle(Bundle, Bundle)
+ */
+ public final static String VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg";
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+
+ public final static int REQUESTED_TIMEOUT_MS_NONE = 0;
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+
+ public final static int REQUESTED_TIMEOUT_MS_MAX = 3599000;
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+
+ public final static int REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000;
+ /**
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see Host#getSettingResultCode(Intent)
+ */
+ public final static String EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE";
+ /**
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see Host#getSettingResultCode(Intent)
+ */
+
+ public final static int RESULT_CODE_OK = Activity.RESULT_OK;
+ public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
+ public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
+ public final static int RESULT_CODE_PENDING = Activity.RESULT_FIRST_USER + 2;
+ public final static int RESULT_CODE_UNKNOWN = Activity.RESULT_FIRST_USER + 3;
+ /**
+ * If a plugin wants to define it's own error codes, start numbering them here.
+ * The code will be placed in an error variable (%err in the case of Tasker) for
+ * the user to process after the plugin action.
+ */
+
+ public final static int RESULT_CODE_FAILED_PLUGIN_FIRST = Activity.RESULT_FIRST_USER + 9;
+ public final static String EXTRA_CALL_SERVICE_PACKAGE = TaskerIntent.TASKER_PACKAGE + ".EXTRA_CALL_SERVICE_PACKAGE";
+ public final static String EXTRA_CALL_SERVICE = TaskerIntent.TASKER_PACKAGE + ".EXTRA_CALL_SERVICE";
+ public final static String EXTRA_CALL_SERVICE_FOREGROUND = TaskerIntent.TASKER_PACKAGE + ".EXTRA_CALL_SERVICE_FOREGROUND";
+ public final static String EXTRA_RESULT_CALL_URI = BASE_KEY + ".EXTRA_RESULT_CALL_URI";
+ /**
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ */
+ private final static String BUNDLE_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS";
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+ private final static String EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT";
+ /**
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see Host#addCompletionIntent(Intent, Intent, boolean)
+ */
+ private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT";
+
+ /**
+ * Used by: plugin EditActivity.
+ *
+ * Indicates to plugin that host will replace variables in specified bundle keys.
+ *
+ * Replacement takes place every time the setting is fired, before the bundle is
+ * passed to the plugin FireReceiver.
+ *
+ * @param extrasFromHost intent extras from the intent received by the edit activity
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ */
+ public static boolean hostSupportsOnFireVariableReplacement(Bundle extrasFromHost) {
+ return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT);
+ }
+
+ /**
+ * Used by: plugin EditActivity.
+ *
+ * Description as above.
+ *
+ * This version also includes backwards compatibility with pre 4.2 Tasker versions.
+ * At some point this function will be deprecated.
+ *
+ * @param editActivity the plugin edit activity, needed to test calling Tasker version
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ */
+
+ public static boolean hostSupportsOnFireVariableReplacement(Activity editActivity) {
+
+ boolean supportedFlag = hostSupportsOnFireVariableReplacement(editActivity.getIntent().getExtras());
+
+ if (!supportedFlag) {
+
+ ComponentName callingActivity = editActivity.getCallingActivity();
+
+ if (callingActivity == null)
+ Log.w(TAG, "hostSupportsOnFireVariableReplacement: null callingActivity, defaulting to false");
+ else {
+ String callerPackage = callingActivity.getPackageName();
+
+ // Tasker only supporteed this from 1.0.10
+ supportedFlag =
+ (callerPackage.startsWith(BASE_KEY)) &&
+ (getPackageVersionCode(editActivity.getPackageManager(), callerPackage) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION)
+ ;
+ }
+ }
+
+ return supportedFlag;
+ }
+
+ public static boolean hostSupportsSynchronousExecution(Bundle extrasFromHost) {
+ return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION);
+ }
+
+ /**
+ * Request the host to wait the specified number of milliseconds before continuing.
+ * Note that the host may choose to ignore the request.
+ *
+ * Maximum value is REQUESTED_TIMEOUT_MS_MAX.
+ * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting
+ * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for
+ * a result).
+ *
+ * Used in EditActivity, before setResult().
+ *
+ * @param intentToHost the intent being returned to the host
+ * @param timeoutMS
+ */
+ public static void requestTimeoutMS(Intent intentToHost, int timeoutMS) {
+ if (timeoutMS < 0)
+ Log.w(TAG, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")");
+ else {
+ if (
+ (timeoutMS > REQUESTED_TIMEOUT_MS_MAX) &&
+ (timeoutMS != REQUESTED_TIMEOUT_MS_NEVER)
+ ) {
+ Log.w(TAG, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")");
+ timeoutMS = REQUESTED_TIMEOUT_MS_MAX;
+ }
+ intentToHost.putExtra(EXTRA_REQUESTED_TIMEOUT, timeoutMS);
+ }
+ }
+
+ /**
+ * Used by: plugin EditActivity
+ *
+ * Indicates to host which bundle keys should be replaced.
+ *
+ * @param resultBundleToHost the bundle being returned to the host
+ * @param listOfKeyNames which bundle keys to replace variables in when setting fires
+ * @see #hostSupportsOnFireVariableReplacement(Bundle)
+ * @see #setKeyEncoding(Bundle, String[], Encoding)
+ */
+ public static void setVariableReplaceKeys(Bundle resultBundleToHost, String[] listOfKeyNames) {
+ addStringArrayToBundleAsString(
+ listOfKeyNames, resultBundleToHost, BUNDLE_KEY_VARIABLE_REPLACE_STRINGS,
+ "setVariableReplaceKeys"
+ );
+ }
+
+ /**
+ * Used by: plugin FireReceiver
+ *
+ * Indicates to plugin whether the host will process variables which it passes back
+ *
+ * @param extrasFromHost intent extras from the intent received by the FireReceiver
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ */
+ public static boolean hostSupportsVariableReturn(Bundle extrasFromHost) {
+ return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES);
+ }
+
+ public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars ) {
+ return signalFinish(context, originalFireIntent, resultCode, vars,null);
+ }
+ /**
+ * Used by: plugin FireReceiver
+ *
+ * Tell the host that the plugin has finished execution.
+ *
+ * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive().
+ *
+ * @param originalFireIntent the intent received from the host (via onReceive())
+ * @param resultCode level of success in performing the settings
+ * @param vars any variables that the plugin wants to set in the host
+ * @see #hostSupportsSynchronousExecution(Bundle)
+ */
+ public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars, Uri callbackUri ) {
+
+ String errorPrefix = "signalFinish: ";
+
+ boolean okFlag = false;
+
+ String completionIntentString = (String) getExtraValueSafe(originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish");
+
+ if (completionIntentString != null) {
+
+ Uri completionIntentUri = null;
+ try {
+ completionIntentUri = Uri.parse(completionIntentString);
+ }
+ // should only throw NullPointer but don't particularly trust it
+ catch (Exception e) {
+ Log.w(TAG, errorPrefix + "couldn't parse " + completionIntentString);
+ }
+
+ if (completionIntentUri != null) {
+ try {
+ Intent completionIntent = Intent.parseUri(completionIntentString, Intent.URI_INTENT_SCHEME);
+
+ completionIntent.putExtra(EXTRA_RESULT_CODE, resultCode);
+
+ if (vars != null)
+ completionIntent.putExtra(EXTRA_VARIABLES_BUNDLE, vars);
+
+ if(callbackUri != null){
+ completionIntent.putExtra(EXTRA_RESULT_CALL_URI, callbackUri);
+ }
+ String callServicePackage = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_PACKAGE, String.class, "signalFinish");
+ String callService = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE, String.class, "signalFinish");
+ Boolean foreground = (Boolean) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_FOREGROUND, Boolean.class, "signalFinish");
+ if (callServicePackage != null && callService != null && foreground != null) {
+ completionIntent.setComponent(new ComponentName(callServicePackage, callService));
+ if (foreground && android.os.Build.VERSION.SDK_INT >= 26) {
+ context.startForegroundService(completionIntent);
+ } else {
+ context.startService(completionIntent);
+ }
+ } else {
+ context.sendBroadcast(completionIntent);
+ }
+
+ okFlag = true;
+ } catch (URISyntaxException e) {
+ Log.w(TAG, errorPrefix + "bad URI: " + completionIntentUri);
+ } catch (IllegalStateException e) {
+ Log.w(TAG, errorPrefix + "host was not in foreground: " + completionIntentUri,e);
+ }
+ }
+ }
+
+ return okFlag;
+ }
+
+ /**
+ * Check for a hint on the timeout value the host is using.
+ * Used by: plugin FireReceiver.
+ * Requires Tasker 4.7+
+ *
+ * @param extrasFromHost intent extras from the intent received by the FireReceiver
+ * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available.
+ * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
+ */
+ public static int getHintTimeoutMS(Bundle extrasFromHost) {
+
+ int timeoutMS = -1;
+
+ Bundle hintsBundle = (Bundle) TaskerPlugin.getBundleValueSafe(extrasFromHost, EXTRA_HINTS_BUNDLE, Bundle.class, "getHintTimeoutMS");
+
+ if (hintsBundle != null) {
+
+ Integer val = (Integer) getBundleValueSafe(hintsBundle, BUNDLE_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS");
+
+ if (val != null)
+ timeoutMS = val;
+ }
+
+ return timeoutMS;
+ }
+ }
+
+ public static class Condition {
+
+ /**
+ * @see #getResultReceiver(Intent)
+ */
+ public final static String EXTRA_RESULT_RECEIVER = TaskerIntent.TASKER_PACKAGE + ".EXTRA_RESULT_RECEIVER";
+
+ /**
+ * Used by: plugin QueryReceiver
+ *
+ * Indicates to plugin whether the host will process variables which it passes back
+ *
+ * @param extrasFromHost intent extras from the intent received by the QueryReceiver
+ * @see #addVariableBundle(Bundle, Bundle)
+ */
+ public static boolean hostSupportsVariableReturn(Bundle extrasFromHost) {
+ return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES);
+ }
+
+
+ public static ResultReceiver getResultReceiver(Intent intentFromHost) {
+ if (intentFromHost == null) {
+ return null;
+ }
+ return (ResultReceiver) getExtraValueSafe(intentFromHost, EXTRA_RESULT_RECEIVER, ResultReceiver.class, "getResultReceiver");
+
+ }
+ }
+
+ public static class Event {
+
+ public final static String PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID";
+
+ private final static String EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA";
+
+ /**
+ * @param extrasFromHost intent extras from the intent received by the QueryReceiver
+ * @see #addPassThroughData(Intent, Bundle)
+ */
+ public static boolean hostSupportsRequestQueryDataPassThrough(Bundle extrasFromHost) {
+ return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH);
+ }
+
+ /**
+ * Specify a bundle of data (probably representing whatever change happened in the condition)
+ * which will be included in the QUERY_CONDITION broadcast sent by the host for each
+ * event instance of the plugin.
+ *
+ * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
+ * with the REQUEST_QUERY that caused it.
+ *
+ * Note that for security reasons it is advisable to also store a message ID with the bundle
+ * which can be compared to known IDs on receipt. The host cannot validate the source of
+ * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible.
+ * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's
+ * own ID to the pass through bundle.
+ *
+ * Note also that there are several situations where REQUEST_QUERY will not result in a
+ * QUERY_CONDITION intent (e.g. event throttling by the host), so plugin-local data
+ * indexed with a message ID needs to be timestamped and eventually timed-out.
+ *
+ * This function can be called multiple times, each time all keys in data will be added to
+ * that of previous calls.
+ *
+ * @param requestQueryIntent intent being sent to the host
+ * @param data the data to be passed-through
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #retrievePassThroughData(Intent)
+ * @see #addPassThroughMessageID
+ */
+ public static void addPassThroughData(Intent requestQueryIntent, Bundle data) {
+
+ Bundle passThroughBundle = retrieveOrCreatePassThroughBundle(requestQueryIntent);
+
+ passThroughBundle.putAll(data);
+ }
+
+ /**
+ * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
+ * by a REQUEST_QUERY from the plugin.
+ *
+ * Note that if addPassThroughMessageID() was previously called, the data will contain an extra
+ * key TaskerPlugin.Event.PASS_THOUGH_BUNDLE_MESSAGE_ID_KEY.
+ *
+ * @param queryConditionIntent QUERY_REQUEST sent from host
+ * @return data previously added to the REQUEST_QUERY intent
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #addPassThroughData(Intent, Bundle)
+ */
+ public static Bundle retrievePassThroughData(Intent queryConditionIntent) {
+ return (Bundle) getExtraValueSafe(
+ queryConditionIntent,
+ EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA,
+ Bundle.class,
+ "retrievePassThroughData"
+ );
+ }
+
+ /**
+ * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding
+ * QUERY_CONDITION broadcast sent by the host for each event instance of the plugin.
+ *
+ * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
+ * with the REQUEST_QUERY that caused it. It also allows the message to be verified
+ * by the plugin to prevent e.g. replay attacks
+ *
+ * @param requestQueryIntent intent being sent to the host
+ * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #retrievePassThroughData(Intent)
+ */
+ public static int addPassThroughMessageID(Intent requestQueryIntent) {
+
+ Bundle passThroughBundle = retrieveOrCreatePassThroughBundle(requestQueryIntent);
+
+ int id = getPositiveNonRepeatingRandomInteger();
+
+ passThroughBundle.putInt(PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, id);
+
+ return id;
+ }
+
+ /*
+ * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
+ * by a REQUEST_QUERY from the plugin.
+ *
+ * @param queryConditionIntent QUERY_REQUEST sent from host
+ * @return the ID which was passed through by the host, or -1 if no ID was found
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #addPassThroughData(Intent,Bundle)
+ */
+ public static int retrievePassThroughMessageID(Intent queryConditionIntent) {
+
+ int toReturn = -1;
+
+ Bundle passThroughData = Event.retrievePassThroughData(queryConditionIntent);
+
+ if (passThroughData != null) {
+ Integer id = (Integer) getBundleValueSafe(
+ passThroughData,
+ PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY,
+ Integer.class,
+ "retrievePassThroughMessageID"
+ );
+
+ if (id != null)
+ toReturn = id;
+ }
+
+ return toReturn;
+ }
+
+ // internal use
+ private static Bundle retrieveOrCreatePassThroughBundle(Intent requestQueryIntent) {
+
+ Bundle passThroughBundle;
+
+ if (requestQueryIntent.hasExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA))
+ passThroughBundle = requestQueryIntent.getBundleExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA);
+ else {
+ passThroughBundle = new Bundle();
+ requestQueryIntent.putExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughBundle);
+ }
+
+ return passThroughBundle;
+ }
+ }
+
+ public static class Host {
+
+ /**
+ * Tell the plugin what capabilities the host support. This should be called when sending
+ * intents to any EditActivity, FireReceiver or QueryReceiver.
+ *
+ * @param toPlugin the intent we're sending
+ * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags
+ */
+ public static Intent addCapabilities(Intent toPlugin, int capabilities) {
+ return toPlugin.putExtra(EXTRA_HOST_CAPABILITIES, capabilities);
+ }
+
+ /**
+ * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin
+ * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true.
+ *
+ * @param fireIntent fire intent going to the plugin
+ * @param completionIntent intent which will signal the host that the plugin is finished.
+ * Implementation is host-dependent.
+ */
+ public static void addCompletionIntent(Intent fireIntent, Intent completionIntent, ComponentName callService) {
+ if (callService != null) {
+ completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_PACKAGE, callService.getPackageName());
+ completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE, callService.getClassName());
+ completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_FOREGROUND, android.os.Build.VERSION.SDK_INT >= 26);
+ }
+ fireIntent.putExtra(
+ Setting.EXTRA_PLUGIN_COMPLETION_INTENT,
+ completionIntent.toUri(Intent.URI_INTENT_SCHEME)
+ );
+ }
+
+ /**
+ * When a setting plugin is finished, it sends the host the intent which was passed to it
+ * via @code{addCompletionIntent}.
+ *
+ * @param completionIntent intent returned from the plugin when it finished.
+ * @return resultCode measure of plugin success, defaults to UNKNOWN
+ */
+ public static int getSettingResultCode(Intent completionIntent) {
+
+ Integer val = (Integer) getExtraValueSafe(completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode");
+
+ return (val == null) ? Setting.RESULT_CODE_UNKNOWN : val;
+ }
+
+ /**
+ * Extract a bundle of variables from an intent received from the FireReceiver. This
+ * should be called if the host previously indicated to the plugin
+ * that it supports setting variable return.
+ *
+ * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive()
+ * @return variables a bundle of variable name/value pairs
+ * @see #addCapabilities(Intent, int)
+ */
+
+ public static Bundle getVariablesBundle(Bundle resultExtras) {
+ return (Bundle) getBundleValueSafe(
+ resultExtras, EXTRA_VARIABLES_BUNDLE, Bundle.class, "getVariablesBundle"
+ );
+ }
+
+ /**
+ * Inform a setting plugin of the timeout value the host is using.
+ *
+ * @param toPlugin the intent we're sending
+ * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from
+ * that which the plugin requests.
+ * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
+ */
+ public static void addHintTimeoutMS(Intent toPlugin, int timeoutMS) {
+ getHintsBundle(toPlugin, "addHintTimeoutMS").putInt(BUNDLE_KEY_HINT_TIMEOUT_MS, timeoutMS);
+ }
+
+ private static Bundle getHintsBundle(Intent intent, String funcName) {
+
+ Bundle hintsBundle = (Bundle) getExtraValueSafe(intent, EXTRA_HINTS_BUNDLE, Bundle.class, funcName);
+
+ if (hintsBundle == null) {
+ hintsBundle = new Bundle();
+ intent.putExtra(EXTRA_HINTS_BUNDLE, hintsBundle);
+ }
+
+ return hintsBundle;
+ }
+
+ public static boolean haveRequestedTimeout(Bundle extrasFromPluginEditActivity) {
+ return extrasFromPluginEditActivity.containsKey(Setting.EXTRA_REQUESTED_TIMEOUT);
+ }
+
+ public static int getRequestedTimeoutMS(Bundle extrasFromPluginEditActivity) {
+ return
+ (Integer) getBundleValueSafe(
+ extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT, Integer.class, "getRequestedTimeout"
+ )
+ ;
+ }
+
+ public static String[] getSettingVariableReplaceKeys(Bundle fromPluginEditActivity) {
+ return getStringArrayFromBundleString(
+ fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS,
+ "getSettingVariableReplaceKeys"
+ );
+ }
+
+ public static String[] getKeysWithEncoding(Bundle fromPluginEditActivity, Encoding encoding) {
+
+ String[] toReturn = null;
+
+ if (Encoding.JSON.equals(encoding))
+ toReturn = getStringArrayFromBundleString(
+ fromPluginEditActivity, TaskerPlugin.BUNDLE_KEY_ENCODING_JSON_KEYS,
+ "getKeyEncoding:JSON"
+ );
+ else
+ Log.w(TAG, "Host.getKeyEncoding: unknown encoding " + encoding);
+
+ return toReturn;
+ }
+
+ public static boolean haveRelevantVariables(Bundle b) {
+ return b.containsKey(BUNDLE_KEY_RELEVANT_VARIABLES);
+ }
+
+ public static void cleanRelevantVariables(Bundle b) {
+ b.remove(BUNDLE_KEY_RELEVANT_VARIABLES);
+ }
+
+ public static void cleanHints(Bundle extras) {
+ extras.remove(TaskerPlugin.EXTRA_HINTS_BUNDLE);
+ }
+
+ public static void cleanRequestedTimeout(Bundle extras) {
+ extras.remove(Setting.EXTRA_REQUESTED_TIMEOUT);
+ }
+
+ public static void cleanSettingReplaceVariables(Bundle b) {
+ b.remove(Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS);
+ }
+ }
+
+}
diff --git a/taskerpluginlibrary/src/main/res/mipmap/ic_launcher.png b/taskerpluginlibrary/src/main/res/mipmap/ic_launcher.png
new file mode 100644
index 00000000..ff10afd6
Binary files /dev/null and b/taskerpluginlibrary/src/main/res/mipmap/ic_launcher.png differ
diff --git a/taskerpluginlibrary/src/main/res/values/strings.xml b/taskerpluginlibrary/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3d5b4631
--- /dev/null
+++ b/taskerpluginlibrary/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+