diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml index ca10fce..e26e720 100644 --- a/.idea/dictionaries/tateisu.xml +++ b/.idea/dictionaries/tateisu.xml @@ -8,6 +8,7 @@ cqypc customtabs dcim + dinglisch emlsgvh enetunreach errored @@ -22,7 +23,10 @@ ioexception mobilesdk realtime + requery sephiroth + tasker + twofortyfouram \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d4a7f30..0c53332 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { minSdkVersion min_sdk_version targetSdkVersion target_sdk_version - versionCode 21 - versionName "1.14.1" + versionCode 22 + versionName "1.14.2" applicationId "jp.juggler.fadownloader" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 11c68ab..d0de03d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,9 +49,19 @@ android:name="jp.juggler.fadownloader.Receiver1" android:exported="true" > + + + + + + + + + + @@ -67,11 +77,39 @@ > - + + + + + + + + + + + + + = LocalFile.DOCUMENT_FILE_VERSION) { - val folder = DocumentFile.fromTreeUri(this, Uri.parse(sv)) - if(folder != null) { - if(folder.exists() && folder.canWrite()) { - folder_uri = sv - } - } - } else { - folder_uri = sv - } - } - if(folder_uri.isEmpty()) { - Utils.showToast(this, true, getString(R.string.local_folder_not_ok)) - return - } - - val file_type = Pref.uiFileType(pref).trim() - if(file_type.isEmpty()) { - Utils.showToast(this, true, getString(R.string.file_type_empty)) - return - } - - val location_mode = Pref.uiLocationMode(pref) - if(location_mode < 0 || location_mode > LocationTracker.LOCATION_HIGH_ACCURACY) { - Utils.showToast(this, true, getString(R.string.location_mode_invalid)) - return + val error = Receiver1.actionStart(this) + if( error != null) { + Utils.showToast(this, true, error) } - - - fun validSeconds(v : Int?) : Boolean { - return v != null && v > 0 - } - - if(repeat) { - if(! validSeconds(Pref.uiInterval.getIntOrNull(pref))) { - Utils.showToast(this, true, getString(R.string.repeat_interval_not_ok)) - return - } - } - - if(location_mode != LocationTracker.NO_LOCATION_UPDATE) { - - if(! validSeconds(Pref.uiLocationIntervalDesired.getIntOrNull(pref))) { - Utils.showToast(this, true, getString(R.string.location_update_interval_not_ok)) - return - } - if(! validSeconds(Pref.uiLocationIntervalMin.getIntOrNull(pref))) { - Utils.showToast(this, true, getString(R.string.location_update_interval_not_ok)) - return - } - } - - val force_wifi = Pref.uiForceWifi(pref) - - val ssid : String - if(! force_wifi) { - ssid = "" - } else { - ssid = Pref.uiSsid(pref).trim() - if(ssid.isEmpty()) { - Utils.showToast(this, true, getString(R.string.ssid_empty)) - return - } - } - - // 最後に押したボタンを覚えておく - pref.edit() - .put(Pref.lastMode, if(repeat) Pref.LAST_MODE_REPEAT else Pref.LAST_MODE_ONCE) - .put(Pref.lastModeUpdate, System.currentTimeMillis()) - .apply() - - // 転送サービスを開始 - val intent = Intent(this, DownloadService::class.java) - intent.action = DownloadService.ACTION_START - - intent.put(pref, Pref.uiTetherSprayInterval) - intent.put(pref, Pref.uiTetherTestConnectionTimeout) - intent.put(pref, Pref.uiWifiChangeApInterval) - intent.put(pref, Pref.uiWifiScanInterval) - intent.put(pref, Pref.uiLocationIntervalDesired) - intent.put(pref, Pref.uiLocationIntervalMin) - intent.put(pref, Pref.uiInterval) - - intent.put(pref, Pref.uiProtectedOnly) - intent.put(pref, Pref.uiSkipAlreadyDownload) - intent.put(pref, Pref.uiStopWhenTetheringOff) - intent.put(pref, Pref.uiForceWifi) - intent.put(pref, Pref.uiRepeat) - intent.put(pref, Pref.uiLocationMode) - - intent.put(ssid, Pref.uiSsid) - intent.put(folder_uri, Pref.uiFolderUri) - intent.put(file_type, Pref.uiFileType) - - intent.put(target_type, Pref.uiTargetType) - intent.putExtra(DownloadService.EXTRA_TARGET_URL, target_url) - - - startService(intent) } internal fun openHelpLayout(layout_id : Int) { diff --git a/app/src/main/java/jp/juggler/fadownloader/ActTaskerSettingAction.kt b/app/src/main/java/jp/juggler/fadownloader/ActTaskerSettingAction.kt new file mode 100644 index 0000000..3504ec9 --- /dev/null +++ b/app/src/main/java/jp/juggler/fadownloader/ActTaskerSettingAction.kt @@ -0,0 +1,114 @@ +package jp.juggler.fadownloader + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Spinner +import jp.juggler.fadownloader.util.LogTag + +class ActTaskerSettingAction : AppCompatActivity(), View.OnClickListener { + + companion object { + val log = LogTag("ActTaskerSettingAction") + + const val STATE_ACTION = "action" + } + + private lateinit var spAction : Spinner + + override fun onCreate(savedInstanceState : Bundle?) { + super.onCreate(savedInstanceState) + + try { + val callingApplicationLabel = packageManager.getApplicationLabel( + packageManager.getApplicationInfo(callingPackage, 0) + ) + title = if(callingApplicationLabel?.isNotEmpty() == true) { + "$callingApplicationLabel > ${getString(R.string.tasker_setting_action)}" + }else{ + getString(R.string.tasker_setting_action) + } + } catch(ex : PackageManager.NameNotFoundException) { + log.e(ex, "Calling package couldn't be found") + } + + + setContentView(R.layout.act_tasker_setting_action) + + spAction = findViewById(R.id.spAction) + val btnSave : View = findViewById(R.id.btnSave) + + btnSave.setOnClickListener(this) + + initSpinner( + spAction, + arrayOf( + getString(R.string.repeat), + getString(R.string.once), + getString(R.string.stop) + ), + null + ) + if(savedInstanceState != null) { + spAction.setSelection(savedInstanceState.getInt(STATE_ACTION, 0)) + } else { + val b = intent?.extras + val actionInt = b?.getInt(Receiver1.EXTRA_ACTION, - 1) + when(actionInt) { + Pref.LAST_MODE_REPEAT -> spAction.setSelection(0) + Pref.LAST_MODE_ONCE -> spAction.setSelection(1) + Pref.LAST_MODE_STOP -> spAction.setSelection(2) + } + + } + } + + override fun onSaveInstanceState(outState : Bundle?) { + outState ?: return + super.onSaveInstanceState(outState) + outState.putInt(STATE_ACTION, spAction.selectedItemPosition) + } + + override fun onClick(v : View?) { + when(v?.id) { + R.id.btnSave -> { + val actionInt = when(spAction.selectedItemPosition) { + 0 -> Pref.LAST_MODE_REPEAT + 1 -> Pref.LAST_MODE_ONCE + else -> Pref.LAST_MODE_STOP + } + val actionString = when(spAction.selectedItemPosition) { + 0 -> "REPEAT" + 1 -> "ONCE" + else -> "STOP" + } + val b = Bundle() + b.putInt(Receiver1.EXTRA_ACTION, actionInt) + val intent = Intent() + intent.putExtra(Receiver1.EXTRA_TASKER_BUNDLE, b) + intent.putExtra( + "com.twofortyfouram.locale.intent.extra.BLURB", + "action=$actionString" + ) + setResult(RESULT_OK, intent) + finish() + } + } + } + + private fun initSpinner( + sp : Spinner, + choices : Array, + action : AdapterView.OnItemSelectedListener? + ) { + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item) + adapter.setDropDownViewResource(R.layout.spinner_dropdown) + adapter.addAll(*choices) + sp.adapter = adapter + sp.onItemSelectedListener = action + } +} diff --git a/app/src/main/java/jp/juggler/fadownloader/ActTaskerSettingCondition.kt b/app/src/main/java/jp/juggler/fadownloader/ActTaskerSettingCondition.kt new file mode 100644 index 0000000..9b4e730 --- /dev/null +++ b/app/src/main/java/jp/juggler/fadownloader/ActTaskerSettingCondition.kt @@ -0,0 +1,54 @@ +package jp.juggler.fadownloader + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.view.View +import jp.juggler.fadownloader.util.LogTag + +class ActTaskerSettingCondition : AppCompatActivity(), View.OnClickListener { + + companion object { + val log = LogTag("ActTaskerSettingCondition") + + } + + override fun onCreate(savedInstanceState : Bundle?) { + super.onCreate(savedInstanceState) + + try { + val callingApplicationLabel = packageManager.getApplicationLabel( + packageManager.getApplicationInfo(callingPackage, 0) + ) + title = if(callingApplicationLabel?.isNotEmpty() == true) { + "$callingApplicationLabel > ${getString(R.string.tasker_setting_condition)}" + }else{ + getString(R.string.tasker_setting_condition) + } + } catch(ex : PackageManager.NameNotFoundException) { + log.e(ex, "Calling package couldn't be found") + } + + setContentView(R.layout.act_tasker_setting_condition) + + findViewById(R.id.btnSave).setOnClickListener(this) + + } + + override fun onClick(v : View?) { + when(v?.id) { + R.id.btnSave -> { + val b = Bundle() + val intent = Intent() + intent.putExtra(Receiver1.EXTRA_TASKER_BUNDLE, b) + intent.putExtra( + "com.twofortyfouram.locale.intent.extra.BLURB", + getString(R.string.is_alive_service) + ) + setResult(RESULT_OK, intent) + finish() + } + } + } +} diff --git a/app/src/main/java/jp/juggler/fadownloader/DownloadService.kt b/app/src/main/java/jp/juggler/fadownloader/DownloadService.kt index 623a178..965d34b 100644 --- a/app/src/main/java/jp/juggler/fadownloader/DownloadService.kt +++ b/app/src/main/java/jp/juggler/fadownloader/DownloadService.kt @@ -31,6 +31,7 @@ class DownloadService : Service() { internal const val NOTIFICATION_ID_SERVICE = 1 + @Volatile internal var service_instance : DownloadService? = null fun getStatusForActivity(context : Context) : String { @@ -125,6 +126,8 @@ class DownloadService : Service() { NetworkTracker(this, log, wifi_tracker_callback) worker_tracker = WorkerTracker(this, log) + + Receiver1.sendTaskerQueryRequery(this,ActTaskerSettingCondition::class.java.name) } private val wifi_tracker_callback = object:NetworkTracker.Callback{ @@ -174,6 +177,8 @@ class DownloadService : Service() { service_instance = null + Receiver1.sendTaskerQueryRequery(this,ActTaskerSettingCondition::class.java.name) + super.onDestroy() } diff --git a/app/src/main/java/jp/juggler/fadownloader/Receiver1.kt b/app/src/main/java/jp/juggler/fadownloader/Receiver1.kt index 4cb447f..610ed68 100644 --- a/app/src/main/java/jp/juggler/fadownloader/Receiver1.kt +++ b/app/src/main/java/jp/juggler/fadownloader/Receiver1.kt @@ -5,18 +5,24 @@ import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.net.Uri +import android.os.Build import android.support.v4.content.ContextCompat +import android.support.v4.provider.DocumentFile +import jp.juggler.fadownloader.model.LocalFile +import jp.juggler.fadownloader.tracker.LocationTracker import jp.juggler.fadownloader.util.LogTag +import jp.juggler.fadownloader.util.Utils class Receiver1 : BroadcastReceiver() { companion object { private val log = LogTag("Receiver1") - + const val ACTION_ALARM = "alarm" - + const val ACTION_NEW_FILE_NOTIFICATION_TAP = "newFileNotificationTap" - + const val ACTION_NEW_FILE_NOTIFICATION_DELETE = "newFileNotificationDelete" private fun intentReceiver1(context : Context, action : String) : Intent { @@ -28,7 +34,7 @@ class Receiver1 : BroadcastReceiver() { fun piActivity(context : Context) : PendingIntent { val intent = Intent(context, ActMain::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) - return PendingIntent.getActivity( + return PendingIntent.getActivity( context, 565, intent, @@ -52,19 +58,19 @@ class Receiver1 : BroadcastReceiver() { intentReceiver1(context, Receiver1.ACTION_NEW_FILE_NOTIFICATION_TAP), PendingIntent.FLAG_UPDATE_CURRENT ) - + fun piNewFileNotificationDelete(context : Context) : PendingIntent = - PendingIntent.getBroadcast( + PendingIntent.getBroadcast( context, 568, intentReceiver1(context, Receiver1.ACTION_NEW_FILE_NOTIFICATION_DELETE), PendingIntent.FLAG_UPDATE_CURRENT ) - fun cancelAlarm(context:Context){ + fun cancelAlarm(context : Context) { try { val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager - am?.cancel( piAlarm(context)) + am?.cancel(piAlarm(context)) } catch(ex : Throwable) { log.trace(ex, "cancelAlarm failed.") } @@ -84,13 +90,198 @@ class Receiver1 : BroadcastReceiver() { ContextCompat.startForegroundService(context, service_intent) } + fun actionStop(context : Context) { + val pref = Pref.pref(context) + pref.edit() + .put(Pref.lastMode, Pref.LAST_MODE_STOP) + .put(Pref.lastModeUpdate, System.currentTimeMillis()) + .apply() + val intent = Intent(context, DownloadService::class.java) + context.stopService(intent) + cancelAlarm(context) + } + private fun actionStart(context : Context,repeat:Boolean ){ + Pref.pref(context).edit().put(Pref.uiRepeat,repeat).apply() + val error = actionStart(context) + if(error != null){ + Utils.showToast(context,true,error) + } + } + + fun actionStart(context : Context) : String? { + val pref = Pref.pref(context) + + // LocationSettingを確認する前のrepeat引数の値を思い出す + val repeat = Pref.uiRepeat(pref) + + // 設定から値を読んでバリデーション + val target_type = Pref.uiTargetType(pref) + if(target_type < 0) { + return context.getString(R.string.target_type_invalid) + } + + val target_url = Pref.loadTargetUrl(pref, target_type) + if(target_url.isEmpty()) { + return context.getString(R.string.target_url_not_ok) + } + + var folder_uri = "" + val sv = Pref.uiFolderUri(pref) + if(sv.isNotEmpty()) { + if(Build.VERSION.SDK_INT >= LocalFile.DOCUMENT_FILE_VERSION) { + val folder = DocumentFile.fromTreeUri(context, Uri.parse(sv)) + if(folder != null) { + if(folder.exists() && folder.canWrite()) { + folder_uri = sv + } + } + } else { + folder_uri = sv + } + } + if(folder_uri.isEmpty()) { + return context.getString(R.string.local_folder_not_ok) + } + + val file_type = Pref.uiFileType(pref).trim() + if(file_type.isEmpty()) { + return context.getString(R.string.file_type_empty) + } + + val location_mode = Pref.uiLocationMode(pref) + if(location_mode < 0 || location_mode > LocationTracker.LOCATION_HIGH_ACCURACY) { + return context.getString(R.string.location_mode_invalid) + } + + + fun validSeconds(v : Int?) : Boolean { + return v != null && v > 0 + } + + if(repeat) { + if(! validSeconds(Pref.uiInterval.getIntOrNull(pref))) { + return context.getString(R.string.repeat_interval_not_ok) + } + } + + if(location_mode != LocationTracker.NO_LOCATION_UPDATE) { + + if(! validSeconds(Pref.uiLocationIntervalDesired.getIntOrNull(pref))) { + return context.getString(R.string.location_update_interval_not_ok) + } + if(! validSeconds(Pref.uiLocationIntervalMin.getIntOrNull(pref))) { + return context.getString(R.string.location_update_interval_not_ok) + } + } + + val force_wifi = Pref.uiForceWifi(pref) + + val ssid : String + if(! force_wifi) { + ssid = "" + } else { + ssid = Pref.uiSsid(pref).trim() + if(ssid.isEmpty()) { + return context.getString(R.string.ssid_empty) + } + } + + // 最後に押したボタンを覚えておく + pref.edit() + .put(Pref.lastMode, if(repeat) Pref.LAST_MODE_REPEAT else Pref.LAST_MODE_ONCE) + .put(Pref.lastModeUpdate, System.currentTimeMillis()) + .apply() + + // 転送サービスを開始 + val intent = Intent(context, DownloadService::class.java) + intent.action = DownloadService.ACTION_START + + intent.put(pref, Pref.uiTetherSprayInterval) + intent.put(pref, Pref.uiTetherTestConnectionTimeout) + intent.put(pref, Pref.uiWifiChangeApInterval) + intent.put(pref, Pref.uiWifiScanInterval) + intent.put(pref, Pref.uiLocationIntervalDesired) + intent.put(pref, Pref.uiLocationIntervalMin) + intent.put(pref, Pref.uiInterval) + + intent.put(pref, Pref.uiProtectedOnly) + intent.put(pref, Pref.uiSkipAlreadyDownload) + intent.put(pref, Pref.uiStopWhenTetheringOff) + intent.put(pref, Pref.uiForceWifi) + intent.put(pref, Pref.uiRepeat) + intent.put(pref, Pref.uiLocationMode) + + intent.put(ssid, Pref.uiSsid) + intent.put(folder_uri, Pref.uiFolderUri) + intent.put(file_type, Pref.uiFileType) + + intent.put(target_type, Pref.uiTargetType) + intent.putExtra(DownloadService.EXTRA_TARGET_URL, target_url) + + ContextCompat.startForegroundService(context, intent) + return null + } + + const val ACTION_TASKER_ACTION = "com.twofortyfouram.locale.intent.action.FIRE_SETTING" + const val EXTRA_TASKER_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE" + const val EXTRA_ACTION = "action" + + private fun onTaskerAction(context : Context, intent : Intent) { + val b = intent.getBundleExtra(EXTRA_TASKER_BUNDLE) + if(b != null) { + val actionInt = b.getInt(EXTRA_ACTION, - 1) + when(actionInt) { + Pref.LAST_MODE_STOP -> actionStop(context) + Pref.LAST_MODE_ONCE ->actionStart(context,false) + Pref.LAST_MODE_REPEAT ->actionStart(context,true) + } + } + } + + const val ACTION_TASKER_QUERY_CONDITION = "com.twofortyfouram.locale.intent.action.QUERY_CONDITION" + + private const val RESULT_CONDITION_SATISFIED = 16 + private const val RESULT_CONDITION_UNSATISFIED = 17 + @Suppress("unused") + private const val RESULT_CONDITION_UNKNOWN = 18 + + private fun onTaskerQueryCondition():Int { + val isServiceAlive = DownloadService.service_instance != null + return when( isServiceAlive ){ + true -> RESULT_CONDITION_SATISFIED + false-> RESULT_CONDITION_UNSATISFIED + } + } + + private const val ACTION_REQUEST_QUERY = "com.twofortyfouram.locale.intent.action.REQUEST_QUERY" + private const val EXTRA_ACTIVITY = "com.twofortyfouram.locale.intent.extra.ACTIVITY" + + fun sendTaskerQueryRequery(context:Context ,editActivityClassName:String){ + try { + val intent = Intent(ACTION_REQUEST_QUERY) + intent.putExtra(EXTRA_ACTIVITY, editActivityClassName) + context.sendBroadcast(intent) + }catch(ex:Throwable){ + log.e(ex,"sendTaskerQueryRequery failed.") + } + } } override fun onReceive(context : Context, broadcast_intent : Intent) { try { when(broadcast_intent.action) { + ACTION_TASKER_QUERY_CONDITION ->{ + resultCode = onTaskerQueryCondition() + return + } + + ACTION_TASKER_ACTION -> { + onTaskerAction(context, broadcast_intent) + return + } + // ダウンロードファイル数の通知やウィジェットのタップ ACTION_NEW_FILE_NOTIFICATION_TAP -> { NewFileWidget.clearDownloadCount(context) @@ -123,4 +314,5 @@ class Receiver1 : BroadcastReceiver() { log.trace(ex, "onReceive failed.") } } + } diff --git a/app/src/main/java/jp/juggler/fadownloader/tracker/NetworkTracker.kt b/app/src/main/java/jp/juggler/fadownloader/tracker/NetworkTracker.kt index 027daf8..29b3cab 100644 --- a/app/src/main/java/jp/juggler/fadownloader/tracker/NetworkTracker.kt +++ b/app/src/main/java/jp/juggler/fadownloader/tracker/NetworkTracker.kt @@ -103,7 +103,7 @@ class NetworkTracker( get() = lastOtherActive.get() private val isDisposed : Boolean - get() = worker != null + get() = worker == null private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) @@ -133,6 +133,7 @@ class NetworkTracker( if(isDisposed) return this.setting = setting + logStatic.d("updateSetting: targetType=${setting.target_type}") timeLastTargetDetected = 0L timeLastSpray = 0L @@ -463,6 +464,7 @@ class NetworkTracker( synchronized(testerMap) { var tester = testerMap[checkUrl] if(tester?.isAlive == true) return + // log.v("${checkUrl}の確認を開始") tester = NetworkTracker.UrlTester(setting, log, targetUrl, checkUrl, onUrlTestComplete) testerMap[checkUrl] = tester tester.start() @@ -813,6 +815,7 @@ class NetworkTracker( ns_list.afterAddAll() lastOtherActive.set(ns_list.other_active) + logStatic.d("checkNetwork:targetType =${setting.target_type}") // ターゲット種別により、テザリング用とAP用の処理に分かれる return when(setting.target_type) { @@ -868,7 +871,7 @@ class NetworkTracker( while(! isCancelled) { - val remain = try { + val remain = try { checkNetwork() } catch(ex : Throwable) { log.trace(ex, "network check failed.") @@ -893,7 +896,8 @@ class NetworkTracker( } } - waitEx(if(remain <= 0L) 5000L else remain) + logStatic.d("run: remain=$remain") + waitEx(if(remain <= 0L) 5000L else if(remain > 10000L )10000L else remain) } } } diff --git a/app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java b/app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java new file mode 100644 index 0000000..c1989ef --- /dev/null +++ b/app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java @@ -0,0 +1,1041 @@ +//package com.yourcompany.yourcondition; +//package com.yourcompany.yoursetting; +package net.dinglisch.android.tasker; + +// 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() + +// v1.7 20160619 +// null check for getCallingActivity() in hostSupportsOnFireVariableReplacement( Activity editActivity ) + +// v1.8 20161002 +// added hostSupportsKeyEncoding(), setKeyEncoding() and Host.getKeysWithEncoding() + +import java.net.URISyntaxException; +import java.security.SecureRandom; +import java.util.regex.Pattern; + +import android.app.Activity; +import android.content.ComponentName; +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.os.ResultReceiver; +import android.util.Log; + +public class TaskerPlugin { + + private final static String TAG = "TaskerPlugin"; + + private final static String BASE_KEY = "net.dinglisch.android.tasker"; + + private final static String EXTRAS_PREFIX = BASE_KEY + ".extras."; + + private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80; + + 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; + + /** + * 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 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&&[^_]]"; + + public 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 static Pattern VARIABLE_NAME_MATCH_PATTERN = null; + + /** + * @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) + */ + 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; + + /** + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16; + + 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 int EXTRA_HOST_CAPABILITY_ENCODING_JSON = 128; + + 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 | + EXTRA_HOST_CAPABILITY_ENCODING_JSON + ; + + /** + * Possible encodings of text in bundle values + * + * @see #setKeyEncoding(Bundle,String[],Encoding) + */ + public enum Encoding { JSON }; + + private final static String BUNDLE_KEY_ENCODING_JSON_KEYS = BASE_KEY + ".JSON_ENCODED_KEYS"; + + public static boolean hostSupportsKeyEncoding( Bundle extrasFromHost, Encoding encoding ) { + switch ( encoding ) { + case JSON: + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_ENCODING_JSON ); + default: + return false; + } + } + + /** + * + * 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"; + + + public static boolean hostSupportsRelevantVariables( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES ); + } + + /** + * Specifies to host which variables might be used by the plugin. + * + * 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; + } + + /** + * 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 + * Null values cause deletion of possible already-existing variables + * A null value where the variable does not already exist results in attempted deletion + * of any existing array indices (%arr1, %arr2 etc) + * + * @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 ); + } + + /** + * Used by: plugin EditActivity + * + * 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 ); + } + + // ----------------------------- SETTING PLUGIN ONLY --------------------------------- // + + 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 #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 #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 #addCompletionIntent(Intent, Intent,ComponentName, boolean) + */ + private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT"; + + /** + * @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 #addCompletionIntent(Intent, Intent,ComponentName, boolean) + */ + public final static String EXTRA_CALL_SERVICE_PACKAGE = BASE_KEY + ".EXTRA_CALL_SERVICE_PACKAGE"; + public final static String EXTRA_CALL_SERVICE = BASE_KEY + ".EXTRA_CALL_SERVICE"; + public final static String EXTRA_CALL_SERVICE_FOREGROUND = BASE_KEY + ".EXTRA_CALL_SERVICE_FOREGROUND"; + /** + * @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; + + /** + * 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 ); + } + + /** + * 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 ) { + + 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 ); + + 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 ); + } + } + } + + 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; + } + } + + // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- // + + public static class Condition { + + /** + * @see #getResultReceiver(Intent) + */ + public final static String EXTRA_RESULT_RECEIVER = BASE_KEY + ".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"); + + } + } + + // ----------------------------- EVENT PLUGIN ONLY --------------------------------- // + + 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 a guaranteed non-repeating within 100 calls message ID + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #retrievePassThroughData(Intent) + * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin + * + */ + 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; + } + } + // ---------------------------------- HOST ----------------------------------------- // + + 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, boolean foreground) { + 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, foreground); + } + 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 ); + } + } + + // ---------------------------------- HELPER FUNCTIONS -------------------------------- // + + 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; + } + + 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++; + } + + 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() ); + } + } + } + + // state tracking for random number sequence + private static int [] lastRandomsSeen = null; + private static int randomInsertPointer = 0; + private static SecureRandom sr = null; + + /** + * 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; + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/act_tasker_setting_action.xml b/app/src/main/res/layout/act_tasker_setting_action.xml new file mode 100644 index 0000000..70ddd97 --- /dev/null +++ b/app/src/main/res/layout/act_tasker_setting_action.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + +