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:
*
*
{@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.
*
{@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.
*
{@link #isAsync()} is called to determine whether the remaining work should be performed on
* a
* background thread.
*
{@link #firePluginSetting(android.content.Context, android.os.Bundle)} is called to trigger
* the plug-in setting's action.
*
*
* 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
================================================
================================================
FILE: HomeAssistantTaskerPlugin/src/main/res/layout/edit_render_template.xml
================================================
================================================
FILE: HomeAssistantTaskerPlugin/src/main/res/layout/edit_server.xml
================================================
================================================
FILE: HomeAssistantTaskerPlugin/src/main/res/layout/main.xml
================================================
================================================
FILE: HomeAssistantTaskerPlugin/src/main/res/layout/select_server.xml
================================================
================================================
FILE: HomeAssistantTaskerPlugin/src/main/res/menu/menu.xml
================================================
================================================
FILE: HomeAssistantTaskerPlugin/src/main/res/values/strings.xml
================================================
Home Assistant Plug-in for TaskerHome AssistantHome Assistant - %1$sCancelAdd a ServerDelete ServerServer NameBase URLAccess TokenEdit ServerTest ServerConnection Successful!Connection Failed :(Test ServiceService Data (JSON, optional)Call ServiceGet StateVariableTest EntityServiceEntity IDAttributes (Optional)TemplateVariableTest TemplateRender TemplateSelect VariableDisable Battery Optimization?
Home Assistant Plug-In for Tasker is currently being optimized for battery. This means that
it may be killed by the system and stop working unexpectedly.\n\nTo prevent this, it\'s
recommended that you change the battery optimization setting for the plug-in to \'Don\'t
Optimize\'.\n\nWould you like to go to settings to change this now?
Are You Sure?
Are you sure you want to delete this server?\n\nIf it is in use by any other actions, those
actions will stop working until they are reconfigured to use a different server!
DELETE THE SERVER!
================================================
FILE: LICENSE
================================================
home-assistant-plugin-for-tasker
https://github.com/MarkAdamson/home-assistant-plugin-for-tasker
Copyright 2019 Mark Adamson
Original Author:
android-toast-setting-plugin-for-locale
https://github.com/twofortyfouram/android-toast-setting-plugin-for-locale
Copyright 2009-2017 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.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: README.md
================================================
# Home Assistant Plug-In for Tasker
This is a Tasker plug-in to allow calling services on and getting entity states from a Home Assistant installation.
## Requirements
- Android 4.0 (API 14)
- [Tasker](https://tasker.joaoapps.com/)
- [Home Assistant 0.78](https://www.home-assistant.io/)
## Usage
**PLEASE NOTE: If your device is on Android 6.0 or higher, you should disable Battery Optimization for this plugin (and Tasker) - otherwise you may find that it doesn't work properly!**
### Add A Server
- Generate a [Long-Lived Access Token](https://www.home-assistant.io/docs/authentication/) in Home Assistant.
- Create a new Task in Tasker.
- Add an Action: 'Plugin' > 'Home Assistant Plug-In for Tasker' to the Task.
- Tap the edit button by 'Configuration'.
- Tap the '+' button near the top right to add a new Home Assistant server.
- Enter the details for your Home Assistant server. The Base URL must include the protocol, and **not** a trailing backslash (eg: `https://my.home-assistant.com`).
- Test the server, then click 'Save'.
### Call A Service
- Create a new Task in Tasker.
- Add the Action 'Plugin' > 'Home Assistant Plug-In for Tasker' > 'Call Service' to the Task.
- Select a Server, or add a new one as above.
- Select a Service, and optionally enter Service Data in JSON format.
- Test the Service call, then click 'Save'.
### Get An Entity's State
- Create a new Task in Tasker.
- Add the Action 'Plugin' > 'Home Assistant Plug-In for Tasker' > 'Get State' to the Task.
- Select a Server, or add a new one as above.
- Select an Entity.
- Enter a Tasker variable name, including the leading `%`.
- Click 'Save'.
- **Make sure to set a timeout - this tells the action to await a result, otherwise the task continues instantly and the variable will not be populated!**
### Variables
Variables are supported in the following fields:
- 'Call Service' > 'Service'
- 'Call Service' > 'Service Data'
- 'Get State' > 'Entity ID'
Remember to always include the leading `%`
## To Do
- [x] Actions
- [x] Tasker Variables
================================================
FILE: build.gradle
================================================
apply plugin: 'com.github.ben-manes.versions'
buildscript {
repositories {
maven { url 'https://maven.google.com' }
jcenter()
}
dependencies {
classpath group:'com.android.tools.build', name:'gradle', version:"${ANDROID_GRADLE_PLUGIN_VERSION_MATCHER}"
classpath group:'com.github.ben-manes', name:'gradle-versions-plugin', version:"${GRADLE_VERSIONS_PLUGIN_VERSION_MATCHER}"
}
}
allprojects {
repositories {
maven { url 'https://maven.google.com' }
jcenter()
}
// Forces all dependencies to be resolved. Useful in continuous integration
// to populate the dependency cache.
// https://discuss.gradle.org/t/download-all-dependencies/6294
task resolveAllDependencies {
doLast {
configurations.all { it.resolve() }
}
}
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:all'
}
}
}
================================================
FILE: circle.yml
================================================
# Many CI systems start with the intention of having convention over configuration, and trying to make things look nice and easy to get started. This can fall apart pretty quickly, if the convention isn't flexible enough or isn't keeping up with the latest developments. That is what happened here, and this CI configuration is gross.
# There are a number of custom things happening in this script that deviate from CircleCI's Android documentation. The reasons are 1. performance and 2. portability between this and some other CI systems we use for other projects.
# To speed up builds, our own versions of gcloud and the Android SDK are downloaded to a separate directory and cached. This saves about 5 minutes per build. Updating the preinstalled components is a pain, because we don't want to cache directories that might get overwritten with a new container image later.
# Note: when forking this repo, environment variables GCLOUD_SERVICE_KEY_BASE_64, GCLOUD_PROJECT_ID, and GCLOUD_DEFAULT_BUCKET need to be defined for CI builds to work.
# Note that this doesn't work to prefix the path. CircleCI adds more crap to the beginning of PATH after we try to define it, including an old version of glcoud. So each many invocations are prefixed with an override of PATH.
# machine:
# environment:
# PATH: "/home/ubuntu/mybin/gcloud/bin:${PATH}"
general:
branches:
ignore:
- gh-pages
machine:
java:
version: oraclejdk8
environment:
ANDROID_HOME: "/home/ubuntu/mybin/android-sdk-linux"
dependencies:
pre:
- mkdir -p ~/flags
- mkdir -p ~/mybin
- ./tools/ci/android-sdk-setup.sh ${ANDROID_HOME} 3859397 ~/flags/android-sdk "tools" "platforms;android-25" "build-tools;25.0.3" "platform-tools" "docs"
- PATH="/home/ubuntu/mybin/gcloud/bin:${PATH}" && ./tools/ci/google-cloud-test-lab-setup.sh /home/ubuntu/mybin/gcloud 155.0.0 ~/flags/gcloud-setup ${GCLOUD_SERVICE_KEY_BASE_64} ${GCLOUD_PROJECT_ID}
- sudo apt-get update; sudo apt-get install parallel
override:
- ./gradlew resolveAllDependencies
cache_directories:
- ".gradle"
- "/home/ubuntu/flags"
- "/home/ubuntu/mybin"
- "/home/ubuntu/.android"
test:
override:
- ./gradlew assemble assembleDebugAndroidTest lint findbugs dependencyUpdates --profile
- PATH="/home/ubuntu/mybin/gcloud/bin:${PATH}" && parallel tools/ci/firebase-test-lab-test-module.sh toastPluginSettingApp ${GCLOUD_DEFAULT_BUCKET} {1} ':::' "model=Nexus4,version=19,orientation=portrait" "model=NexusLowRes,version=25,orientation=portrait"
# Convert the coverage.ec files into readable reports
- ./gradlew createDebugCoverageReport -x connectedDebugAndroidTest
- cp -r build/reports/profile $CIRCLE_ARTIFACTS
- cp -r build/dependencyUpdates $CIRCLE_ARTIFACTS
- cp -r toastPluginSettingApp/build/outputs $CIRCLE_ARTIFACTS
- cp -r toastPluginSettingApp/build/reports $CIRCLE_ARTIFACTS
- cp -r toastPluginSettingApp/build/outputs/androidTest-results/* $CIRCLE_TEST_REPORTS
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Mon Jul 08 14:35:42 BST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
================================================
FILE: gradle.properties
================================================
// Moar memory is required for in-process dex.
org.gradle.jvmargs=-Xmx2g
// Enable automatic download of Android SDK components.
android.builder.sdkDownload=true
// Android build details
ANDROID_BUILD_TOOLS_VERSION=28.0.3
ANDROID_MIN_SDK_VERSION=14
ANDROID_TARGET_SDK_VERSION=28
ANDROID_COMPILE_SDK_VERSION=26
// App version details
ANDROID_VERSION_CODE=13
ANDROID_VERSION_NAME=1.1.5
// Signing configuration
RELEASE_KEYSTORE_PATH=
RELEASE_KEYSTORE_PASSWORD=
RELEASE_KEY_ALIAS=
RELEASE_KEY_ALIAS_PASSWORD=
// Version matchers for dependencies
ANDROID_GRADLE_PLUGIN_VERSION_MATCHER=3.2.1
ANDROID_SUPPORT_VERSION_MATCHER=]27.0.0,28[
JCIP_ANNOTATION_VERSION_MATCHER=1.0
GRADLE_VERSIONS_PLUGIN_VERSION_MATCHER=0.14.0
TWOFORTYFOURAM_ANNOTATION_VERSION_MATCHER=[2.0.1,3.0[
TWOFORTYFOURAM_ASSERTION_VERSION_MATCHER=[1.1.1,2.0[
TWOFORTYFOURAM_PLUGIN_API_VERSION_MATCHER=[1.0.1,2.0.0[
TWOFORTYFOURAM_PLUGIN_CLIENT_SDK_VERSION_MATCHER=[4.0.2,5.0[
TWOFORTYFOURAM_SPACKLE_VERSION_MATCHER=[2.0.0,3.0[
TWOFORTYFOURAM_TEST_VERSION_MATCHER=[1.0.0,2.0[
// Toggle JaCoCo code coverage reports.
IS_COVERAGE_ENABLED=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle
================================================
include ':HomeAssistantTaskerPlugin'
================================================
FILE: tools/ci/android-sdk-license
================================================
8933bad161af4178b1185d1a37fbf41ea5269c55
================================================
FILE: tools/ci/android-sdk-setup.sh
================================================
#!/bin/bash
# Sets up the Android SDK.
# Marks a flag to remember whether the SDK has been installed previously
# and will bypass downloading if the flag exists. Note that if you change the version or
# components passed in, you should delete the flag and the installation so that the script will download
# the new version or components.
#
# The script expects several parameters in this order:
# 1. android home directory, e.g. ~/android-sdk
# 2. Android tools version, e.g. 3859397
# 3. flag path, e.g. ~/flags/android-sdk-setup
# 4. android sdk components, as used by `sdkmanager`. e.g. "platforms;android-25" "build-tools;25.0.2" "platform-tools" "docs" "extras;android;m2repository"
set +e
android_sdk_installation_dir="$1"
android_tools_version="$2"
android_sdk_flag_file="$3"
shift 3
android_sdk_components_to_install=("$@")
if ! test -f ${android_sdk_flag_file}; then
echo "Install Android SDK ${android_tools_version} to ${android_sdk_installation_dir} with ${android_sdk_components_to_install[@]}"
# Optionally download the SDK, as it might already exist in some environments.
if ! test -d $android_sdk_installation_dir; then
mkdir -p $android_sdk_installation_dir
curl -o $HOME/android-sdk-temp.zip "https://dl.google.com/android/repository/sdk-tools-linux-${android_tools_version}.zip"
unzip $HOME/android-sdk-temp.zip -d $android_sdk_installation_dir
rm $HOME/android-sdk-temp.zip
fi
mkdir -p ${android_sdk_installation_dir}/licenses/
cp tools/ci/android-sdk-license ${android_sdk_installation_dir}/licenses/android-sdk-license
if ! test -d ~/.android; then
mkdir ~/.android
fi
cp tools/ci/repositories.cfg ~/.android/repositories.cfg
for component in ${android_sdk_components_to_install[@]}; do
echo "Installing ${component}"
${android_sdk_installation_dir}/tools/bin/sdkmanager ${component}
done
touch ${android_sdk_flag_file}
fi
================================================
FILE: tools/ci/firebase-test-lab-test-module.sh
================================================
#!/bin/bash
# Executes tests on a specified module and extracts the results into the
# standard Android test directories. This uses the default Firebase Test Lab
# bucket with a custom results directory, in order to be able to copy the
# results back programmatically.
#
# This script only runs a test on a single device configuration at a time, so it
# does not support matrices. In order to create a matrix, invoke this script
# multiple times with additional arguments.
#
# When files are copied off of gcloud, they are given unique names to ensure
# that multiple invocations of this script will not cause naming collisions.
#
# This script expects the following parameters in order:
# 1. module tested
# 2. Default Firebase test lab bucket. You'll need to invoke the gcloud command manually and find the bucket name to pass for this parameter.
# 3. Firebase Test Lab device as a quoted string. E.g. "model=NexusLowRes,version=25,orientation=portrait"
# 4. Additional Firebase Test Lab arguments, as a quoted string.
# Example usage:
# ./tools/ci/firebase-test-lab-test-module.sh spackleLib test-lab-5fjjivmbih0ck-i7fd9i4fw50ym "model=NexusLowRes,version=25,orientation=portrait"
set -e
module="$1"
default_bucket="$2"
device="$3"
gcloud_args="$4"
uuid=`uuidgen`
# If a test case fails, exit code 10 will be returned. The script should continue if that occurs.
set +e
gcloud firebase test android run --results-dir=${uuid} tools/gcloud.yml:${module} --device ${device} ${gcloud_args}
gcloud_exit_code=$?
set -e
if [[ $gcloud_exit_code != 0 && $gcloud_exit_code != 10 ]]; then exit $gcloud_exit_code; fi
# Copy test results.
echo "Fetching test results"
mkdir -p ${module}/build/outputs/androidTest-results/connected/
gsutil -m cp -r "gs://${default_bucket}/${uuid}/**test_result_*.xml" "${module}/build/outputs/androidTest-results/connected/${uuid}-test-result.xml"
# Copy coverage.
echo "Fetching coverage"
mkdir -p ${module}/build/outputs/code-coverage/connected/
gsutil -m cp -r "gs://${default_bucket}/${uuid}/**/artifacts/coverage.ec" "${module}/build/outputs/code-coverage/connected/${uuid}-coverage.ec"
================================================
FILE: tools/ci/google-cloud-test-lab-setup.sh
================================================
#!/bin/bash
# Sets up gcloud. Marks a flag to remember whether the SDK has been installed previously
# and will bypass downloading if the flag exists. Note that if you change the version,
# you should delete the flag and the installation so that the script will download
# the new version. The script expects several parameters in this order:
# 1. gcloud installation directory — the directory must exist and should be empty (if the flag doesn't exist). e.g. ~/gcloud
# 2. gcloud version, e.g. 143.0.1
# 3. flag path, e.g. ~/flags/gcloud-setup
# 4. gcloud service key — base64 encoded gcloud service key
# 5. gcloud project name — name of the gcloud project.
# usage:
# google-cloud-test-lab-setup.sh ~/gcloud 143.0.1 ~/flags/gcloud-setup
set +e
gcloud_installation_dir="$1"
gcloud_version="$2"
gcloud_flag_file="$3"
gcloud_service_key_base64="$4"
gcloud_project="$5"
if ! test -f ${gcloud_flag_file}; then
echo "Install gcloud SDK ${gcloud_version} to ${gcloud_installation_dir}"
(cd ~ && curl "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${gcloud_version}-linux-x86_64.tar.gz" | tar zx && mv google-cloud-sdk ${gcloud_installation_dir})
${gcloud_installation_dir}/bin/gcloud --quiet components install beta
touch ${gcloud_flag_file}
fi
gcloud_service_account_key_path=~/gcloud_service_account_key_temp.json
echo $gcloud_service_key_base64 | base64 --decode > ${gcloud_service_account_key_path}
${gcloud_installation_dir}/bin/gcloud --quiet auth activate-service-account --key-file ${gcloud_service_account_key_path}
rm ${gcloud_service_account_key_path}
${gcloud_installation_dir}/bin/gcloud --quiet config set project $gcloud_project
================================================
FILE: tools/ci/repositories.cfg
================================================
### User Sources for Android SDK Manager
#Mon Jan 30 14:33:04 PST 2017
count=0
================================================
FILE: tools/findbugs/android-filter.xml
================================================
================================================
FILE: tools/gcloud.yml
================================================
toastPluginSettingApp:
type: instrumentation
app: toastPluginSettingApp/build/outputs/apk/toastPluginSettingApp-debug.apk
test: toastPluginSettingApp/build/outputs/apk/toastPluginSettingApp-debug-androidTest.apk
timeout: 10m
results-history-name: toastPluginSettingApp
include: [coverage-environment-variables]
coverage-environment-variables:
environment-variables:
coverage: true
coverageFile: "/sdcard/coverage.ec"
directories-to-pull: ["/sdcard"]