Repository: MarkAdamson/home-assistant-plugin-for-tasker Branch: master Commit: 6df3a10d85ec Files: 66 Total size: 261.5 KB Directory structure: gitextract_wh_8w5zl/ ├── .gitignore ├── HomeAssistantTaskerPlugin/ │ ├── build.gradle │ ├── lint.xml │ ├── proguard-project.txt │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── markadamson/ │ │ └── taskerplugin/ │ │ └── homeassistant/ │ │ ├── bundle/ │ │ │ └── PluginBundleValuesTest.java │ │ ├── receiver/ │ │ │ └── FireReceiverTest.java │ │ ├── setting/ │ │ │ └── toast/ │ │ │ └── test/ │ │ │ ├── InstallLocation.java │ │ │ └── ManifestTest.java │ │ └── ui/ │ │ └── activity/ │ │ └── EditActivityTest.java │ ├── debug/ │ │ └── AndroidManifest.xml │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── markadamson/ │ │ ├── locale/ │ │ │ └── sdk/ │ │ │ └── client/ │ │ │ ├── internal/ │ │ │ │ └── PluginActivityDelegate.java │ │ │ └── ui/ │ │ │ └── activity/ │ │ │ ├── AbstractAppCompatPluginActivity.java │ │ │ └── IPluginActivity.java │ │ └── taskerplugin/ │ │ └── homeassistant/ │ │ ├── Constants.java │ │ ├── PluginApplication.java │ │ ├── TaskerPlugin.java │ │ ├── Utils.java │ │ ├── bundle/ │ │ │ ├── GetStatePluginBundleValues.java │ │ │ ├── PluginBundleValues.java │ │ │ └── RenderTemplatePluginBundleValues.java │ │ ├── model/ │ │ │ ├── HAAPI.java │ │ │ ├── HAAPIException.java │ │ │ ├── HAAPIResult.java │ │ │ ├── HAAPITask.java │ │ │ ├── HAEntity.java │ │ │ ├── HAServer.java │ │ │ └── HAServerStore.java │ │ ├── receiver/ │ │ │ ├── AbstractAsyncReceiver.java │ │ │ ├── AbstractPluginSettingReceiver.java │ │ │ └── FireReceiver.java │ │ ├── service/ │ │ │ └── ActionService.java │ │ └── ui/ │ │ ├── ServerSelectionUI.java │ │ └── activity/ │ │ ├── EditActivity.java │ │ ├── EditGetStateActivity.java │ │ ├── EditRenderTemplateActivity.java │ │ └── EditServerActivity.java │ └── res/ │ ├── drawable/ │ │ ├── baseline_add_24.xml │ │ ├── baseline_cancel_24.xml │ │ ├── baseline_delete_24.xml │ │ ├── baseline_edit_24.xml │ │ └── outline_label_24.xml │ ├── layout/ │ │ ├── edit_get_state.xml │ │ ├── edit_render_template.xml │ │ ├── edit_server.xml │ │ ├── main.xml │ │ └── select_server.xml │ ├── menu/ │ │ └── menu.xml │ └── values/ │ └── strings.xml ├── LICENSE ├── README.md ├── build.gradle ├── circle.yml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── tools/ ├── ci/ │ ├── android-sdk-license │ ├── android-sdk-setup.sh │ ├── firebase-test-lab-test-module.sh │ ├── google-cloud-test-lab-setup.sh │ └── repositories.cfg ├── findbugs/ │ └── android-filter.xml └── gcloud.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ syntax: glob .crashlytics_data/ .DS_Store .gradle/ .settings .idea *.iml *.apk bin/ build/ gen/ local.properties HomeAssistantTaskerPlugin/release/ ================================================ FILE: HomeAssistantTaskerPlugin/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'findbugs' dependencies { implementation group:'net.jcip', name:'jcip-annotations', version:"${JCIP_ANNOTATION_VERSION_MATCHER}" implementation group:'com.android.support', name:'support-annotations', version:"${ANDROID_SUPPORT_VERSION_MATCHER}" implementation group:'com.android.support', name:'appcompat-v7', version:"${ANDROID_SUPPORT_VERSION_MATCHER}" implementation group:'com.twofortyfouram', name:'android-annotation', version:"${TWOFORTYFOURAM_ANNOTATION_VERSION_MATCHER}" implementation group:'com.twofortyfouram', name:'android-assertion', version:"${TWOFORTYFOURAM_ASSERTION_VERSION_MATCHER}" implementation group:'com.twofortyfouram', name:'android-plugin-api-for-locale', version:"${TWOFORTYFOURAM_PLUGIN_API_VERSION_MATCHER}" implementation group:'com.twofortyfouram', name:'android-plugin-client-sdk-for-locale', version:"${TWOFORTYFOURAM_PLUGIN_CLIENT_SDK_VERSION_MATCHER}" implementation group:'com.twofortyfouram', name:'android-spackle', version:"${TWOFORTYFOURAM_SPACKLE_VERSION_MATCHER}" androidTestImplementation group:'com.twofortyfouram', name:'android-test', version:"${TWOFORTYFOURAM_TEST_VERSION_MATCHER}" } android { compileSdkVersion Integer.parseInt(ANDROID_COMPILE_SDK_VERSION) buildToolsVersion ANDROID_BUILD_TOOLS_VERSION defaultConfig { minSdkVersion Integer.parseInt(ANDROID_MIN_SDK_VERSION) targetSdkVersion Integer.parseInt(ANDROID_TARGET_SDK_VERSION) versionCode Integer.parseInt(ANDROID_VERSION_CODE) versionName ANDROID_VERSION_NAME // Keep only the default and English localizations; which reduces app size since // dependencies contain localizations. // The last two are for support of pseudolocales in debug builds. resConfigs 'en', 'en-rUS', 'en_XA', 'ar_XB' vectorDrawables.useSupportLibrary = true } if (RELEASE_KEYSTORE_PATH && RELEASE_KEYSTORE_PASSWORD && RELEASE_KEY_ALIAS && RELEASE_KEY_ALIAS_PASSWORD) { signingConfigs { release { storeFile file(RELEASE_KEYSTORE_PATH) storePassword RELEASE_KEYSTORE_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_KEY_ALIAS_PASSWORD } } } buildTypes { debug { testCoverageEnabled Boolean.parseBoolean(IS_COVERAGE_ENABLED) applicationIdSuffix '.debug' pseudoLocalesEnabled true zipAlignEnabled true } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-project.txt' shrinkResources true if (RELEASE_KEYSTORE_PATH && RELEASE_KEYSTORE_PASSWORD && RELEASE_KEY_ALIAS && RELEASE_KEY_ALIAS_PASSWORD) { signingConfig signingConfigs.release } } } } task findbugs(type: FindBugs) { ignoreFailures = true classes = fileTree('build/intermediates/classes/release/') source = fileTree('src/main/java/') classpath = files() excludeFilter = file("$rootProject.projectDir/tools/findbugs/android-filter.xml") effort = 'max' reportLevel = 'low' reports { // Only one report type can be enabled at a time, apparently. xml.enabled = false html.enabled = true html.destination = "$project.buildDir/outputs/reports/findbugs/findbugs.html" } } ================================================ FILE: HomeAssistantTaskerPlugin/lint.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/proguard-project.txt ================================================ # This improves obfuscation and moves non-public classes to their own namespace. -repackageclasses 'com.markadamson.taskerplugin.homeassistant' # Ensure that stacktraces are reversible. -renamesourcefileattribute SourceFile -keepattributes SourceFile,LineNumberTable ================================================ FILE: HomeAssistantTaskerPlugin/src/androidTest/java/com/markadamson/taskerplugin/homeassistant/bundle/PluginBundleValuesTest.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.bundle; import android.os.Bundle; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import com.twofortyfouram.spackle.AppBuildInfo; /** * Tests {@link PluginBundleValues}. */ public final class PluginBundleValuesTest extends AndroidTestCase { @SmallTest public static void testExtraConstants() { /* * NOTE: This test is expected to fail initially when you are adapting this example to your * own plug-in. Once you've settled on constant names for your Intent extras, go ahead and * update this test case. * * The goal of this test case is to prevent accidental renaming of the Intent extras. If the * extra is intentionally changed, then this unit test needs to be intentionally updated. */ assertEquals( "com.markadamson.taskerplugin.homeassistant.extra.STRING_MESSAGE", PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE); //$NON-NLS-1$ assertEquals( "com.markadamson.taskerplugin.homeassistant.extra.INT_VERSION_CODE", PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE); //$NON-NLS-1$ } @SmallTest public void testGenerateBundle() { final Bundle bundle = PluginBundleValues.generateBundle(getContext(), "Foo"); //$NON-NLS-1$ assertNotNull(bundle); assertEquals(2, bundle.keySet().size()); assertEquals("Foo", bundle.getString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE)); //$NON-NLS-1$ assertEquals(AppBuildInfo.getVersionCode(getContext()), bundle.getInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE)); } @SmallTest public static void testVerifyBundle_correct() { final Bundle bundle = new Bundle(); bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, "I am a toast message!"); //$NON-NLS-1$ bundle.putInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE, 1); assertTrue(PluginBundleValues.isBundleValid(bundle)); } @SmallTest public static void testVerifyBundle_null() { assertFalse(PluginBundleValues.isBundleValid(null)); } @SmallTest public static void testVerifyBundle_missing_extra() { assertFalse(PluginBundleValues.isBundleValid(new Bundle())); } @SmallTest public static void testVerifyBundle_extra_items() { final Bundle bundle = new Bundle(); bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, "I am a toast message!"); //$NON-NLS-1$ bundle.putInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE, 1); bundle.putString("test", "test"); //$NON-NLS-1$//$NON-NLS-2$ assertFalse(PluginBundleValues.isBundleValid(bundle)); } @SmallTest public static void testVerifyBundle_null_message() { final Bundle bundle = new Bundle(); bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, null); bundle.putInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE, 1); assertFalse(PluginBundleValues.isBundleValid(bundle)); } @SmallTest public static void testVerifyBundle_empty_message() { final Bundle bundle = new Bundle(); bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, ""); //$NON-NLS-1$ bundle.putInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE, 1); assertFalse(PluginBundleValues.isBundleValid(bundle)); } @SmallTest public static void testVerifyBundle_wrong_type() { { final Bundle bundle = new Bundle(); bundle.putInt(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, 1); bundle.putInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE, 1); assertFalse(PluginBundleValues.isBundleValid(bundle)); } { final Bundle bundle = new Bundle(); bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, "I am a toast message!"); //$NON-NLS-1$ bundle.putString(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE, "test"); //$NON-NLS-1$ assertFalse(PluginBundleValues.isBundleValid(bundle)); } } } ================================================ FILE: HomeAssistantTaskerPlugin/src/androidTest/java/com/markadamson/taskerplugin/homeassistant/receiver/FireReceiverTest.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.receiver; import com.markadamson.taskerplugin.homeassistant.bundle.PluginBundleValues; import android.content.BroadcastReceiver; import android.content.Intent; import android.os.Bundle; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; /** * Tests the {@link FireReceiver}. */ public final class FireReceiverTest extends AndroidTestCase { /* * These test cases perform sanity checks. These tests are not very extensive and additional * testing is required to verify the BroadcastReceiver works correctly. For example, a human * would need to manually verify that a Toast message appears when a correct Intent is sent to * the receiver. Depending on what your setting implements, you may be able to verify more * easily that the setting triggered the desired result via unit tests than this sample setting * can. */ @SmallTest public void testNullMessage() { final BroadcastReceiver fireReceiver = new FireReceiver(); final Bundle bundle = PluginBundleValues .generateBundle(getContext(), "test_message"); //$NON-NLS-1$ bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, null); /* * The receiver shouldn't crash if the EXTRA_BUNDLE is incorrect */ fireReceiver.onReceive(getContext(), new Intent( com.twofortyfouram.locale.api.Intent.ACTION_FIRE_SETTING).putExtra( com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE, bundle)); } @SmallTest public void testNormal() { final BroadcastReceiver fireReceiver = new FireReceiver(); final Bundle bundle = PluginBundleValues .generateBundle(getContext(), "test_message"); //$NON-NLS-1$ fireReceiver.onReceive(getContext(), new Intent( com.twofortyfouram.locale.api.Intent.ACTION_FIRE_SETTING).putExtra( com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE, bundle)); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/androidTest/java/com/markadamson/taskerplugin/homeassistant/setting/toast/test/InstallLocation.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.setting.toast.test; import com.twofortyfouram.annotation.Slow; import com.twofortyfouram.annotation.Slow.Speed; import com.twofortyfouram.annotation.VisibleForTesting; import com.twofortyfouram.annotation.VisibleForTesting.Visibility; import com.twofortyfouram.assertion.Assertions; import net.jcip.annotations.ThreadSafe; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.XmlResourceParser; import android.support.annotation.NonNull; import java.io.IOException; /** * Represents the Android Manifest's possible states for install location. */ @ThreadSafe public enum InstallLocation { /** * The application permits installation to either internal or external * storage, with Android automatically deciding. */ auto, /** * The application can only be installed to internal storage. */ internalOnly, /** * The application permits installation to either internal or external * storage, with preference for external storage. */ preferExternal, /** * No install location was specified in the Android Manifest. In terms of * how Android interprets this, it is basically the same as * {@link #internalOnly}. */ MISSING, /** * An unknown install location, such as a new install location added in * newer versions of Android. */ UNKNOWN; /** * The Android Manifest int value for auto install location. */ /* * Note: This value is a private API in Android and could change without * warning */ @VisibleForTesting(Visibility.PRIVATE) /* package */ static final int MANIFEST_INSTALL_LOCATION_AUTO = 0; /** * The Android Manifest int value for internal only install location. */ /* * Note: This value is a private API in Android and could change without * warning */ @VisibleForTesting(Visibility.PRIVATE) /* package */ static final int MANIFEST_INSTALL_LOCATION_INTERNAL_ONLY = 1; /** * The Android Manifest int value for internal or external storage, with * preference for external storage. */ /* * Note: This value is a private API in Android and could change without * warning */ @VisibleForTesting(Visibility.PRIVATE) /* package */ static final int MANIFEST_INSTALL_LOCATION_PREFER_EXTERNAL = 2; /** * Takes the integer value of install location from the Android Manifest and * converts it to an enum value. * * @param location one of the Android Manifest install locations. * @return The enum type for the install location. */ @NonNull /* package */ static InstallLocation getInstallLocation(final int location) { switch (location) { case MANIFEST_INSTALL_LOCATION_AUTO: { return auto; } case MANIFEST_INSTALL_LOCATION_INTERNAL_ONLY: { return internalOnly; } case MANIFEST_INSTALL_LOCATION_PREFER_EXTERNAL: { return preferExternal; } default: { return UNKNOWN; } } } /** * Gets a package's install location, as per the Android Manifest. * * @param context Application context. * @param packageName Package whose install location is to be checked. * @return the install location. * @throws NameNotFoundException if {@code packageName} isn't installed. * @throws XmlPullParserException If the target package's manifest couldn't * be parsed. * @throws IOException If an error occurred reading the target package. */ @NonNull @Slow(Speed.MILLISECONDS) public static InstallLocation getManifestInstallLocation(@NonNull final Context context, @NonNull final String packageName) throws NameNotFoundException, XmlPullParserException, IOException { Assertions.assertNotNull(context, "context"); //$NON-NLS-1$ Assertions.assertNotNull(packageName, "packageName"); //$NON-NLS-1$ /* * There isn't a public API to check the installLocation of an APK, so * this is a hacky implementation to read the value directly from the * package's AndroidManifest. */ final XmlResourceParser xml = context .createPackageContext(packageName, Context.CONTEXT_RESTRICTED).getAssets() .openXmlResourceParser("AndroidManifest.xml"); //$NON-NLS-1$ try { for (int eventType = xml.getEventType(); XmlPullParser.END_DOCUMENT != eventType; eventType = xml .nextToken()) { switch (eventType) { case XmlPullParser.START_TAG: { if (xml.getName().matches("manifest")) { //$NON-NLS-1$ for (int x = 0; x < xml.getAttributeCount(); x++) { if (xml.getAttributeName(x) .matches("installLocation")) { //$NON-NLS-1$ return InstallLocation.getInstallLocation(Integer.parseInt(xml .getAttributeValue(x))); } } } break; } } } /* * Once this point is reached, it can be assumed the installLocation * didn't exist in the AndroidManifest */ return InstallLocation.MISSING; } finally { xml.close(); } } } ================================================ FILE: HomeAssistantTaskerPlugin/src/androidTest/java/com/markadamson/taskerplugin/homeassistant/setting/toast/test/ManifestTest.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.setting.toast.test; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.support.annotation.NonNull; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import java.util.LinkedList; import java.util.List; /** * Tests to verify proper entries in the plug-in's Android Manifest. */ public final class ManifestTest extends AndroidTestCase { @SmallTest public void testApplicationEnabled() { assertTrue(getContext().getApplicationInfo().enabled); } @SmallTest public void testPluginActivityPresent() { final List activities = getPluginActivities(getContext()); assertFalse(activities.isEmpty()); for (final ResolveInfo x : activities) { assertTrue(x.activityInfo.enabled); assertTrue(x.activityInfo.exported); /* * Verify that the plug-in doesn't request permissions not available to the host */ assertNull(x.activityInfo.permission); /* * Verify that the plug-in has a label attribute in the AndroidManifest */ assertFalse(0 == x.activityInfo.labelRes); /* * Verify that the plug-in has a icon attribute in the AndroidManifest */ assertFalse(0 == x.activityInfo.icon); } } @SmallTest public void testPluginReceiver() { final List receivers = getPluginReceivers(getContext()); assertEquals(1, receivers.size()); for (final ResolveInfo x : receivers) { assertTrue(x.activityInfo.enabled); assertTrue(x.activityInfo.exported); /* * Verify that the plug-in doesn't request permissions not available to the host */ assertNull(x.activityInfo.permission); } } /** * Verifies the package is configured to be installed to internal memory */ @SmallTest public void testManifestInstallLocation() throws Exception { /* * Note that in addition to this test, Locale will also check that a plug-in is actually on * internal memory at runtime. This primarily affects custom ROMs that permit moving apps to * external memory even if the app specifies internalOnly. */ assertEquals(InstallLocation.internalOnly, InstallLocation.getManifestInstallLocation( getContext(), getContext().getPackageName())); } /** * Gets a list of all Activities in {@code context}'s package that export * {@link com.twofortyfouram.locale.api.Intent#ACTION_EDIT_SETTING}. * * @param context Application context. */ private static List getPluginActivities(@NonNull final Context context) { final String packageName = context.getPackageName(); final List result = new LinkedList(); for (final ResolveInfo x : context.getPackageManager().queryIntentActivities( new Intent(com.twofortyfouram.locale.api.Intent.ACTION_EDIT_SETTING), 0)) { if (packageName.equals(x.activityInfo.packageName)) { result.add(x); } } return result; } /** * Gets a list of all BroadcastReceivers in {@code context}'s package that export * {@link com.twofortyfouram.locale.api.Intent#ACTION_FIRE_SETTING ACTION_FIRE_SETTING}. * * @param context Application context. */ private static List getPluginReceivers(@NonNull final Context context) { final String packageName = context.getPackageName(); final List result = new LinkedList(); for (final ResolveInfo x : context.getPackageManager().queryBroadcastReceivers( new Intent(com.twofortyfouram.locale.api.Intent.ACTION_FIRE_SETTING), 0)) { if (packageName.equals(x.activityInfo.packageName)) { result.add(x); } } return result; } } ================================================ FILE: HomeAssistantTaskerPlugin/src/androidTest/java/com/markadamson/taskerplugin/homeassistant/ui/activity/EditActivityTest.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.ui.activity; import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Looper; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.test.suitebuilder.annotation.MediumTest; import android.text.TextUtils; import android.widget.EditText; import com.markadamson.taskerplugin.homeassistant.bundle.PluginBundleValues; import com.markadamson.taskerplugin.homeassistant.R; import com.twofortyfouram.test.ui.activity.ActivityTestUtil; /** * Tests the {@link EditActivity}. */ public final class EditActivityTest extends ActivityInstrumentationTestCase2 { /** * Context of the target application. This is initialized in {@link #setUp()}. */ private Context mTargetContext; /** * Instrumentation for the test. This is initialized in {@link #setUp()}. */ private Instrumentation mInstrumentation; /** * Constructor for the test class; required by Android. */ public EditActivityTest() { super(EditActivity.class); } /** * Setup that executes before every test case */ @Override protected void setUp() throws Exception { super.setUp(); mInstrumentation = getInstrumentation(); mTargetContext = mInstrumentation.getTargetContext(); /* * Perform test case specific initialization. This is required to be set up here because * setActivityIntent has no effect inside a method annotated with @UiThreadTest */ if ("testNewSettingCancel".equals(getName())) { //$NON-NLS-1$ setActivityIntent(new Intent(com.twofortyfouram.locale.api.Intent.ACTION_EDIT_SETTING) .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation")); //$NON-NLS-1$ } else if ("testNewSettingSave".equals(getName())) { //$NON-NLS-1$ setActivityIntent(new Intent(com.twofortyfouram.locale.api.Intent.ACTION_EDIT_SETTING) .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation")); //$NON-NLS-1$ } else if ("testOldSetting".equals(getName())) { //$NON-NLS-1$ final Bundle bundle = PluginBundleValues.generateBundle(mTargetContext, "I am a toast message!"); //$NON-NLS-1$ setActivityIntent(new Intent(com.twofortyfouram.locale.api.Intent.ACTION_EDIT_SETTING) .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation") .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BLURB, "I am a toast message!") .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE, bundle)); //$NON-NLS-1$ } else if ("testBadBundle".equals(getName())) { //$NON-NLS-1$ final Bundle bundle = PluginBundleValues.generateBundle(mTargetContext, "I am a toast message!"); //$NON-NLS-1$ bundle.putString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE, null); setActivityIntent(new Intent(com.twofortyfouram.locale.api.Intent.ACTION_EDIT_SETTING) .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation") .putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE, bundle)); //$NON-NLS-1$ } } @MediumTest @UiThreadTest public void testNewSettingCancel() throws Throwable { final Activity activity = getActivity(); assertMessageAutoSync(""); //$NON-NLS-1$ assertHintAutoSync(mTargetContext.getString(R.string.message_hint)); activity.finish(); assertEquals(Activity.RESULT_CANCELED, ActivityTestUtil.getActivityResultCode(activity)); } @MediumTest @UiThreadTest public void testNewSettingSave() throws Throwable { final Activity activity = getActivity(); assertMessageAutoSync(""); //$NON-NLS-1$ assertHintAutoSync(mTargetContext.getString(R.string.message_hint)); setMessageAutoSync(getName()); activity.finish(); assertActivityResultAutoSync(getName()); } @MediumTest @UiThreadTest public void testOldSetting() throws Throwable { final Activity activity = getActivity(); /* * It is necessary to call this manually; the test case won't call * onPostCreate() for us :-( */ getActivity().onPostCreateWithPreviousResult( getActivity().getIntent().getBundleExtra( com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE), getActivity().getIntent().getStringExtra( com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BLURB)); assertMessageAutoSync("I am a toast message!"); //$NON-NLS-1$ activity.finish(); assertEquals(Activity.RESULT_CANCELED, ActivityTestUtil.getActivityResultCode (activity)); } /** * Verifies the Activity properly handles a bundle with a bad value embedded in it. */ @MediumTest @UiThreadTest public void testBadBundle() throws Throwable { final Activity activity = getActivity(); assertMessageAutoSync(""); //$NON-NLS-1$ assertHintAutoSync(mTargetContext.getString(R.string.message_hint)); activity.finish(); assertEquals(Activity.RESULT_CANCELED, ActivityTestUtil.getActivityResultCode (activity)); } /** * Asserts the Activity result contains the expected values for the given display state. * * @param message The message the plug-in is supposed to show. */ private void assertActivityResultAutoSync(final String message) throws Throwable { final Activity activity = getActivity(); final Runnable runnable = new Runnable() { public void run() { activity.finish(); assertEquals(Activity.RESULT_OK, ActivityTestUtil.getActivityResultCode(activity)); final Intent result = ActivityTestUtil.getActivityResultData(activity); assertNotNull(result); final Bundle extras = result.getExtras(); assertNotNull(extras); assertEquals( String.format( "Extras should only contain %s and %s but actually contain %s", com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE, com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BLURB, extras.keySet()), 2, extras.keySet() //$NON-NLS-1$ .size()); assertFalse(TextUtils.isEmpty(extras .getString(com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BLURB))); final Bundle pluginBundle = extras .getBundle(com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE); assertNotNull(pluginBundle); assertTrue(PluginBundleValues.isBundleValid(pluginBundle)); assertEquals(message, pluginBundle.getString(PluginBundleValues.BUNDLE_EXTRA_STRING_MESSAGE)); } }; autoSyncRunnable(runnable); } /** * Asserts provided message is what the UI shows. * * @param message Message to assert equals the EditText. */ private void assertMessageAutoSync(final String message) throws Throwable { final Runnable runnable = new Runnable() { private final Activity mActivity = getActivity(); public void run() { assertEquals(message, ((EditText) mActivity.findViewById(android.R.id.text1)) .getText().toString()); } }; autoSyncRunnable(runnable); } /** * Asserts provided hint is what the UI shows. * * @param hint Hint to assert equals the EditText. */ private void assertHintAutoSync(final String hint) throws Throwable { final Runnable runnable = new Runnable() { private final Activity mActivity = getActivity(); public void run() { assertEquals(hint, ((EditText) mActivity.findViewById(android.R.id.text1)) .getHint()); } }; autoSyncRunnable(runnable); } /** * Sets the message. * * @param message The message to set. */ private void setMessageAutoSync(final String message) throws Throwable { final Runnable runnable = new Runnable() { private final Activity mActivity = getActivity(); public void run() { final EditText editText = (EditText) mActivity.findViewById(android.R.id.text1); editText.setText(message); } }; autoSyncRunnable(runnable); } /** * Executes a runnable on the main thread. This method works even if the current thread is * already the main thread. * * @param runnable to execute. */ protected final void autoSyncRunnable(final Runnable runnable) { //noinspection ObjectEquality if (Looper.getMainLooper() == Looper.myLooper()) { runnable.run(); } else { getInstrumentation().runOnMainSync(runnable); getInstrumentation().waitForIdleSync(); } } } ================================================ FILE: HomeAssistantTaskerPlugin/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/AndroidManifest.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/locale/sdk/client/internal/PluginActivityDelegate.java ================================================ package com.markadamson.locale.sdk.client.internal; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.twofortyfouram.assertion.BundleAssertions; import com.markadamson.locale.sdk.client.ui.activity.IPluginActivity; import com.twofortyfouram.log.Lumberjack; import com.twofortyfouram.spackle.bundle.BundleComparer; import com.twofortyfouram.spackle.bundle.BundleScrubber; import net.jcip.annotations.Immutable; import static com.twofortyfouram.assertion.Assertions.assertNotNull; /** * Activities that implement the {@link IPluginActivity} interface can delegate much of their * responsibility to this class. * * @param Plug-in activity. */ /* * This class is intended to make the implementation of various plug-in Activities DRY. * * This class has no state, so therefore is immutable. */ @Immutable public final class PluginActivityDelegate { /** * @param intent Intent to check. * @return True if intent is a Locale plug-in edit Intent. */ public static boolean isLocalePluginIntent(@NonNull final Intent intent) { assertNotNull(intent, "intent"); //$NON-NLS-1$ final String action = intent.getAction(); return com.twofortyfouram.locale.api.Intent.ACTION_EDIT_CONDITION.equals(action) || com.twofortyfouram.locale.api.Intent.ACTION_EDIT_SETTING.equals(action); } public void onCreate(@NonNull final T activity, @Nullable final Bundle savedInstanceState) { assertNotNull(activity, "activity"); //$NON-NLS-1$ final Intent intent = activity.getIntent(); if (isLocalePluginIntent(intent)) { if (BundleScrubber.scrub(intent)) { return; } final Bundle previousBundle = activity.getPreviousBundle(); if (BundleScrubber.scrub(previousBundle)) { return; } Lumberjack .v("Creating Activity with Intent=%s, savedInstanceState=%s, EXTRA_BUNDLE=%s", intent, savedInstanceState, previousBundle); //$NON-NLS-1$ } } public void onPostCreate(@NonNull final T activity, @Nullable final Bundle savedInstanceState) { assertNotNull(activity, "activity"); //$NON-NLS-1$ if (PluginActivityDelegate.isLocalePluginIntent(activity.getIntent())) { if (null == savedInstanceState) { final Bundle previousBundle = activity.getPreviousBundle(); final String previousBlurb = activity.getPreviousBlurb(); if (null != previousBundle && null != previousBlurb) { activity.onPostCreateWithPreviousResult(previousBundle, previousBlurb); } } } } public void finish(@NonNull final T activity, final boolean isCancelled) { if (PluginActivityDelegate.isLocalePluginIntent(activity.getIntent())) { if (!isCancelled) { final Bundle resultBundle = activity.getResultBundle(); if (null != resultBundle) { BundleAssertions.assertSerializable(resultBundle); final String blurb = activity.getResultBlurb(resultBundle); assertNotNull(blurb, "blurb"); //$NON-NLS-1$ if (!BundleComparer.areBundlesEqual(resultBundle, activity.getPreviousBundle()) || !blurb.equals(activity.getPreviousBlurb())) { final Intent resultIntent = new Intent(); resultIntent.putExtra(com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE, resultBundle); resultIntent.putExtra( com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BLURB, blurb); TaskerPlugin.addRelevantVariableList(resultIntent, activity.getRelevantVariableList()); TaskerPlugin.Setting.requestTimeoutMS(resultIntent, activity.requestedTimeoutMS()); activity.setResult(Activity.RESULT_OK, resultIntent); } } } } } @Nullable public final String getPreviousBlurb(@NonNull final T activity) { final String blurb = activity.getIntent().getStringExtra( com.twofortyfouram.locale.api.Intent.EXTRA_STRING_BLURB); return blurb; } @Nullable public Bundle getPreviousBundle(@NonNull final T activity) { assertNotNull(activity, "activity"); //$NON-NLS-1$ final Bundle bundle = activity.getIntent().getBundleExtra( com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE); if (null != bundle) { if (activity.isBundleValid(bundle)) { return bundle; } } return null; } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/locale/sdk/client/ui/activity/AbstractAppCompatPluginActivity.java ================================================ /* * android-plugin-client-sdk-for-locale https://github.com/twofortyfouram/android-plugin-client-sdk-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.locale.sdk.client.ui.activity; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import com.markadamson.locale.sdk.client.internal.PluginActivityDelegate; /** *

NOTE: This class is for compatibility with Material Design via the appcompat-v7 library. To use this * class, appcompat-v7 must be on the application's build path. Typically, this would involve adding * appcompat-v7 to the dependencies section of the application's build.gradle script. For example, * the dependency might look something like this * {@code compile group:'com.android.support', name:'appcompat-v7', version:'[21,)'}

*

* Implements the basic behaviors of a "Edit" activity for a * plug-in, handling the Intent protocol for storing and retrieving the plug-in's data. * Recall that a plug-in Activity more or less saves a Bundle and a String blurb via the Intent * extras {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} and {@link * com.twofortyfouram.locale.api.Intent#EXTRA_STRING_BLURB EXTRA_STRING_BLURB}. * Those extras represent the configured plug-in, so this Activity helps plug-ins store and * retrieve * those * extras while abstracting the actual Intent protocol. *

*

* The Activity can be started in one of two states: *

    *
  • New plug-in instance: The Activity's Intent will not contain * {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE}.
  • *
  • Old plug-in instance: The Activity's Intent will contain * {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} and {@link * com.twofortyfouram.locale.api.Intent#EXTRA_STRING_BLURB EXTRA_STRING_BLURB} from a previously * saved plug-in instance that the user is editing. The previously saved Bundle * and blurb can be retrieved at any time via {@link #getPreviousBundle()} and * {@link #getPreviousBlurb()}. These will also be delivered via * {@link #onPostCreateWithPreviousResult(android.os.Bundle, String)} during the * Activity's {@link #onPostCreate(android.os.Bundle)} phase when the Activity is first * created.
  • *
*

During * the Activity's {@link #finish()} lifecycle callback, this class will call {@link * #getResultBundle()} and {@link #getResultBlurb(android.os.Bundle)}, which should return the * Bundle and blurb data the Activity would like to save back to the host. *

*

* Note that all of these behaviors only apply if the Intent * starting the Activity is one of the plug-in "edit" Intent actions. *

* * @see com.twofortyfouram.locale.api.Intent#ACTION_EDIT_CONDITION ACTION_EDIT_CONDITION * @see com.twofortyfouram.locale.api.Intent#ACTION_EDIT_SETTING ACTION_EDIT_SETTING */ public abstract class AbstractAppCompatPluginActivity extends AppCompatActivity implements IPluginActivity { /** * Flag boolean that can be set prior to calling {@link #finish()} to control whether the * Activity * attempts to save a result back to the host. Typically this is only set to true after an * explicit user interaction to abort editing the plug-in, such as tapping a "cancel" button. */ /* * There is no need to save/restore this field's state. */ protected boolean mIsCancelled = false; @NonNull private final PluginActivityDelegate mPluginActivityDelegate = new PluginActivityDelegate<>(); @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPluginActivityDelegate.onCreate(this, savedInstanceState); } @Override public void onPostCreate(@Nullable final Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); mPluginActivityDelegate.onPostCreate(this, savedInstanceState); } @Override public void finish() { mPluginActivityDelegate.finish(this, mIsCancelled); /* * Super call must come after the Activity result is set. If it comes * first, then the Activity result will be lost. */ super.finish(); } /** * @return The {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} that was * previously saved to the host and subsequently passed back to this Activity for further * editing. Internally, this method relies on {@link #isBundleValid(android.os.Bundle)}. If * the bundle exists but is not valid, this method will return null. */ @Nullable public final Bundle getPreviousBundle() { return mPluginActivityDelegate.getPreviousBundle(this); } /** * @return The {@link com.twofortyfouram.locale.api.Intent#EXTRA_STRING_BLURB * EXTRA_STRING_BLURB} that was * previously saved to the host and subsequently passed back to this Activity for further * editing. */ @Nullable public final String getPreviousBlurb() { return mPluginActivityDelegate.getPreviousBlurb(this); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/locale/sdk/client/ui/activity/IPluginActivity.java ================================================ package com.markadamson.locale.sdk.client.ui.activity; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; /** * Common interface for plug-in Activities. */ public interface IPluginActivity { /** * @return The {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} that was * previously saved to the host and subsequently passed back to this Activity for further * editing. Internally, this method relies on {@link #isBundleValid(android.os.Bundle)}. If * the bundle exists but is not valid, this method will return null. */ @Nullable Bundle getPreviousBundle(); /** * @return The {@link com.twofortyfouram.locale.api.Intent#EXTRA_STRING_BLURB * EXTRA_STRING_BLURB} that was * previously saved to the host and subsequently passed back to this Activity for further * editing. */ @Nullable String getPreviousBlurb(); /** *

Validates the Bundle, to ensure that a malicious application isn't attempting to pass * an invalid Bundle.

* * @param bundle The plug-in's Bundle previously returned by the edit * Activity. {@code bundle} should not be mutated by this method. * @return true if {@code bundle} is valid for the plug-in. */ boolean isBundleValid(@NonNull final Bundle bundle); /** * Plug-in Activity lifecycle callback to allow the Activity to restore * state for editing a previously saved plug-in instance. This callback will * occur during the onPostCreate() phase of the Activity lifecycle. *

{@code bundle} will have been * validated by {@link #isBundleValid(android.os.Bundle)} prior to this * method being called. If {@link #isBundleValid(android.os.Bundle)} returned false, then this * method will not be called. This helps ensure that plug-in Activity subclasses only have to * worry about bundle validation once, in the {@link #isBundleValid(android.os.Bundle)} * method.

*

Note this callback only occurs the first time the Activity is created, so it will not be * called * when the Activity is recreated (e.g. {@code savedInstanceState != null}) such as after a * configuration change like a screen rotation.

* * @param previousBundle Previous bundle that the Activity saved. * @param previousBlurb Previous blurb that the Activity saved */ void onPostCreateWithPreviousResult( @NonNull final Bundle previousBundle, @NonNull final String previousBlurb); /** * @return Bundle for the plug-in or {@code null} if a valid Bundle cannot * be generated. */ @Nullable Bundle getResultBundle(); /** * @param bundle Valid bundle for the component. * @return Blurb for {@code bundle}. */ @NonNull String getResultBlurb(@NonNull final Bundle bundle); /** * @return Relevant variables. */ @NonNull String[] getRelevantVariableList(); int requestedTimeoutMS(); } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/Constants.java ================================================ package com.markadamson.taskerplugin.homeassistant; import android.support.annotation.NonNull; public class Constants { @NonNull public static final String BUNDLE_EXTRA_BUNDLE_TYPE = "com.markadamson.taskerplugin.homeassistant.extra.BUNDLE_TYPE"; //$NON-NLS-1$ public static final int BUNDLE_CALL_SERVICE = 0; public static final int BUNDLE_GET_STATE = 1; public static final int BUNDLE_RENDER_TEMPLATE = 2; } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/PluginApplication.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant; import android.app.Application; import com.twofortyfouram.log.Lumberjack; /** * Implements an application object for the plug-in. */ /* * This application is non-essential for the plug-in's operation; it simply enables debugging * options globally for the app. */ public final class PluginApplication extends Application { @Override public void onCreate() { super.onCreate(); Lumberjack.init(getApplicationContext()); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/TaskerPlugin.java ================================================ //package com.yourcompany.yourcondition; //package com.yourcompany.yoursetting; package com.markadamson.taskerplugin.homeassistant; // 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" ); } public static boolean hasVariableReplaceKeys(Bundle resultBundleToHost) { return resultBundleToHost.containsKey(BUNDLE_KEY_VARIABLE_REPLACE_STRINGS); } /** * 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; } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/Utils.java ================================================ package com.markadamson.taskerplugin.homeassistant; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Build; import android.os.PowerManager; import android.provider.Settings; import android.support.v7.app.AlertDialog; import android.view.View; import android.widget.ArrayAdapter; import android.widget.EditText; public class Utils { public static void initVariableSelectUI(String[] variables, View button, final EditText destination) { final ArrayAdapter adapter = new ArrayAdapter<>(button.getContext(), android.R.layout.select_dialog_item, variables); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder builderSingle = new AlertDialog.Builder(v.getContext()); builderSingle.setTitle("Variable Select"); builderSingle.setNegativeButton("cancel", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builderSingle.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String variable = adapter.getItem(which); assert variable != null; int start = Math.max(destination.getSelectionStart(), 0); int end = Math.max(destination.getSelectionEnd(), 0); destination.getText().replace(Math.min(start, end), Math.max(start, end), variable, 0, variable.length()); } }); builderSingle.show(); } }); } public static void checkBatteryOptimisation(final Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { String packageName = activity.getPackageName(); PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE); if (!pm.isIgnoringBatteryOptimizations(packageName)) { new android.app.AlertDialog.Builder(activity) .setTitle(R.string.disable_battery_optimization) .setMessage(R.string.battery_optimization_dialog) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { activity.startActivity(new Intent() .setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)); } }) .setNegativeButton(android.R.string.no, null) .show(); } } } private Utils() { throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/bundle/GetStatePluginBundleValues.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.bundle; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.markadamson.taskerplugin.homeassistant.Constants; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.twofortyfouram.assertion.BundleAssertions; import com.twofortyfouram.log.Lumberjack; import com.twofortyfouram.spackle.AppBuildInfo; import net.jcip.annotations.ThreadSafe; import java.util.UUID; import static com.twofortyfouram.assertion.Assertions.assertNotEmpty; import static com.twofortyfouram.assertion.Assertions.assertNotNull; /** * Manages the {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} for this * plug-in. */ @ThreadSafe public final class GetStatePluginBundleValues { /** * Type: {@code String}. *

* Server UUID as string. */ @NonNull public static final String BUNDLE_EXTRA_STRING_SERVER = "com.markadamson.taskerplugin.homeassistant.extra.STRING_SERVER"; //$NON-NLS-1$ /** * Type: {@code String}. *

* Domain/service to call. */ @NonNull public static final String BUNDLE_EXTRA_STRING_ENTITY = "com.markadamson.taskerplugin.homeassistant.extra.STRING_ENTITY"; //$NON-NLS-1$ /** * Type: {@code String}. *

* State output variable. */ @NonNull public static final String BUNDLE_EXTRA_STRING_VARIABLE = "com.markadamson.taskerplugin.homeassistant.extra.STRING_VARIABLE"; //$NON-NLS-1$ /** * Type: {@code String}. *

* Attributes output variable. */ @NonNull public static final String BUNDLE_EXTRA_STRING_ATTRS_VARIABLE = "com.markadamson.taskerplugin.homeassistant.extra.STRING_ATTRS_VARIABLE"; //$NON-NLS-1$ /** * Type: {@code int}. *

* versionCode of the plug-in that saved the Bundle. */ /* * This extra is not strictly required, however it makes backward and forward compatibility * significantly easier. For example, suppose a bug is found in how some version of the plug-in * stored its Bundle. By having the version, the plug-in can better detect when such bugs occur. */ @NonNull public static final String BUNDLE_EXTRA_INT_VERSION_CODE = "com.markadamson.taskerplugin.homeassistant.extra.INT_VERSION_CODE"; //$NON-NLS-1$ /** * Method to verify the content of the bundle are correct. *

* This method will not mutate {@code bundle}. * * @param bundle bundle to verify. May be null, which will always return false. * @return true if the Bundle is valid, false if the bundle is invalid. */ public static boolean isBundleValid(@Nullable final Bundle bundle) { if (null == bundle) { return false; } try { BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_SERVER, false, false); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_ENTITY, false, false); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_VARIABLE, false, true); BundleAssertions.assertHasInt(bundle, BUNDLE_EXTRA_INT_VERSION_CODE); BundleAssertions.assertHasInt(bundle, Constants.BUNDLE_EXTRA_BUNDLE_TYPE, Constants.BUNDLE_GET_STATE, Constants.BUNDLE_GET_STATE); int bundleVer = bundle.getInt(BUNDLE_EXTRA_INT_VERSION_CODE), expectedCount = 5; // Bundle may now have replacement vars key: if (bundleVer >= 5 && TaskerPlugin.Setting.hasVariableReplaceKeys(bundle)) expectedCount++; if (bundleVer >= 6) { BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_ATTRS_VARIABLE, false, true); expectedCount++; } BundleAssertions.assertKeyCount(bundle, expectedCount); } catch (final AssertionError e) { Lumberjack.e("Bundle failed verification%s", e); //$NON-NLS-1$ return false; } return true; } /** * @param context Application context. * @param server The server UUID. * @param entity The domain/service to call. * @param stateVariable The state output variable. * @param attrsVariable The attributes output variable. * @return A plug-in bundle. */ @NonNull public static Bundle generateBundle(@NonNull final Context context, @NonNull final UUID server, @NonNull final String entity, @NonNull final String stateVariable, @NonNull final String attrsVariable) { assertNotNull(context, "context"); //$NON-NLS-1$ assertNotNull(server, "server"); //$NON-NLS-1$ assertNotEmpty(entity, "service"); //$NON-NLS-1$ final Bundle result = new Bundle(); result.putInt(BUNDLE_EXTRA_INT_VERSION_CODE, AppBuildInfo.getVersionCode(context)); result.putInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE, Constants.BUNDLE_GET_STATE); result.putString(BUNDLE_EXTRA_STRING_SERVER, server.toString()); result.putString(BUNDLE_EXTRA_STRING_ENTITY, entity); result.putString(BUNDLE_EXTRA_STRING_VARIABLE, stateVariable); result.putString(BUNDLE_EXTRA_STRING_ATTRS_VARIABLE, attrsVariable); TaskerPlugin.Setting.setVariableReplaceKeys(result, new String[] {BUNDLE_EXTRA_STRING_ENTITY}); return result; } /** * @param bundle A valid plug-in bundle. * @return The message inside the plug-in bundle. */ @NonNull public static UUID getServer(@NonNull final Bundle bundle) { return UUID.fromString(bundle.getString(BUNDLE_EXTRA_STRING_SERVER)); } @NonNull public static String getEntity(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_ENTITY); } @NonNull public static String getStateVariable(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_VARIABLE); } @NonNull public static String getAttrsVariable(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_ATTRS_VARIABLE, ""); } /** * Private constructor prevents instantiation * * @throws UnsupportedOperationException because this class cannot be instantiated. */ private GetStatePluginBundleValues() { throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/bundle/PluginBundleValues.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.bundle; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.markadamson.taskerplugin.homeassistant.Constants; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.twofortyfouram.assertion.BundleAssertions; import com.twofortyfouram.log.Lumberjack; import com.twofortyfouram.spackle.AppBuildInfo; import net.jcip.annotations.ThreadSafe; import java.util.UUID; import static com.twofortyfouram.assertion.Assertions.assertNotEmpty; import static com.twofortyfouram.assertion.Assertions.assertNotNull; /** * Manages the {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} for this * plug-in. */ @ThreadSafe public final class PluginBundleValues { /** * Type: {@code String}. *

* Server UUID as string. */ @NonNull public static final String BUNDLE_EXTRA_STRING_SERVER = "com.markadamson.taskerplugin.homeassistant.extra.STRING_SERVER"; //$NON-NLS-1$ /** * Type: {@code String}. *

* Domain/service to call. */ @NonNull public static final String BUNDLE_EXTRA_STRING_SERVICE = "com.markadamson.taskerplugin.homeassistant.extra.STRING_SERVICE"; //$NON-NLS-1$ /** * Type: {@code String}. *

* Service data (JSON, optional). */ @NonNull public static final String BUNDLE_EXTRA_STRING_DATA = "com.markadamson.taskerplugin.homeassistant.extra.STRING_DATA"; //$NON-NLS-1$ /** * Type: {@code int}. *

* versionCode of the plug-in that saved the Bundle. */ /* * This extra is not strictly required, however it makes backward and forward compatibility * significantly easier. For example, suppose a bug is found in how some version of the plug-in * stored its Bundle. By having the version, the plug-in can better detect when such bugs occur. */ @NonNull public static final String BUNDLE_EXTRA_INT_VERSION_CODE = "com.markadamson.taskerplugin.homeassistant.extra.INT_VERSION_CODE"; //$NON-NLS-1$ /** * Method to verify the content of the bundle are correct. *

* This method will not mutate {@code bundle}. * * @param bundle bundle to verify. May be null, which will always return false. * @return true if the Bundle is valid, false if the bundle is invalid. */ public static boolean isBundleValid(@Nullable final Bundle bundle) { if (null == bundle) { return false; } try { BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_SERVER, false, false); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_SERVICE, false, false); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_DATA, false, true); BundleAssertions.assertHasInt(bundle, BUNDLE_EXTRA_INT_VERSION_CODE); int bundleVer = bundle.getInt(BUNDLE_EXTRA_INT_VERSION_CODE), expectedCount = 4; if (bundleVer >= 3) { BundleAssertions.assertHasInt(bundle, Constants.BUNDLE_EXTRA_BUNDLE_TYPE, Constants.BUNDLE_CALL_SERVICE, Constants.BUNDLE_CALL_SERVICE); expectedCount++; } // Bundle may now have replacement vars key: if (bundleVer >= 5 && TaskerPlugin.Setting.hasVariableReplaceKeys(bundle)) expectedCount++; BundleAssertions.assertKeyCount(bundle, expectedCount); } catch (final AssertionError e) { Lumberjack.e("Bundle failed verification%s", e); //$NON-NLS-1$ return false; } return true; } /** * @param context Application context. * @param server The server UUID. * @param service The domain/service to call. * @param data The service data to send. * @return A plug-in bundle. */ @NonNull public static Bundle generateBundle(@NonNull final Context context, @NonNull final UUID server, @NonNull final String service, @NonNull final String data) { assertNotNull(context, "context"); //$NON-NLS-1$ assertNotNull(server, "server"); //$NON-NLS-1$ assertNotEmpty(service, "service"); //$NON-NLS-1$ final Bundle result = new Bundle(); result.putInt(BUNDLE_EXTRA_INT_VERSION_CODE, AppBuildInfo.getVersionCode(context)); result.putInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE, Constants.BUNDLE_CALL_SERVICE); result.putString(BUNDLE_EXTRA_STRING_SERVER, server.toString()); result.putString(BUNDLE_EXTRA_STRING_SERVICE, service); result.putString(BUNDLE_EXTRA_STRING_DATA, data); TaskerPlugin.Setting.setVariableReplaceKeys(result, new String[] {BUNDLE_EXTRA_STRING_SERVICE, BUNDLE_EXTRA_STRING_DATA}); return result; } /** * @param bundle A valid plug-in bundle. * @return The message inside the plug-in bundle. */ @NonNull public static UUID getServer(@NonNull final Bundle bundle) { return UUID.fromString(bundle.getString(BUNDLE_EXTRA_STRING_SERVER)); } @NonNull public static String getService(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_SERVICE); } @NonNull public static String getData(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_DATA); } /** * Private constructor prevents instantiation * * @throws UnsupportedOperationException because this class cannot be instantiated. */ private PluginBundleValues() { throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/bundle/RenderTemplatePluginBundleValues.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.bundle; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.markadamson.taskerplugin.homeassistant.Constants; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.twofortyfouram.assertion.BundleAssertions; import com.twofortyfouram.log.Lumberjack; import com.twofortyfouram.spackle.AppBuildInfo; import net.jcip.annotations.ThreadSafe; import java.util.UUID; import static com.twofortyfouram.assertion.Assertions.assertNotEmpty; import static com.twofortyfouram.assertion.Assertions.assertNotNull; /** * Manages the {@link com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} for this * plug-in. */ @ThreadSafe public final class RenderTemplatePluginBundleValues { /** * Type: {@code String}. *

* Server UUID as string. */ @NonNull public static final String BUNDLE_EXTRA_STRING_SERVER = "com.markadamson.taskerplugin.homeassistant.extra.STRING_SERVER"; //$NON-NLS-1$ /** * Type: {@code String}. *

* Template to render. */ @NonNull public static final String BUNDLE_EXTRA_STRING_TEMPLATE = "com.markadamson.taskerplugin.homeassistant.extra.STRING_TEMPLATE"; //$NON-NLS-1$ /** * Type: {@code String}. *

* Variable to store result in. */ @NonNull public static final String BUNDLE_EXTRA_STRING_VARIABLE = "com.markadamson.taskerplugin.homeassistant.extra.STRING_VARIABLE"; //$NON-NLS-1$ /** * Type: {@code int}. *

* versionCode of the plug-in that saved the Bundle. */ /* * This extra is not strictly required, however it makes backward and forward compatibility * significantly easier. For example, suppose a bug is found in how some version of the plug-in * stored its Bundle. By having the version, the plug-in can better detect when such bugs occur. */ @NonNull public static final String BUNDLE_EXTRA_INT_VERSION_CODE = "com.markadamson.taskerplugin.homeassistant.extra.INT_VERSION_CODE"; //$NON-NLS-1$ /** * Method to verify the content of the bundle are correct. *

* This method will not mutate {@code bundle}. * * @param bundle bundle to verify. May be null, which will always return false. * @return true if the Bundle is valid, false if the bundle is invalid. */ public static boolean isBundleValid(@Nullable final Bundle bundle) { if (null == bundle) { return false; } try { BundleAssertions.assertHasInt(bundle, Constants.BUNDLE_EXTRA_BUNDLE_TYPE, Constants.BUNDLE_RENDER_TEMPLATE, Constants.BUNDLE_RENDER_TEMPLATE); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_SERVER, false, false); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_TEMPLATE, false, true); BundleAssertions.assertHasString(bundle, BUNDLE_EXTRA_STRING_VARIABLE, false, false); BundleAssertions.assertHasInt(bundle, BUNDLE_EXTRA_INT_VERSION_CODE); int bundleVer = bundle.getInt(BUNDLE_EXTRA_INT_VERSION_CODE), expectedCount = 5; // Bundle may now have replacement vars key: if (TaskerPlugin.Setting.hasVariableReplaceKeys(bundle)) expectedCount++; BundleAssertions.assertKeyCount(bundle, expectedCount); } catch (final AssertionError e) { Lumberjack.e("Bundle failed verification%s", e); //$NON-NLS-1$ return false; } return true; } /** * @param context Application context. * @param server The server UUID. * @param template The domain/template to call. * @param variable The template variable to send. * @return A plug-in bundle. */ @NonNull public static Bundle generateBundle(@NonNull final Context context, @NonNull final UUID server, @NonNull final String template, @NonNull final String variable) { assertNotNull(context, "context"); //$NON-NLS-1$ assertNotNull(server, "server"); //$NON-NLS-1$ assertNotNull(template, "template"); //$NON-NLS-1$ assertNotNull(variable, "variable"); //$NON-NLS-1$ assertNotEmpty(variable, "variable"); //$NON-NLS-1$ final Bundle result = new Bundle(); result.putInt(BUNDLE_EXTRA_INT_VERSION_CODE, AppBuildInfo.getVersionCode(context)); result.putInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE, Constants.BUNDLE_RENDER_TEMPLATE); result.putString(BUNDLE_EXTRA_STRING_SERVER, server.toString()); result.putString(BUNDLE_EXTRA_STRING_TEMPLATE, template); result.putString(BUNDLE_EXTRA_STRING_VARIABLE, variable); TaskerPlugin.Setting.setVariableReplaceKeys(result, new String[] {BUNDLE_EXTRA_STRING_TEMPLATE}); return result; } /** * @param bundle A valid plug-in bundle. * @return The message inside the plug-in bundle. */ @NonNull public static UUID getServer(@NonNull final Bundle bundle) { return UUID.fromString(bundle.getString(BUNDLE_EXTRA_STRING_SERVER)); } @NonNull public static String getTemplate(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_TEMPLATE); } @NonNull public static String getVariable(@NonNull final Bundle bundle) { return bundle.getString(BUNDLE_EXTRA_STRING_VARIABLE); } /** * Private constructor prevents instantiation * * @throws UnsupportedOperationException because this class cannot be instantiated. */ private RenderTemplatePluginBundleValues() { throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAAPI.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; public class HAAPI { private final HAServer mServer; public HAAPI(HAServer mServer) { this.mServer = mServer; } public boolean testServer() throws HAAPIException { HttpURLConnection httpConn = null; try { URL url = new URL(mServer.getBaseURL() + "/api/"); httpConn = (HttpURLConnection) url.openConnection(); httpConn.setRequestMethod("GET"); httpConn.setRequestProperty("Authorization", "Bearer " + mServer.getAccessToken()); InputStream inputStream = httpConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = bufferedReader.readLine(); JSONObject apiResult = new JSONObject(line); return apiResult.has("message") && "API running.".equals(apiResult.getString("message")); } catch (IOException e) { if (httpConn != null) try { throw new HAAPIException("Network Error: ".concat(httpConn.getResponseMessage()), e); } catch (IOException e1) { throw new HAAPIException("Network Error", e); } else throw new HAAPIException("IO Error", e); } catch (JSONException e) { throw new HAAPIException("JSON Error", e); } } public List getServices() throws HAAPIException { HttpURLConnection httpConn = null; try { URL url = new URL(mServer.getBaseURL() + "/api/services"); httpConn = (HttpURLConnection) url.openConnection(); httpConn.setRequestMethod("GET"); httpConn.setRequestProperty("Authorization", "Bearer " + mServer.getAccessToken()); InputStream inputStream = httpConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = bufferedReader.readLine(); List result = new ArrayList<>(); JSONArray apiResults = new JSONArray(line); for (int d = 0; d < apiResults.length(); d++) { JSONObject jsonDomain = apiResults.getJSONObject(d); String strDomain = jsonDomain.getString("domain"); JSONObject jsonServices = jsonDomain.getJSONObject("services"); Iterator keys = jsonServices.keys(); while (keys.hasNext()) result.add(strDomain + "." + keys.next()); } Collections.sort(result); return result; } catch (IOException e) { if (httpConn != null) try { throw new HAAPIException("Network Error: ".concat(httpConn.getResponseMessage()), e); } catch (IOException e1) { throw new HAAPIException("Network Error", e); } else throw new HAAPIException("IO Error", e); } catch (JSONException e) { throw new HAAPIException("JSON Error", e); } } public void callService(String domain, String service, String data) throws HAAPIException { HttpURLConnection httpConn = null; try { URL url = new URL(mServer.getBaseURL() + "/api/services/" + domain + "/" + service); httpConn = (HttpURLConnection)url.openConnection(); httpConn.setRequestMethod("POST"); httpConn.setRequestProperty("Authorization", "Bearer " + mServer.getAccessToken()); httpConn.setRequestProperty("Content-Type", "application/json"); httpConn.setDoOutput(true); if (data != null &! data.isEmpty()) { OutputStream os = httpConn.getOutputStream(); os.write(data.getBytes("UTF-8")); os.close(); } if (httpConn.getResponseCode() != 200) { InputStream errorStream = httpConn.getErrorStream(); InputStreamReader errorStreamReader = new InputStreamReader(errorStream); BufferedReader bufferedReader = new BufferedReader(errorStreamReader); JSONObject jsonResult = new JSONObject(bufferedReader.readLine()); throw new HAAPIException(jsonResult.getString("message")); } } catch (IOException e) { if (httpConn != null) try { throw new HAAPIException("Network Error: ".concat(httpConn.getResponseMessage()), e); } catch (IOException e1) { throw new HAAPIException("Network Error", e); } else throw new HAAPIException("IO Error", e); } catch (JSONException e) { throw new HAAPIException("JSON Error", e); } } public List getEntities() throws HAAPIException { HttpURLConnection httpConn = null; try { URL url = new URL(mServer.getBaseURL() + "/api/states"); httpConn = (HttpURLConnection)url.openConnection(); httpConn.setRequestMethod("GET"); httpConn.setRequestProperty("Authorization", "Bearer " + mServer.getAccessToken()); InputStream inputStream = httpConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = bufferedReader.readLine(); List result = new ArrayList<>(); JSONArray apiResults = new JSONArray(line); for (int e = 0; e < apiResults.length(); e++) result.add(apiResults.getJSONObject(e).getString("entity_id")); Collections.sort(result); return result; } catch (IOException e) { if (httpConn != null) try { throw new HAAPIException("Network Error: ".concat(httpConn.getResponseMessage()), e); } catch (IOException e1) { throw new HAAPIException("Network Error", e); } else throw new HAAPIException("IO Error", e); } catch (JSONException e) { throw new HAAPIException("JSON Error", e); } } public HAEntity getEntity(String entityId) throws HAAPIException { HttpURLConnection httpConn = null; try { URL url = new URL(mServer.getBaseURL() + "/api/states/" + entityId); httpConn = (HttpURLConnection)url.openConnection(); httpConn.setRequestMethod("GET"); httpConn.setRequestProperty("Authorization", "Bearer " + mServer.getAccessToken()); InputStream inputStream = httpConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = bufferedReader.readLine(); JSONObject json = new JSONObject(line); return new HAEntity( json.getString("state"), json.getString("attributes")); } catch (IOException e) { if (httpConn != null) try { throw new HAAPIException("Network Error: ".concat(httpConn.getResponseMessage()), e); } catch (IOException e1) { throw new HAAPIException("Network Error", e); } else throw new HAAPIException("IO Error", e); } catch (JSONException e) { throw new HAAPIException("JSON Error", e); } } public String renderTemplate(String template) throws HAAPIException { HttpURLConnection httpConn = null; try { URL url = new URL(mServer.getBaseURL() + "/api/template"); httpConn = (HttpURLConnection)url.openConnection(); httpConn.setRequestMethod("POST"); httpConn.setRequestProperty("Authorization", "Bearer " + mServer.getAccessToken()); httpConn.setRequestProperty("Content-Type", "application/json"); httpConn.setDoOutput(true); JSONObject jsonBody = new JSONObject(); jsonBody.put("template", template); byte[] outputBytes = jsonBody.toString().getBytes("UTF-8"); OutputStream os = httpConn.getOutputStream(); os.write(outputBytes); os.close(); StringBuilder sb = new StringBuilder(); BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream())); int c; while ((c = reader.read()) != -1) sb.append((char) c); return sb.toString(); } catch (IOException e) { if (httpConn != null) try { throw new HAAPIException("Network Error: ".concat(httpConn.getResponseMessage()), e); } catch (IOException e1) { throw new HAAPIException("Network Error", e); } else throw new HAAPIException("IO Error", e); } catch (JSONException e) { throw new HAAPIException("JSON Error", e); } } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAAPIException.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; import java.lang.Exception; public class HAAPIException extends Exception { private static final long serialVersionUID = 7395216398898021862L; public HAAPIException() { super(); } public HAAPIException(String message) { super(message); } public HAAPIException(String message, Throwable cause) { super(message, cause); } public HAAPIException(Throwable cause) { super(cause); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAAPIResult.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; public class HAAPIResult { private final HAAPIException mException ; private final T mResult; public HAAPIResult(T result) { mResult = result; mException = null; } public HAAPIResult(HAAPIException exception) { mException = exception; mResult = null; } public HAAPIException getException() { return mException; } public T getResult() { return mResult; } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAAPITask.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; import android.os.AsyncTask; public abstract class HAAPITask extends AsyncTask> { private final HAAPI mAPI; protected HAAPITask(HAServer server) { this.mAPI = new HAAPI(server); } @Override @SafeVarargs protected final HAAPIResult doInBackground(Params... params) { try { return new HAAPIResult<>(doAPIInBackground(mAPI, params)); } catch (HAAPIException e) { return new HAAPIResult<>(e); } } protected abstract Result doAPIInBackground(HAAPI api, Params... params) throws HAAPIException; } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAEntity.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; public class HAEntity { private String mState, mAttributes; public HAEntity(String mState, String mAttributes) { this.mState = mState; this.mAttributes = mAttributes; } public String getState() { return mState; } public String getAttributes() { return mAttributes; } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAServer.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; import org.json.JSONException; import org.json.JSONObject; public class HAServer { private String name, baseURL, accessToken; public HAServer(String name, String baseURL, String accessToken) { this.name = name; this.baseURL = baseURL; this.accessToken = accessToken; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getBaseURL() { return baseURL; } public void setBaseURL(String baseURL) { this.baseURL = baseURL; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } @Override public String toString() { return getName(); } private static String NAME = "Name"; private static String BASE_URL = "BaseURL"; private static String ACCESS_TOKEN = "AccessToken"; public static HAServer fromJSON(JSONObject json) throws JSONException { return new HAServer( json.getString(NAME), json.getString(BASE_URL), json.getString(ACCESS_TOKEN) ); } public JSONObject toJSON() { JSONObject result = new JSONObject(); try { result.put(NAME, name); result.put(BASE_URL, baseURL); result.put(ACCESS_TOKEN, accessToken); } catch (JSONException e) { e.printStackTrace(); } return result; } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/model/HAServerStore.java ================================================ package com.markadamson.taskerplugin.homeassistant.model; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import org.json.JSONException; import org.json.JSONObject; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; public class HAServerStore { private final Context mContext; public HAServerStore(final Context context) { mContext = context; } private static String PREFS_KEY = "com.markadamson.taskerplugin.homeassistant.model.HAServerStore.PREFS_KEY"; public Map getServers() { Map result = new HashMap<>(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); if (prefs.contains(PREFS_KEY)) { try { JSONObject jsonServers = new JSONObject(prefs.getString(PREFS_KEY, "")); Iterator keys = jsonServers.keys(); while (keys.hasNext()) { String uuid = keys.next(); result.put(UUID.fromString(uuid), HAServer.fromJSON((JSONObject) jsonServers.get(uuid))); } } catch (JSONException e) { e.printStackTrace(); prefs.edit().remove(PREFS_KEY).apply(); } } return result; } public UUID addServer(HAServer server) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); JSONObject jsonServers; if (prefs.contains(PREFS_KEY)) { try { jsonServers = new JSONObject(prefs.getString(PREFS_KEY, "")); } catch (JSONException e) { jsonServers = new JSONObject(); } } else jsonServers = new JSONObject(); UUID result = UUID.randomUUID(); try { jsonServers.put(result.toString(), server.toJSON()); } catch (JSONException e) { e.printStackTrace(); } prefs.edit().putString(PREFS_KEY, jsonServers.toString()).apply(); return result; } public void deleteServer(UUID serverID) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); if (prefs.contains(PREFS_KEY)) { JSONObject jsonServers; try { jsonServers = new JSONObject(prefs.getString(PREFS_KEY, "")); } catch (JSONException e) { e.printStackTrace(); prefs.edit().remove(PREFS_KEY).apply(); return; } if (jsonServers.has(serverID.toString())) { jsonServers.remove(serverID.toString()); prefs.edit().putString(PREFS_KEY, jsonServers.toString()).apply(); } } } public void updateServer(UUID serverID, HAServer server) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); if (!prefs.contains(PREFS_KEY)) throw new RuntimeException("No such server in storage!"); JSONObject jsonServers; try { jsonServers = new JSONObject(prefs.getString(PREFS_KEY, "")); } catch (JSONException e) { e.printStackTrace(); prefs.edit().remove(PREFS_KEY).apply(); throw new RuntimeException("No such server in storage!", e); } if (!jsonServers.has(serverID.toString())) throw new RuntimeException("No such server in storage!"); jsonServers.remove(serverID.toString()); try { jsonServers.put(serverID.toString(), server.toJSON()); } catch (JSONException e) { e.printStackTrace(); } prefs.edit().putString(PREFS_KEY, jsonServers.toString()).apply(); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/receiver/AbstractAsyncReceiver.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-plugin-client-sdk-for-locale https://github.com/twofortyfouram/android-plugin-client-sdk-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.receiver; import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; import android.util.Pair; import com.twofortyfouram.spackle.AndroidSdkVersion; import com.twofortyfouram.spackle.ThreadUtil; import com.twofortyfouram.spackle.ThreadUtil.ThreadPriority; import net.jcip.annotations.ThreadSafe; import static com.twofortyfouram.assertion.Assertions.assertInRangeInclusive; import static com.twofortyfouram.assertion.Assertions.assertNotNull; /** * Simplifies asynchronous broadcast handling. Subclasses call * {@link #goAsyncWithCallback(AsyncCallback, boolean)}, and the abstract class takes * care of executing the callback on a background thread. */ @ThreadSafe /* package */ abstract class AbstractAsyncReceiver extends BroadcastReceiver { /* * This method is package visible rather than protected so that it will be * obfuscated by ProGuard. * * @param callback Callback to execute on a background thread. * @param isOrdered Indicates whether an ordered broadcast is being processed. */ @TargetApi(VERSION_CODES.HONEYCOMB) /* package */ final void goAsyncWithCallback(@NonNull final AsyncCallback callback, final boolean isOrdered) { assertNotNull(callback, "callback"); //$NON-NLS-1$ final PendingResult pendingResult = goAsync(); if (null == pendingResult) { throw new AssertionError( "PendingResult was null. Was goAsync() called previously?"); //$NON-NLS-1$ } final Handler.Callback handlerCallback = new AsyncHandlerCallback(); final HandlerThread thread = ThreadUtil.newHandlerThread(getClass().getName(), ThreadPriority.BACKGROUND); final Handler handler = new Handler(thread.getLooper(), handlerCallback); final Object obj = new Pair(pendingResult, callback); final int isOrderedInt = isOrdered ? 1 : 0; final Message msg = handler .obtainMessage(AsyncHandlerCallback.MESSAGE_HANDLE_CALLBACK, isOrderedInt, 0, obj); final boolean isMessageSent = handler.sendMessage(msg); if (!isMessageSent) { throw new AssertionError(); } } @TargetApi(VERSION_CODES.HONEYCOMB) private static final class AsyncHandlerCallback implements Handler.Callback { /** * Message MUST contain a {@code Pair} as the {@code msg.obj} * and a boolean encoded in the {@code msg.arg1} to indicate whether the broadcast was * ordered. */ public static final int MESSAGE_HANDLE_CALLBACK = 0; @Override public boolean handleMessage(final Message msg) { assertNotNull(msg, "msg"); //$NON-NLS-1$ switch (msg.what) { case MESSAGE_HANDLE_CALLBACK: { assertNotNull(msg.obj, "msg.obj"); //$NON-NLS-1$ assertInRangeInclusive(msg.arg1, 0, 1, "msg.arg1"); //$NON-NLS-1$ final Pair pair = castObj(msg.obj); final boolean isOrdered = 0 != msg.arg1; final PendingResult pendingResult = pair.first; final AsyncCallback asyncCallback = pair.second; try { final int resultCode = asyncCallback.runAsync(); if (isOrdered) { pendingResult.setResultCode(resultCode); } } finally { pendingResult.finish(); } quit(); break; } } return true; } @NonNull @SuppressWarnings("unchecked") private static Pair castObj(@NonNull final Object o) { return (Pair) o; } private static void quit() { if (AndroidSdkVersion.isAtLeastSdk(VERSION_CODES.JELLY_BEAN_MR2)) { quitJellybeanMr2(); } else { Looper.myLooper().quit(); } } @TargetApi(VERSION_CODES.JELLY_BEAN_MR2) private static void quitJellybeanMr2() { Looper.myLooper().quitSafely(); } } /* package */static interface AsyncCallback { /** * @return The result code to be set if this is an ordered broadcast. */ public int runAsync(); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/receiver/AbstractPluginSettingReceiver.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-plugin-client-sdk-for-locale https://github.com/twofortyfouram/android-plugin-client-sdk-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.receiver; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import com.twofortyfouram.log.Lumberjack; import com.twofortyfouram.spackle.AndroidSdkVersion; import com.twofortyfouram.spackle.bundle.BundleScrubber; /** *

Abstract superclass for a plug-in setting BroadcastReceiver implementation.

*

The plug-in receiver lifecycle is as follows:

*
    *
  1. {@link #onReceive(android.content.Context, android.content.Intent)} is called by the Android * frameworks. * onReceive() will verify that the Intent is valid. If the Intent is invalid, the receiver * returns * immediately. If the Intent appears to be valid, then the lifecycle continues.
  2. *
  3. {@link #isBundleValid(android.os.Bundle)} is called to determine whether {@link * com.twofortyfouram.locale.api.Intent#EXTRA_BUNDLE EXTRA_BUNDLE} is valid. If the Bundle is * invalid, then the * receiver returns immediately. If the bundle is valid, then the lifecycle continues.
  4. *
  5. {@link #isAsync()} is called to determine whether the remaining work should be performed on * a * background thread.
  6. *
  7. {@link #firePluginSetting(android.content.Context, android.os.Bundle)} is called to trigger * the plug-in setting's action.
  8. *
*

* Implementations of this BroadcastReceiver must be registered in the Android * Manifest with an Intent filter for * {@link com.twofortyfouram.locale.api.Intent#ACTION_FIRE_SETTING ACTION_FIRE_SETTING}. The * BroadcastReceiver must be exported, enabled, and cannot have permissions * enforced on it. *

*/ public abstract class AbstractPluginSettingReceiver extends AbstractAsyncReceiver { /* * The multiple return statements in this method are a little gross, but the * alternative of nested if statements is even worse :/ */ @Override public final void onReceive(final Context context, final Intent intent) { if (BundleScrubber.scrub(intent)) { return; } Lumberjack.v("Received %s", intent); //$NON-NLS-1$ /* * Note: It is OK if a host sends an ordered broadcast for plug-in * settings. Such a behavior would allow the host to optionally block until the * plug-in setting finishes. */ if (!com.twofortyfouram.locale.api.Intent.ACTION_FIRE_SETTING.equals(intent.getAction())) { Lumberjack .e("Intent action is not %s", com.twofortyfouram.locale.api.Intent.ACTION_FIRE_SETTING); //$NON-NLS-1$ return; } /* * Ignore implicit intents, because they are not valid. It would be * meaningless if ALL plug-in setting BroadcastReceivers installed were * asked to handle queries not intended for them. Ideally this * implementation here would also explicitly assert the class name as * well, but then the unit tests would have trouble. In the end, * asserting the package is probably good enough. */ if (!context.getPackageName().equals(intent.getPackage()) && !new ComponentName(context, this.getClass().getName()).equals(intent .getComponent())) { Lumberjack.e("Intent is not explicit"); //$NON-NLS-1$ return; } final Bundle bundle = intent .getBundleExtra(com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE); if (BundleScrubber.scrub(intent)) { return; } if (null == bundle) { Lumberjack.e("%s is missing", com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE); //$NON-NLS-1$ return; } if (!isBundleValid(bundle)) { Lumberjack.e("%s is invalid", com.twofortyfouram.locale.api.Intent.EXTRA_BUNDLE); //$NON-NLS-1$ return; } if (isAsync() && AndroidSdkVersion.isAtLeastSdk(Build.VERSION_CODES.HONEYCOMB)) { final AsyncCallback callback = new AsyncCallback() { @NonNull private final Context mContext = context; @NonNull private final Bundle mBundle = bundle; @NonNull private final Intent mIntent = intent; @Override public int runAsync() { firePluginSetting(mContext, mIntent, mBundle); return Activity.RESULT_OK; } }; goAsyncWithCallback(callback, isOrderedBroadcast()); } else { firePluginSetting(context, intent, bundle); } } /** *

Gives the plug-in receiver an opportunity to validate the Bundle, to * ensure that a malicious application isn't attempting to pass * an invalid Bundle.

*

* This method will be called on the BroadcastReceiver's Looper (normatively the main thread) *

* * @param bundle The plug-in's Bundle previously returned by the edit * Activity. {@code bundle} should not be mutated by this method. * @return true if {@code bundle} appears to be valid. false if {@code bundle} appears to be * invalid. */ protected abstract boolean isBundleValid(@NonNull final Bundle bundle); /** * Configures the receiver whether it should process the Intent in a * background thread. Plug-ins should return true if their * {@link #firePluginSetting(android.content.Context, android.os.Bundle)} method performs any * sort of disk IO (ContentProvider query, reading SharedPreferences, etc.). * or other work that may be slow. *

* Asynchronous BroadcastReceivers are not supported prior to Honeycomb, so * with older platforms broadcasts will always be processed on the BroadcastReceiver's Looper * (which for Manifest registered receivers will be the main thread). * * @return True if the receiver should process the Intent in a background * thread. False if the plug-in should process the Intent on the * BroadcastReceiver's Looper (normatively the main thread). */ protected abstract boolean isAsync(); /** * If {@link #isAsync()} returns true, this method will be called on a * background thread. If {@link #isAsync()} returns false, this method will * be called on the main thread. Regardless of which thread this method is * called on, this method MUST return within 10 seconds per the requirements * for BroadcastReceivers. * * @param context BroadcastReceiver context. * @param bundle The plug-in's Bundle previously returned by the edit * Activity. */ protected abstract void firePluginSetting(@NonNull final Context context, @NonNull final Intent intent, @NonNull final Bundle bundle); } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/receiver/FireReceiver.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.receiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import com.markadamson.taskerplugin.homeassistant.Constants; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.markadamson.taskerplugin.homeassistant.bundle.GetStatePluginBundleValues; import com.markadamson.taskerplugin.homeassistant.bundle.PluginBundleValues; import com.markadamson.taskerplugin.homeassistant.bundle.RenderTemplatePluginBundleValues; import com.markadamson.taskerplugin.homeassistant.service.ActionService; import com.twofortyfouram.log.Lumberjack; public final class FireReceiver extends AbstractPluginSettingReceiver { @Override protected boolean isBundleValid(@NonNull final Bundle bundle) { if (bundle.getInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE) < 3 || bundle.getInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE) == Constants.BUNDLE_CALL_SERVICE) return PluginBundleValues.isBundleValid(bundle); else if (bundle.getInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE) == Constants.BUNDLE_GET_STATE) return GetStatePluginBundleValues.isBundleValid(bundle); else return RenderTemplatePluginBundleValues.isBundleValid(bundle); } @Override protected boolean isAsync() { return false; } @Override protected void firePluginSetting(@NonNull final Context context, @NonNull final Intent intent, @NonNull final Bundle bundle) { Lumberjack.d("FireReceiver.firePluginSetting"); intent.putExtra(ActionService.EXT_BUNDLE, bundle); ActionService.enqueueWork(context, intent); Lumberjack.d("Set result code \" Pending\""); if (isOrderedBroadcast()) setResultCode(TaskerPlugin.Setting.RESULT_CODE_PENDING); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/service/ActionService.java ================================================ package com.markadamson.taskerplugin.homeassistant.service; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.JobIntentService; import com.markadamson.taskerplugin.homeassistant.Constants; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.markadamson.taskerplugin.homeassistant.bundle.GetStatePluginBundleValues; import com.markadamson.taskerplugin.homeassistant.bundle.PluginBundleValues; import com.markadamson.taskerplugin.homeassistant.bundle.RenderTemplatePluginBundleValues; import com.markadamson.taskerplugin.homeassistant.model.HAAPI; import com.markadamson.taskerplugin.homeassistant.model.HAAPIException; import com.markadamson.taskerplugin.homeassistant.model.HAEntity; import com.markadamson.taskerplugin.homeassistant.model.HAServerStore; import com.twofortyfouram.log.Lumberjack; import java.util.UUID; /** * Created by marka on 14/06/2019. */ public class ActionService extends JobIntentService { public static final String EXT_BUNDLE = "com.markadamson.taskerplugin.homeassistant.service.ActionService.EXT_BUNDLE"; /** * Unique job ID for this service. */ static final int JOB_ID = 1000; /** * Convenience method for enqueuing work in to this service. */ public static void enqueueWork(Context context, Intent work) { Lumberjack.d("ActionService.enqueueWork"); enqueueWork(context, ActionService.class, JOB_ID, work); } private void signalUnknownServer(Intent intent) { Bundle vars = new Bundle(); vars.putString(TaskerPlugin.Setting.VARNAME_ERROR_MESSAGE, "Unknown Server - Has it been deleted?"); TaskerPlugin.Setting.signalFinish(this, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, vars); } @Override protected void onHandleWork(@NonNull Intent intent) { Lumberjack.d("ActionService.onHandleWork"); Bundle bundle = intent.getBundleExtra(EXT_BUNDLE); HAServerStore servers = new HAServerStore(this); try { if (bundle.getInt(PluginBundleValues.BUNDLE_EXTRA_INT_VERSION_CODE) < 3 || bundle.getInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE) == Constants.BUNDLE_CALL_SERVICE) { Lumberjack.d("Call Service", bundle); UUID serverId = PluginBundleValues.getServer(bundle); if (!servers.getServers().containsKey(serverId)) { signalUnknownServer(intent); return; } String[] service = PluginBundleValues.getService(bundle).split("\\."); Lumberjack.d("Calling api..."); new HAAPI(servers.getServers().get(serverId)) .callService(service[0], service[1], PluginBundleValues.getData(bundle)); Lumberjack.d("Signalling finish..."); TaskerPlugin.Setting.signalFinish(this, intent, TaskerPlugin.Setting.RESULT_CODE_OK, null); } else if (bundle.getInt(Constants.BUNDLE_EXTRA_BUNDLE_TYPE) == Constants.BUNDLE_GET_STATE) { Lumberjack.d("Get State", bundle); UUID serverId = GetStatePluginBundleValues.getServer(bundle); if (!servers.getServers().containsKey(serverId)) { signalUnknownServer(intent); return; } String entityId = GetStatePluginBundleValues.getEntity(bundle); Lumberjack.d("Calling api..."); HAEntity entity = new HAAPI(servers.getServers().get(serverId)) .getEntity(entityId); Bundle vars = new Bundle(); vars.putString(GetStatePluginBundleValues.getStateVariable(bundle), entity.getState()); String attrsVar = GetStatePluginBundleValues.getAttrsVariable(bundle); if (!attrsVar.isEmpty()) vars.putString(attrsVar, entity.getAttributes()); Lumberjack.d("Signalling finish..."); TaskerPlugin.Setting.signalFinish(this, intent, TaskerPlugin.Setting.RESULT_CODE_OK, vars); } else { Lumberjack.d("Render Template", bundle); UUID serverId = RenderTemplatePluginBundleValues.getServer(bundle); if (!servers.getServers().containsKey(serverId)) { signalUnknownServer(intent); return; } String template = RenderTemplatePluginBundleValues.getTemplate(bundle); Lumberjack.d("Calling api..."); String result = new HAAPI(servers.getServers().get(serverId)) .renderTemplate(template); Bundle vars = new Bundle(); vars.putString(RenderTemplatePluginBundleValues.getVariable(bundle), result); Lumberjack.d("Signalling finish..."); TaskerPlugin.Setting.signalFinish(this, intent, TaskerPlugin.Setting.RESULT_CODE_OK, vars); } } catch (HAAPIException e) { Lumberjack.e(e.getMessage()); e.printStackTrace(); Bundle vars = new Bundle(); vars.putString(TaskerPlugin.Setting.VARNAME_ERROR_MESSAGE, e.getMessage()); TaskerPlugin.Setting.signalFinish(this, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, vars); } } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/ui/ServerSelectionUI.java ================================================ package com.markadamson.taskerplugin.homeassistant.ui; import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; import android.support.v7.app.AlertDialog; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Spinner; import com.markadamson.taskerplugin.homeassistant.R; import com.markadamson.taskerplugin.homeassistant.model.HAServer; import com.markadamson.taskerplugin.homeassistant.model.HAServerStore; import com.markadamson.taskerplugin.homeassistant.ui.activity.EditServerActivity; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; public class ServerSelectionUI { private final CharSequence mHostName; private final HAServerStore mServerStore; private final Spinner spnServers; private List mIds; private List mServers; private ArrayAdapter mServerAdapter; public ServerSelectionUI(final Activity activity, final CharSequence hostName, final OnServerSelectedListener serverListener) { mHostName = hostName; mServerStore = new HAServerStore(activity); Map serverMap = mServerStore.getServers(); mIds = new ArrayList<>(serverMap.keySet()); mServers = new ArrayList<>(); for(UUID id : mIds) mServers.add(serverMap.get(id)); mServerAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_dropdown_item, mServers); spnServers = activity.findViewById(R.id.spn_server); spnServers.setAdapter(mServerAdapter); spnServers.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView adapterView, View view, int position, long id) { if (serverListener != null) serverListener.onServerSelected(mServers.get(position)); } @Override public void onNothingSelected(AdapterView adapterView) { if (serverListener != null) serverListener.onNothingSelected(); } }); activity.findViewById(R.id.btn_add_server).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { activity.startActivityForResult( new Intent(activity, EditServerActivity.class) .putExtra(EditServerActivity.EXT_HOST_NAME, mHostName) .putExtra(EditServerActivity.EXT_REQUEST_CODE, EditServerActivity.REQ_NEW_SERVER), EditServerActivity.REQ_NEW_SERVER); } } ); activity.findViewById(R.id.btn_edit_server).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if (mServerAdapter.getCount() > 0) { HAServer server = mServerAdapter.getItem(spnServers.getSelectedItemPosition()); activity.startActivityForResult( new Intent(activity, EditServerActivity.class) .putExtra(EditServerActivity.EXT_HOST_NAME, mHostName) .putExtra(EditServerActivity.EXT_REQUEST_CODE, EditServerActivity.REQ_EDIT_SERVER) .putExtra(EditServerActivity.EXT_SERVER_NAME, server.getName()) .putExtra(EditServerActivity.EXT_BASE_URL, server.getBaseURL()) .putExtra(EditServerActivity.EXT_ACCESS_TOKEN, server.getAccessToken()), EditServerActivity.REQ_EDIT_SERVER ); } } } ); activity.findViewById(R.id.btn_delete_server).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if (mServers.size() > 0) new AlertDialog.Builder(activity) .setTitle(R.string.are_you_sure) .setMessage(R.string.delete_server_warning) .setPositiveButton(R.string.yes_delete_the_server, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { int idx = spnServers.getSelectedItemPosition(); mServerStore.deleteServer(mIds.get(idx)); mIds.remove(idx); mServers.remove(idx); mServerAdapter.notifyDataSetChanged(); } }) .setNegativeButton(android.R.string.no, null) .show(); } } ); } public int serverCount() { return mServers.size(); } public HAServer currentServer() { return mServers.get(spnServers.getSelectedItemPosition()); } public UUID currentId() { return mIds.get(spnServers.getSelectedItemPosition()); } public void setSelection(UUID id) { spnServers.setSelection(mIds.indexOf(id)); } public boolean onActivityResult(int requestCode, int resultCode, Intent data) { if (!Arrays.asList(new Integer[]{EditServerActivity.REQ_NEW_SERVER,EditServerActivity.REQ_EDIT_SERVER}).contains(requestCode)) return false; if (resultCode == Activity.RESULT_OK) { HAServer server = new HAServer( data.getStringExtra(EditServerActivity.EXT_SERVER_NAME), data.getStringExtra(EditServerActivity.EXT_BASE_URL), data.getStringExtra(EditServerActivity.EXT_ACCESS_TOKEN) ); switch (requestCode) { case EditServerActivity.REQ_NEW_SERVER: mIds.add(mServerStore.addServer(server)); mServerAdapter.add(server); break; case EditServerActivity.REQ_EDIT_SERVER: int idx = spnServers.getSelectedItemPosition(); mServerStore.updateServer(mIds.get(idx), server); mServers.set(idx, server); mServerAdapter.notifyDataSetChanged(); break; } } return true; } public interface OnServerSelectedListener { void onServerSelected(HAServer server); void onNothingSelected(); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/ui/activity/EditActivity.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.ui.activity; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.support.annotation.NonNull; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.EditText; import android.widget.Toast; import com.markadamson.locale.sdk.client.ui.activity.AbstractAppCompatPluginActivity; import com.markadamson.taskerplugin.homeassistant.R; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.markadamson.taskerplugin.homeassistant.Utils; import com.markadamson.taskerplugin.homeassistant.bundle.PluginBundleValues; import com.markadamson.taskerplugin.homeassistant.model.HAAPI; import com.markadamson.taskerplugin.homeassistant.model.HAAPIException; import com.markadamson.taskerplugin.homeassistant.model.HAAPIResult; import com.markadamson.taskerplugin.homeassistant.model.HAAPITask; import com.markadamson.taskerplugin.homeassistant.model.HAServer; import com.markadamson.taskerplugin.homeassistant.ui.ServerSelectionUI; import com.twofortyfouram.log.Lumberjack; import net.jcip.annotations.NotThreadSafe; import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.UUID; @NotThreadSafe public final class EditActivity extends AbstractAppCompatPluginActivity { private ServerSelectionUI mServerUI; private ArrayAdapter mServiceAdapter; private AutoCompleteTextView atvService; private EditText etServiceData; private abstract static class MyAPITask extends HAAPITask { WeakReference activityReference; MyAPITask(EditActivity activity, HAServer server) { super(server); activityReference = new WeakReference<>(activity); } } private static class GetServicesTask extends MyAPITask> { GetServicesTask(EditActivity activity, HAServer server) { super(activity, server); activity.mServiceAdapter.clear(); } @Override protected List doAPIInBackground(HAAPI api, Void... voids) throws HAAPIException { return api.getServices(); } @Override protected void onPostExecute(HAAPIResult> services) { EditActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; if (services.getException() != null) { services.getException().printStackTrace(); Toast.makeText(activity, services.getException().getMessage(), Toast.LENGTH_SHORT).show(); return; } activity.mServiceAdapter.clear(); activity.mServiceAdapter.addAll(services.getResult()); } } private static class TestServiceTask extends MyAPITask { TestServiceTask(EditActivity context, HAServer server) { super(context, server); } @Override protected Void doAPIInBackground(HAAPI api, String... strings) throws HAAPIException { api.callService(strings[0], strings[1], strings[2]); return null; } @Override protected void onPostExecute(HAAPIResult result) { EditActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; if (result.getException() != null) { result.getException().printStackTrace(); Toast.makeText(activity, result.getException().getMessage(), Toast.LENGTH_SHORT).show(); return; } Toast.makeText(activity, "Success!", Toast.LENGTH_SHORT).show(); } } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); /* * To help the user keep context, the title shows the host's name and the subtitle * shows the plug-in's name. */ CharSequence callingApplicationLabel = null; try { callingApplicationLabel = getPackageManager().getApplicationLabel( getPackageManager().getApplicationInfo(getCallingPackage(), 0)); } catch (final PackageManager.NameNotFoundException e) { Lumberjack.e("Calling package couldn't be found%s", e); //$NON-NLS-1$ } if (null != callingApplicationLabel) { setTitle(callingApplicationLabel); } Resources r = getResources(); getSupportActionBar().setSubtitle(r.getString(R.string.activity_subtitle, r.getString(R.string.call_service))); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mServerUI = new ServerSelectionUI(this, callingApplicationLabel, new ServerSelectionUI.OnServerSelectedListener() { @Override public void onServerSelected(HAServer server) { new GetServicesTask(EditActivity.this, server).execute(); } @Override public void onNothingSelected() { mServiceAdapter.clear(); } }); String[] variablesFromHost = TaskerPlugin.getRelevantVariableList(getIntent().getExtras()); mServiceAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, new ArrayList()); atvService = findViewById(R.id.atv_service); atvService.setAdapter(mServiceAdapter); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_service_variable), atvService); etServiceData = findViewById(R.id.et_service_data); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_service_data_variable), etServiceData); findViewById(R.id.btn_test_service).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if (atvService.getText().toString().contains("%")) { Toast.makeText(EditActivity.this, "Cannot test using variables!", Toast.LENGTH_SHORT).show(); return; } String[] service = atvService.getText().toString().split("\\."); if (service.length != 2) { Toast.makeText(EditActivity.this, "Invalid service!", Toast.LENGTH_SHORT).show(); return; } new TestServiceTask(EditActivity.this, mServerUI.currentServer()) .execute(service[0], service[1], etServiceData.getText().toString()); } } ); Utils.checkBatteryOptimisation(this); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); mServerUI.onActivityResult(requestCode, resultCode, data); } private void restoreState(UUID id, String service, String data) { mServerUI.setSelection(id); atvService.setText(service); etServiceData.setText(data); } @Override public void onPostCreateWithPreviousResult(@NonNull final Bundle previousBundle, @NonNull final String previousBlurb) { restoreState( PluginBundleValues.getServer(previousBundle), PluginBundleValues.getService(previousBundle), PluginBundleValues.getData(previousBundle) ); } @Override public boolean isBundleValid(@NonNull final Bundle bundle) { return PluginBundleValues.isBundleValid(bundle); } @Override public Bundle getResultBundle() { return PluginBundleValues.generateBundle(getApplicationContext(), mServerUI.currentId(), atvService.getText().toString(), etServiceData.getText().toString()); } @NonNull @Override public String getResultBlurb(@NonNull final Bundle bundle) { final String message = PluginBundleValues.getService(bundle); final int maxBlurbLength = getResources().getInteger( R.integer.com_twofortyfouram_locale_sdk_client_maximum_blurb_length); if (message.length() > maxBlurbLength) { return message.substring(0, maxBlurbLength - 1).concat("…"); } return message; } @NonNull @Override public String[] getRelevantVariableList() { return new String[0]; } @Override public int requestedTimeoutMS() { return 10000; } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } private boolean canSave() { if (mServerUI.serverCount() == 0) { Toast.makeText(this, "Please select a Server", Toast.LENGTH_SHORT).show(); return false; } if (atvService.getText().toString().isEmpty()) { Toast.makeText(this, "Please select a Service", Toast.LENGTH_SHORT).show(); return false; } return true; } @Override public void onBackPressed() { if (canSave()) super.onBackPressed(); } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (android.R.id.home == item.getItemId()) { if (canSave()) finish(); } else if (R.id.menu_cancel == item.getItemId()) { mIsCancelled = true; finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/ui/activity/EditGetStateActivity.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Original author: * android-toast-setting-plugin-for-locale * Copyright 2014 two forty four a.m. LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.ui.activity; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.support.annotation.NonNull; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.EditText; import android.widget.Toast; import com.markadamson.locale.sdk.client.ui.activity.AbstractAppCompatPluginActivity; import com.markadamson.taskerplugin.homeassistant.R; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.markadamson.taskerplugin.homeassistant.Utils; import com.markadamson.taskerplugin.homeassistant.bundle.GetStatePluginBundleValues; import com.markadamson.taskerplugin.homeassistant.model.HAAPI; import com.markadamson.taskerplugin.homeassistant.model.HAAPIException; import com.markadamson.taskerplugin.homeassistant.model.HAAPIResult; import com.markadamson.taskerplugin.homeassistant.model.HAAPITask; import com.markadamson.taskerplugin.homeassistant.model.HAEntity; import com.markadamson.taskerplugin.homeassistant.model.HAServer; import com.markadamson.taskerplugin.homeassistant.ui.ServerSelectionUI; import com.twofortyfouram.log.Lumberjack; import net.jcip.annotations.NotThreadSafe; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.UUID; @NotThreadSafe public final class EditGetStateActivity extends AbstractAppCompatPluginActivity { private ServerSelectionUI mServerUI; private ArrayAdapter mEntityAdapter; private AutoCompleteTextView atvEntity; private EditText etStateVariable, etAttrsVariable; private abstract static class MyAPITask extends HAAPITask { WeakReference activityReference; MyAPITask(EditGetStateActivity context, HAServer server) { super(server); activityReference = new WeakReference<>(context); } } private static class GetEntitiesTask extends MyAPITask> { GetEntitiesTask(EditGetStateActivity activity, HAServer server) { super(activity, server); activity.mEntityAdapter.clear(); } @Override protected List doAPIInBackground(HAAPI api, Void... voids) throws HAAPIException { return api.getEntities(); } @Override protected void onPostExecute(HAAPIResult> entities) { EditGetStateActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; if (entities.getException() != null) { entities.getException().printStackTrace(); Toast.makeText(activity, entities.getException().getMessage(), Toast.LENGTH_SHORT).show(); return; } activity.mEntityAdapter.clear(); activity.mEntityAdapter.addAll(entities.getResult()); } } private static class TestEntityTask extends MyAPITask { TestEntityTask(EditGetStateActivity context, HAServer server) { super(context, server); } @Override protected HAEntity doAPIInBackground(HAAPI api, String... strings) throws HAAPIException { return api.getEntity(strings[0]); } @Override protected void onPostExecute(HAAPIResult state) { EditGetStateActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; if (state.getException() != null) { state.getException().printStackTrace(); Toast.makeText(activity, state.getException().getMessage(), Toast.LENGTH_SHORT).show(); return; } Toast.makeText(activity, state.getResult().getState(), Toast.LENGTH_SHORT).show(); } } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.edit_get_state); /* * To help the user keep context, the title shows the host's name and the subtitle * shows the plug-in's name. */ CharSequence callingApplicationLabel = null; try { callingApplicationLabel = getPackageManager().getApplicationLabel( getPackageManager().getApplicationInfo(getCallingPackage(), 0)); } catch (final PackageManager.NameNotFoundException e) { Lumberjack.e("Calling package couldn't be found%s", e); //$NON-NLS-1$ } if (null != callingApplicationLabel) { setTitle(callingApplicationLabel); } Resources r = getResources(); getSupportActionBar().setSubtitle(r.getString(R.string.activity_subtitle, r.getString(R.string.get_state))); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mServerUI = new ServerSelectionUI(this, callingApplicationLabel, new ServerSelectionUI.OnServerSelectedListener() { @Override public void onServerSelected(HAServer server) { new GetEntitiesTask(EditGetStateActivity.this, server).execute(); } @Override public void onNothingSelected() { mEntityAdapter.clear(); } }); String[] variablesFromHost = TaskerPlugin.getRelevantVariableList(getIntent().getExtras()); mEntityAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, new ArrayList()); atvEntity = findViewById(R.id.atv_entity); atvEntity.setAdapter(mEntityAdapter); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_entity_variable), atvEntity); etStateVariable = findViewById(R.id.et_state_variable); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_state_variable), etStateVariable); etAttrsVariable = findViewById(R.id.et_attrs_variable); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_attrs_variable), etAttrsVariable); findViewById(R.id.btn_test_entity).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if (atvEntity.getText().toString().contains("%")) { Toast.makeText(EditGetStateActivity.this, "Cannot test using variables in entity id!", Toast.LENGTH_SHORT).show(); return; } new TestEntityTask(EditGetStateActivity.this, mServerUI.currentServer()) .execute(atvEntity.getText().toString()); } } ); Utils.checkBatteryOptimisation(this); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); mServerUI.onActivityResult(requestCode, resultCode, data); } private void restoreState(UUID id, String entity, String stateVariable, String attrsVariable) { mServerUI.setSelection(id); atvEntity.setText(entity); etStateVariable.setText(stateVariable); etAttrsVariable.setText(attrsVariable); } @Override public void onPostCreateWithPreviousResult(@NonNull final Bundle previousBundle, @NonNull final String previousBlurb) { restoreState( GetStatePluginBundleValues.getServer(previousBundle), GetStatePluginBundleValues.getEntity(previousBundle), GetStatePluginBundleValues.getStateVariable(previousBundle), GetStatePluginBundleValues.getAttrsVariable(previousBundle) ); } @Override public boolean isBundleValid(@NonNull final Bundle bundle) { return GetStatePluginBundleValues.isBundleValid(bundle); } @Override public Bundle getResultBundle() { return GetStatePluginBundleValues.generateBundle(getApplicationContext(), mServerUI.currentId(), atvEntity.getText().toString(), etStateVariable.getText().toString(), etAttrsVariable.getText().toString()); } @NonNull @Override public String getResultBlurb(@NonNull final Bundle bundle) { final String message = GetStatePluginBundleValues.getEntity(bundle); final int maxBlurbLength = getResources().getInteger( R.integer.com_twofortyfouram_locale_sdk_client_maximum_blurb_length); if (message.length() > maxBlurbLength) { return message.substring(0, maxBlurbLength - 1).concat("…"); } return message; } @NonNull @Override public String[] getRelevantVariableList() { ArrayList vars = new ArrayList<>(); vars.add(String.format("%s\nEntity State\nThe entity state retrieved from %s", etStateVariable.getText().toString(), mServerUI.currentServer().getName())); if (!etAttrsVariable.getText().toString().isEmpty()) vars.add(String.format("%s\nEntity Attributes\nThe entity attributes retrieved from %s", etAttrsVariable.getText().toString(), mServerUI.currentServer().getName())); return vars.toArray(new String[vars.size()]); } @Override public int requestedTimeoutMS() { return 10000; } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } private boolean canSave() { boolean result = false; if (mServerUI.serverCount() == 0) Toast.makeText(this, "Please select a Server", Toast.LENGTH_SHORT).show(); else if (atvEntity.getText().toString().isEmpty()) Toast.makeText(this, "Please select an Entity", Toast.LENGTH_SHORT).show(); else if (!TaskerPlugin.variableNameValid(etStateVariable.getText().toString())) Toast.makeText(this, "State: Not a valid variable name", Toast.LENGTH_SHORT).show(); else if (!(etAttrsVariable.getText().toString().isEmpty() || TaskerPlugin.variableNameValid(etStateVariable.getText().toString()))) Toast.makeText(this, "Attributes: Not a valid variable name", Toast.LENGTH_SHORT).show(); else result = true; return result; } @Override public void onBackPressed() { if (canSave()) super.onBackPressed(); } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (android.R.id.home == item.getItemId()) { if (canSave()) finish(); } else if (R.id.menu_cancel == item.getItemId()) { mIsCancelled = true; finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/ui/activity/EditRenderTemplateActivity.java ================================================ /* * home-assistant-plugin-for-tasker * Copyright 2019 Mark Adamson * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.markadamson.taskerplugin.homeassistant.ui.activity; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.support.annotation.NonNull; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.Toast; import com.markadamson.locale.sdk.client.ui.activity.AbstractAppCompatPluginActivity; import com.markadamson.taskerplugin.homeassistant.R; import com.markadamson.taskerplugin.homeassistant.TaskerPlugin; import com.markadamson.taskerplugin.homeassistant.Utils; import com.markadamson.taskerplugin.homeassistant.bundle.RenderTemplatePluginBundleValues; import com.markadamson.taskerplugin.homeassistant.model.HAAPI; import com.markadamson.taskerplugin.homeassistant.model.HAAPIException; import com.markadamson.taskerplugin.homeassistant.model.HAAPIResult; import com.markadamson.taskerplugin.homeassistant.model.HAAPITask; import com.markadamson.taskerplugin.homeassistant.model.HAServer; import com.markadamson.taskerplugin.homeassistant.ui.ServerSelectionUI; import com.twofortyfouram.log.Lumberjack; import net.jcip.annotations.NotThreadSafe; import java.lang.ref.WeakReference; import java.util.UUID; @NotThreadSafe public final class EditRenderTemplateActivity extends AbstractAppCompatPluginActivity { private ServerSelectionUI mServerUI; private EditText etTemplate, etVariable; private abstract static class MyAPITask extends HAAPITask { WeakReference activityReference; MyAPITask(EditRenderTemplateActivity activity, HAServer server) { super(server); activityReference = new WeakReference<>(activity); } } private static class TestTemplateTask extends MyAPITask { TestTemplateTask(EditRenderTemplateActivity context, HAServer server) { super(context, server); } @Override protected String doAPIInBackground(HAAPI api, String... strings) throws HAAPIException { return api.renderTemplate(strings[0]); } @Override protected void onPostExecute(HAAPIResult result) { EditRenderTemplateActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; if (result.getException() != null) { result.getException().printStackTrace(); Toast.makeText(activity, result.getException().getMessage(), Toast.LENGTH_SHORT).show(); return; } Toast.makeText(activity, result.getResult(), Toast.LENGTH_SHORT).show(); } } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.edit_render_template); /* * To help the user keep context, the title shows the host's name and the subtitle * shows the plug-in's name. */ CharSequence callingApplicationLabel = null; try { callingApplicationLabel = getPackageManager().getApplicationLabel( getPackageManager().getApplicationInfo(getCallingPackage(), 0)); } catch (final PackageManager.NameNotFoundException e) { Lumberjack.e("Calling package couldn't be found%s", e); //$NON-NLS-1$ } if (null != callingApplicationLabel) { setTitle(callingApplicationLabel); } Resources r = getResources(); getSupportActionBar().setSubtitle(r.getString(R.string.activity_subtitle, r.getString(R.string.render_template))); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mServerUI = new ServerSelectionUI(this, callingApplicationLabel, new ServerSelectionUI.OnServerSelectedListener() { @Override public void onServerSelected(HAServer server) { } @Override public void onNothingSelected() { } }); String[] variablesFromHost = TaskerPlugin.getRelevantVariableList(getIntent().getExtras()); etTemplate = findViewById(R.id.et_template); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_template_variable), etTemplate); etVariable = findViewById(R.id.et_variable); Utils.initVariableSelectUI(variablesFromHost, findViewById(R.id.btn_variable), etVariable); findViewById(R.id.btn_test_template).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if (etTemplate.getText().toString().contains("%")) { Toast.makeText(EditRenderTemplateActivity.this, "Cannot test using variables!", Toast.LENGTH_SHORT).show(); return; } new TestTemplateTask(EditRenderTemplateActivity.this, mServerUI.currentServer()) .execute(etTemplate.getText().toString()); } } ); Utils.checkBatteryOptimisation(this); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); mServerUI.onActivityResult(requestCode, resultCode, data); } private void restoreState(UUID id, String template, String variable) { mServerUI.setSelection(id); etTemplate.setText(template); etVariable.setText(variable); } @Override public void onPostCreateWithPreviousResult(@NonNull final Bundle previousBundle, @NonNull final String previousBlurb) { restoreState( RenderTemplatePluginBundleValues.getServer(previousBundle), RenderTemplatePluginBundleValues.getTemplate(previousBundle), RenderTemplatePluginBundleValues.getVariable(previousBundle) ); } @Override public boolean isBundleValid(@NonNull final Bundle bundle) { return RenderTemplatePluginBundleValues.isBundleValid(bundle); } @Override public Bundle getResultBundle() { return RenderTemplatePluginBundleValues.generateBundle(getApplicationContext(), mServerUI.currentId(), etTemplate.getText().toString(), etVariable.getText().toString()); } @NonNull @Override public String getResultBlurb(@NonNull final Bundle bundle) { final String message = RenderTemplatePluginBundleValues.getTemplate(bundle); final int maxBlurbLength = getResources().getInteger( R.integer.com_twofortyfouram_locale_sdk_client_maximum_blurb_length); if (message.length() > maxBlurbLength) { return message.substring(0, maxBlurbLength - 1).concat("…"); } return message; } @NonNull @Override public String[] getRelevantVariableList() { return new String[] {String.format("%s\nRendered Template\nThe rendered template from %s", etVariable.getText().toString(), mServerUI.currentServer().getName())}; } @Override public int requestedTimeoutMS() { return 10000; } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } private boolean canSave() { boolean result = false; if (mServerUI.serverCount() == 0) Toast.makeText(this, "Please select a Server", Toast.LENGTH_SHORT).show(); else if (!TaskerPlugin.variableNameValid(etVariable.getText().toString())) Toast.makeText(this, "Not a valid variable name", Toast.LENGTH_SHORT).show(); else result = true; return result; } @Override public void onBackPressed() { if (canSave()) super.onBackPressed(); } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (android.R.id.home == item.getItemId()) { if (canSave()) finish(); } else if (R.id.menu_cancel == item.getItemId()) { mIsCancelled = true; finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/java/com/markadamson/taskerplugin/homeassistant/ui/activity/EditServerActivity.java ================================================ package com.markadamson.taskerplugin.homeassistant.ui.activity; import android.app.Activity; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.Toast; import com.markadamson.taskerplugin.homeassistant.R; import com.markadamson.taskerplugin.homeassistant.model.HAAPI; import com.markadamson.taskerplugin.homeassistant.model.HAAPIException; import com.markadamson.taskerplugin.homeassistant.model.HAAPIResult; import com.markadamson.taskerplugin.homeassistant.model.HAAPITask; import com.markadamson.taskerplugin.homeassistant.model.HAServer; import java.lang.ref.WeakReference; public class EditServerActivity extends AppCompatActivity { public static final int REQ_NEW_SERVER = 0; public static final int REQ_EDIT_SERVER = 1; private static final int[] MODE_TITLE = {R.string.add_a_server, R.string.edit_server}; public static final String EXT_HOST_NAME = "com.markadamson.taskerplugin.homeassistant.ui.activity.EditServerActivity.EXT_HOST_NAME"; public static final String EXT_REQUEST_CODE = "com.markadamson.taskerplugin.homeassistant.ui.activity.EditServerActivity.EXT_REQUEST CODE"; public static final String EXT_SERVER_NAME = "com.markadamson.taskerplugin.homeassistant.ui.activity.EditServerActivity.EXT_SERVER_NAME"; public static final String EXT_BASE_URL = "com.markadamson.taskerplugin.homeassistant.ui.activity.EditServerActivity.EXT_BASE_URL"; public static final String EXT_ACCESS_TOKEN = "com.markadamson.taskerplugin.homeassistant.ui.activity.EditServerActivity.EXT_ACCESS_TOKEN"; EditText etServerName, etBaseURL, etAccessToken; private abstract static class MyAPITask extends HAAPITask { WeakReference activityReference; MyAPITask(EditServerActivity context, HAServer server) { super(server); activityReference = new WeakReference<>(context); } } private static class TestServerTask extends MyAPITask { TestServerTask(EditServerActivity context, HAServer server) { super(context, server); } @Override protected Boolean doAPIInBackground(HAAPI api, Void... voids) throws HAAPIException { return api.testServer(); } @Override protected void onPostExecute(HAAPIResult result) { EditServerActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; if (result.getException() != null) { result.getException().printStackTrace(); Toast.makeText(activity, result.getException().getMessage(), Toast.LENGTH_SHORT).show(); return; } Toast.makeText(activity, result.getResult() ? R.string.connection_successful : R.string.connection_failed, Toast.LENGTH_SHORT).show(); } } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.edit_server); etServerName = findViewById(R.id.et_server_name); etBaseURL = findViewById(R.id.et_base_url); etAccessToken = findViewById(R.id.et_access_token); Intent i = getIntent(); if (i.hasExtra(EXT_SERVER_NAME)) etServerName.setText(i.getStringExtra(EXT_SERVER_NAME)); if (i.hasExtra(EXT_BASE_URL)) etBaseURL.setText(i.getStringExtra(EXT_BASE_URL)); if (i.hasExtra(EXT_ACCESS_TOKEN)) etAccessToken.setText(i.getStringExtra(EXT_ACCESS_TOKEN)); if (i.hasExtra(EXT_HOST_NAME)) setTitle(i.getCharSequenceExtra(EXT_HOST_NAME)); Resources r = getResources(); String mode = r.getString(MODE_TITLE[i.getIntExtra(EXT_REQUEST_CODE, REQ_EDIT_SERVER)]); getSupportActionBar().setSubtitle(r.getString(R.string.activity_subtitle, mode)); getSupportActionBar().setDisplayHomeAsUpEnabled(true); findViewById(R.id.btn_test_server).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { new TestServerTask(EditServerActivity.this, new HAServer( etServerName.getText().toString(), etBaseURL.getText().toString(), etAccessToken.getText().toString() )).execute(); } } ); } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } @Override public void onBackPressed() { setResult(Activity.RESULT_OK, new Intent() .putExtra(EXT_SERVER_NAME, etServerName.getText().toString()) .putExtra(EXT_BASE_URL, etBaseURL.getText().toString()) .putExtra(EXT_ACCESS_TOKEN, etAccessToken.getText().toString())); super.onBackPressed(); } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (android.R.id.home == item.getItemId()) { setResult(Activity.RESULT_OK, new Intent() .putExtra(EXT_SERVER_NAME, etServerName.getText().toString()) .putExtra(EXT_BASE_URL, etBaseURL.getText().toString()) .putExtra(EXT_ACCESS_TOKEN, etAccessToken.getText().toString())); finish(); } else if (R.id.menu_cancel == item.getItemId()) { finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: HomeAssistantTaskerPlugin/src/main/res/drawable/baseline_add_24.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/res/drawable/baseline_cancel_24.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/res/drawable/baseline_delete_24.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/res/drawable/baseline_edit_24.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/res/drawable/outline_label_24.xml ================================================ ================================================ FILE: HomeAssistantTaskerPlugin/src/main/res/layout/edit_get_state.xml ================================================