/* * PowerSwitch by Max Rosin & Markus Ressel * Copyright (C) 2015 Markus Ressel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html // Release Notes // v1.1 20140202 // added function variableNameValid() // fixed some javadoc entries (thanks to David Stone) // v1.2 20140211 // added ACTION_EDIT_EVENT // v1.3 20140227 // added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER // requestTimeoutMS(): added range check // v1.4 20140516 // support for data pass through in REQUEST_QUERY intent // some javadoc entries fixed (thanks again David :-)) // v1.5 20141120 // added RESULT_CODE_FAILED_PLUGIN_FIRST // added Setting.VARNAME_ERROR_MESSAGE // v1.6 20150213 // added Setting.getHintTimeoutMS() // added Host.addHintTimeoutMS() // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html // Release Notes // v1.1 20140202 // added function variableNameValid() // fixed some javadoc entries (thanks to David Stone) // v1.2 20140211 // added ACTION_EDIT_EVENT // v1.3 20140227 // added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER // requestTimeoutMS(): added range check // v1.4 20140516 // support for data pass through in REQUEST_QUERY intent // some javadoc entries fixed (thanks again David :-)) // v1.5 20141120 // added RESULT_CODE_FAILED_PLUGIN_FIRST // added Setting.VARNAME_ERROR_MESSAGE // v1.6 20150213 // added Setting.getHintTimeoutMS() // added Host.addHintTimeoutMS() // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html // Release Notes // v1.1 20140202 // added function variableNameValid() // fixed some javadoc entries (thanks to David Stone) // v1.2 20140211 // added ACTION_EDIT_EVENT // v1.3 20140227 // added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER // requestTimeoutMS(): added range check // v1.4 20140516 // support for data pass through in REQUEST_QUERY intent // some javadoc entries fixed (thanks again David :-)) // v1.5 20141120 // added RESULT_CODE_FAILED_PLUGIN_FIRST // added Setting.VARNAME_ERROR_MESSAGE // v1.6 20150213 // added Setting.getHintTimeoutMS() // added Host.addHintTimeoutMS() // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html // Release Notes // v1.1 20140202 // added function variableNameValid() // fixed some javadoc entries (thanks to David Stone) // v1.2 20140211 // added ACTION_EDIT_EVENT // v1.3 20140227 // added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER // requestTimeoutMS(): added range check // v1.4 20140516 // support for data pass through in REQUEST_QUERY intent // some javadoc entries fixed (thanks again David :-)) // v1.5 20141120 // added RESULT_CODE_FAILED_PLUGIN_FIRST // added Setting.VARNAME_ERROR_MESSAGE // v1.6 20150213 // added Setting.getHintTimeoutMS() // added Host.addHintTimeoutMS() // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html // Release Notes // v1.1 20140202 // added function variableNameValid() // fixed some javadoc entries (thanks to David Stone) // v1.2 20140211 // added ACTION_EDIT_EVENT // v1.3 20140227 // added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER // requestTimeoutMS(): added range check // v1.4 20140516 // support for data pass through in REQUEST_QUERY intent // some javadoc entries fixed (thanks again David :-)) // v1.5 20141120 // added RESULT_CODE_FAILED_PLUGIN_FIRST // added Setting.VARNAME_ERROR_MESSAGE // v1.6 20150213 // added Setting.getHintTimeoutMS() // added Host.addHintTimeoutMS() package eu.power_switch.api.taskerplugin; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.util.Log; import java.net.URISyntaxException; import java.security.SecureRandom; import java.util.regex.Pattern; /** * Helper class for Tasker plugin specific tasks * <p/> * Created by Markus on 24.02.2016. */ public class TaskerPlugin { // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html // Release Notes // v1.1 20140202 // added function variableNameValid() // fixed some javadoc entries (thanks to David Stone) // v1.2 20140211 // added ACTION_EDIT_EVENT // v1.3 20140227 // added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER // requestTimeoutMS(): added range check // v1.4 20140516 // support for data pass through in REQUEST_QUERY intent // some javadoc entries fixed (thanks again David :-)) // v1.5 20141120 // added RESULT_CODE_FAILED_PLUGIN_FIRST // added Setting.VARNAME_ERROR_MESSAGE // v1.6 20150213 // added Setting.getHintTimeoutMS() // added Host.addHintTimeoutMS() //@formatter:off /** * @see Setting#hostSupportsVariableReturn(Bundle) */ public final static int EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2; /** * @see Condition#hostSupportsVariableReturn(Bundle) */ public final static int EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4; /** * @see Setting#hostSupportsOnFireVariableReplacement(Bundle) */ public final static int EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8; public final static int EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32; public final static int EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64; public final static String VARIABLE_PREFIX = "%"; // when generating non-repeating integers, look this far back for repeats // see getPositiveNonRepeatingRandomInteger() private final static int RANDOM_HISTORY_SIZE = 100; private final static String VARIABLE_NAME_START_EXPRESSION = "[\\w&&[^_]]"; private final static String VARIABLE_NAME_MID_EXPRESSION = "[\\w0-9]+"; private final static String VARIABLE_NAME_END_EXPRESSION = "[\\w0-9&&[^_]]"; private final static String VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION = VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION; public final static String VARIABLE_NAME_MATCH_EXPRESSION = VARIABLE_PREFIX + "+" + VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION; private final static String TAG = "TaskerPlugin"; private final static String BASE_KEY = "net.dinglisch.android.tasker"; /** * Action that the EditActivity for an event plugin should be launched by */ public final static String ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT"; private final static String EXTRAS_PREFIX = BASE_KEY + ".extras."; private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80; /** * @see #addVariableBundle(Bundle, Bundle) * @see Host#getVariablesBundle(Bundle) */ private final static String EXTRA_VARIABLES_BUNDLE = EXTRAS_PREFIX + "VARIABLES"; /** * Host capabilities, passed to plugin with edit intents */ private final static String EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES"; /** * @see Setting#hostSupportsVariableReturn(Bundle) */ private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16; public final static int EXTRA_HOST_CAPABILITY_ALL = EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES | EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES | EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT | EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES | EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION | EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH; /** * Miscellaneous operational hints going one way or the other * * @see Setting#hostSupportsVariableReturn(Bundle) */ private final static String EXTRA_HINTS_BUNDLE = EXTRAS_PREFIX + "HINTS"; private final static String BUNDLE_KEY_HINT_PREFIX = ".hints."; private final static String BUNDLE_KEY_HINT_TIMEOUT_MS = BUNDLE_KEY_HINT_PREFIX + "TIMEOUT"; /** * @see #hostSupportsRelevantVariables(Bundle) * @see #addRelevantVariableList(Intent, String[]) * @see #getRelevantVariableList(Bundle) */ private final static String BUNDLE_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES"; private static Pattern VARIABLE_NAME_MATCH_PATTERN = null; // state tracking for random number sequence private static int[] lastRandomsSeen = null; private static int randomInsertPointer = 0; private static SecureRandom sr = null; //@formatter:on public static boolean hostSupportsRelevantVariables(Bundle extrasFromHost) { return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES); } /** * Specifies to localHost which variables might be used by the plugin. * <p/> * Used in EditActivity, before setResult(). * * @param intentToHost the intent being returned to the localHost * @param variableNames array of relevant variable names */ public static void addRelevantVariableList(Intent intentToHost, String[] variableNames) { intentToHost.putExtra(BUNDLE_KEY_RELEVANT_VARIABLES, variableNames); } // ----------------------------- SETTING PLUGIN ONLY --------------------------------- // /** * Validate a variable name. * <p/> * 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; } // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- // /** * Allows the plugin/localHost to indicate to each other a set of variables which they are referencing. * The localHost may use this to e.g. show a variable selection list in it's UI. * The localHost 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; } // ----------------------------- EVENT PLUGIN ONLY --------------------------------- // /** * Used by: plugin QueryReceiver, FireReceiver * <p/> * Add a bundle of variable name/value pairs. * <p/> * Names must be valid Tasker local variable names. * Values must be String, String [] or ArrayList<String> * Null values cause deletion of possible already-existing variables. * * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras()) * @param variables the variables to send * @see Setting#hostSupportsVariableReturn(Bundle) * @see #variableNameValid(String) */ public static void addVariableBundle(Bundle resultExtras, Bundle variables) { resultExtras.putBundle(EXTRA_VARIABLES_BUNDLE, variables); } // ---------------------------------- HOST ----------------------------------------- // 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; } // ---------------------------------- HELPER FUNCTIONS -------------------------------- // private static Object getExtraValueSafe(Intent i, String key, Class<?> expectedClass, String funcName) { return (i.hasExtra(key)) ? getBundleValueSafe(i.getExtras(), key, expectedClass, funcName) : null; } 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++; } } return digitCount != (varName.length() - 1); } /** * 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; } public static class Setting { /** * Variable name into which a description of any error that occurred can be placed * for the user to process. * <p/> * Should *only* be set when the BroadcastReceiver result code indicates a failure. * <p/> * 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. * <p/> * 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; /** * @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) */ private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT"; /** * Used by: plugin EditActivity. * <p/> * Indicates to plugin that localHost will replace variables in specified bundle keys. * <p/> * 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. * <p/> * Description as above. * <p/> * 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) { String callerPackage = editActivity.getCallingActivity().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 localHost to wait the specified number of milliseconds before continuing. * Note that the localHost may choose to ignore the request. * <p/> * 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). * <p/> * Used in EditActivity, before setResult(). * * @param intentToHost the intent being returned to the localHost * @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 * <p/> * Indicates to localHost which bundle keys should be replaced. * * @param resultBundleToHost the bundle being returned to the localHost * @param listOfKeyNames which bundle keys to replace variables in when setting fires * @see #hostSupportsOnFireVariableReplacement(Bundle) */ public static void setVariableReplaceKeys(Bundle resultBundleToHost, String[] listOfKeyNames) { StringBuilder builder = new StringBuilder(); if (listOfKeyNames != null) { for (String keyName : listOfKeyNames) { if (keyName.contains(" ")) { Log.w(TAG, "setVariableReplaceKeys: ignoring bad keyName containing space: " + keyName); } else { if (builder.length() > 0) { builder.append(' '); } builder.append(keyName); } if (builder.length() > 0) { resultBundleToHost.putString(BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, builder.toString()); } } } } /** * Used by: plugin FireReceiver * <p/> * Indicates to plugin whether the localHost 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); } /** * Used by: plugin FireReceiver * <p/> * Tell the localHost that the plugin has finished execution. * <p/> * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive(). * * @param originalFireIntent the intent received from the localHost (via onReceive()) * @param resultCode level of success in performing the settings * @param vars any variables that the plugin wants to set in the localHost * @see #hostSupportsSynchronousExecution(Bundle) */ public static boolean signalFinish(Context context, Intent originalFireIntent, int resultCode, Bundle vars) { 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); } context.sendBroadcast(completionIntent); okFlag = true; } catch (URISyntaxException e) { Log.w(TAG, errorPrefix + "bad URI: " + completionIntentUri); } } } return okFlag; } /** * Check for a hint on the timeout value the localHost 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 { /** * Used by: plugin QueryReceiver * <p/> * Indicates to plugin whether the localHost 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 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 localHost for each * event instance of the plugin. * <p/> * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the * with the REQUEST_QUERY that caused it. * <p/> * 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 localHost 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. * <p/> * Note also that there are several situations where REQUEST_QUERY will not result in a * QUERY_CONDITION intent (e.g. event throttling by the localHost), so plugin-local data * indexed with a message ID needs to be timestamped and eventually timed-out. * <p/> * 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 localHost * @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 localHost which was generated * by a REQUEST_QUERY from the plugin. * <p/> * 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 localHost * @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 localHost for each event instance of the plugin. * <p/> * 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 localHost * @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 localHost which was generated * by a REQUEST_QUERY from the plugin. * * @param queryConditionIntent QUERY_REQUEST sent from localHost * @return the ID which was passed through by the localHost, 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 localHost 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 localHost that the plugin is finished. * Implementation is localHost-dependent. */ public static void addCompletionIntent(Intent fireIntent, Intent completionIntent) { fireIntent.putExtra( Setting.EXTRA_PLUGIN_COMPLETION_INTENT, completionIntent.toUri(Intent.URI_INTENT_SCHEME) ); } /** * When a setting plugin is finished, it sends the localHost 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 localHost 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 localHost 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) { String spec = (String) getBundleValueSafe(fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, String.class, "getSettingVariableReplaceKeys"); String[] replaceKeys = null; if (spec != null) { replaceKeys = spec.split(" "); } return replaceKeys; } 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); } } }