Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ClusterM/sony-headphones-control.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2019-08-28 14:54:29 +0300
committerAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2019-08-28 14:59:11 +0300
commitb4c440e2ced0db7755447b1952b3003e6dd21651 (patch)
treea6019a7c12df03015a4d2b41c4520afe0625b6c5
First commit
-rw-r--r--app/.gitignore1
-rw-r--r--app/build.gradle29
-rw-r--r--app/proguard-rules.pro21
-rw-r--r--app/src/main/AndroidManifest.xml40
-rw-r--r--app/src/main/java/com/clusterrr/sonyheadphonescontrol/MainActivity.java131
-rw-r--r--app/src/main/java/com/clusterrr/sonyheadphonescontrol/TaskerFireReceiver.java199
-rw-r--r--app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java1041
-rw-r--r--app/src/main/res/layout/activity_main.xml118
-rw-r--r--app/src/main/res/mipmap-hdpi/headphones.pngbin0 -> 4822 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/headphones.pngbin0 -> 2458 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/headphones.pngbin0 -> 6945 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/headphones.pngbin0 -> 13363 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/headphones.pngbin0 -> 18558 bytes
-rw-r--r--app/src/main/res/values/colors.xml6
-rw-r--r--app/src/main/res/values/strings.xml11
-rw-r--r--app/src/main/res/values/styles.xml11
-rw-r--r--build.gradle27
-rw-r--r--gradle.properties26
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 54329 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rw-r--r--gradlew172
-rw-r--r--gradlew.bat84
-rw-r--r--settings.gradle2
23 files changed, 1925 insertions, 0 deletions
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..3543521
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..f80ff87
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,29 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "com.clusterrr.sonyheadphonescontrol"
+ minSdkVersion 14
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ 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}"
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..6e7ffa9
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4f8a51d
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.clusterrr.sonyheadphonescontrol">
+
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+
+ <supports-screens
+ android:anyDensity="true"
+ android:largeScreens="true"
+ android:normalScreens="true"
+ android:smallScreens="true"
+ android:xlargeScreens="true" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/headphones"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <receiver
+ android:name=".TaskerFireReceiver"
+ android:exported="true">
+
+ <!-- this Intent filter allows the plug-in to discovered by Locale -->
+ <intent-filter>
+ <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
+ </intent-filter>
+ </receiver>
+
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/app/src/main/java/com/clusterrr/sonyheadphonescontrol/MainActivity.java b/app/src/main/java/com/clusterrr/sonyheadphonescontrol/MainActivity.java
new file mode 100644
index 0000000..ceb4e16
--- /dev/null
+++ b/app/src/main/java/com/clusterrr/sonyheadphonescontrol/MainActivity.java
@@ -0,0 +1,131 @@
+package com.clusterrr.sonyheadphonescontrol;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.RadioButton;
+import android.widget.SeekBar;
+import android.widget.Switch;
+
+import com.twofortyfouram.spackle.bundle.BundleScrubber;
+
+public class MainActivity extends AppCompatActivity implements View.OnClickListener, SeekBar.OnSeekBarChangeListener, CompoundButton.OnCheckedChangeListener {
+ int mode = 0;
+ int volume = 20;
+ boolean voice = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Intent intent = getIntent();
+ Bundle bundle = null;
+ if (intent != null) {
+ BundleScrubber.scrub(intent);
+ bundle = intent.getBundleExtra(TaskerFireReceiver.EXTRA_BUNDLE);
+ }
+ if (bundle != null) {
+ BundleScrubber.scrub(bundle);
+ int mode = bundle.getInt(TaskerFireReceiver.EXTRA_STRING_MODE, 0);
+ switch (mode) {
+ case 0:
+ ((RadioButton) findViewById(R.id.radioButtonDisable)).setChecked(true);
+ break;
+ case 1:
+ ((RadioButton) findViewById(R.id.radioButtonNoiseCancelling)).setChecked(true);
+ break;
+ case 2:
+ ((RadioButton) findViewById(R.id.radioButtonWindCancelling)).setChecked(true);
+ break;
+ case 3:
+ int volume = bundle.getInt(TaskerFireReceiver.EXTRA_STRING_VOLUME, 20);
+ boolean voice = bundle.getBoolean(TaskerFireReceiver.EXTRA_STRING_VOICE, false);
+ ((RadioButton) findViewById(R.id.radioButtonAmbientSound)).setChecked(true);
+ ((SeekBar) findViewById(R.id.seekBarVolume)).setProgress(volume);
+ ((Switch) findViewById(R.id.switchVoiceOptimized)).setChecked(voice);
+ }
+ }
+
+ ((RadioButton) findViewById(R.id.radioButtonDisable)).setOnCheckedChangeListener(this);
+ ((RadioButton) findViewById(R.id.radioButtonNoiseCancelling)).setOnCheckedChangeListener(this);
+ ((RadioButton) findViewById(R.id.radioButtonWindCancelling)).setOnCheckedChangeListener(this);
+ ((RadioButton) findViewById(R.id.radioButtonAmbientSound)).setOnCheckedChangeListener(this);
+ ((SeekBar) findViewById(R.id.seekBarVolume)).setOnSeekBarChangeListener(this);
+ ((Switch) findViewById(R.id.switchVoiceOptimized)).setOnCheckedChangeListener(this);
+
+ ((Button) findViewById(R.id.buttonTest)).setOnClickListener(this);
+ ((Button) findViewById(R.id.buttonSave)).setOnClickListener(this);
+
+ saveSettings();
+ }
+
+ void saveSettings() {
+ String blurb = "";
+
+ if (((RadioButton) findViewById(R.id.radioButtonDisable)).isChecked()) {
+ mode = 0;
+ blurb = ((RadioButton) findViewById(R.id.radioButtonDisable)).getText().toString();
+ } else if (((RadioButton) findViewById(R.id.radioButtonNoiseCancelling)).isChecked()) {
+ mode = 1;
+ blurb = ((RadioButton) findViewById(R.id.radioButtonNoiseCancelling)).getText().toString();
+ } else if (((RadioButton) findViewById(R.id.radioButtonWindCancelling)).isChecked()) {
+ mode = 2;
+ blurb = ((RadioButton) findViewById(R.id.radioButtonWindCancelling)).getText().toString();
+ } else if (((RadioButton) findViewById(R.id.radioButtonAmbientSound)).isChecked()) {
+ mode = 3;
+ volume = ((SeekBar) findViewById(R.id.seekBarVolume)).getProgress();
+ voice = ((Switch) findViewById(R.id.switchVoiceOptimized)).isChecked();
+ blurb = ((RadioButton) findViewById(R.id.radioButtonAmbientSound)).getText().toString() +
+ ", volume=" + volume + (voice ? ", voice optimized" : "");
+ }
+
+ Intent resultIntent = new Intent();
+ final Bundle resultBundle = new Bundle();
+ resultBundle.putInt(TaskerFireReceiver.EXTRA_STRING_MODE, mode);
+ resultBundle.putInt(TaskerFireReceiver.EXTRA_STRING_VOLUME, volume);
+ resultBundle.putBoolean(TaskerFireReceiver.EXTRA_STRING_VOICE, voice);
+ resultIntent.putExtra(TaskerFireReceiver.EXTRA_BUNDLE, resultBundle);
+ resultIntent.putExtra(TaskerFireReceiver.EXTRA_STRING_BLURB, blurb);
+ setResult(RESULT_OK, resultIntent);
+
+ ((SeekBar) findViewById(R.id.seekBarVolume)).setEnabled(mode == 3);
+ ((Switch) findViewById(R.id.switchVoiceOptimized)).setEnabled(mode == 3);
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.buttonTest:
+ TaskerFireReceiver.execute(this, null, mode, volume, voice);
+ break;
+ case R.id.buttonSave:
+ finish();
+ break;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ saveSettings();
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ saveSettings();
+ }
+}
diff --git a/app/src/main/java/com/clusterrr/sonyheadphonescontrol/TaskerFireReceiver.java b/app/src/main/java/com/clusterrr/sonyheadphonescontrol/TaskerFireReceiver.java
new file mode 100644
index 0000000..642b145
--- /dev/null
+++ b/app/src/main/java/com/clusterrr/sonyheadphonescontrol/TaskerFireReceiver.java
@@ -0,0 +1,199 @@
+package com.clusterrr.sonyheadphonescontrol;
+
+/*
+ * Copyright 2013 two forty four a.m. LLC <http://www.twofortyfouram.com>
+ *
+ * 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.
+ */
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothSocket;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.twofortyfouram.spackle.bundle.BundleScrubber;
+
+import net.dinglisch.android.tasker.TaskerPlugin;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+public final class TaskerFireReceiver extends BroadcastReceiver {
+ public static final String TAG = "SonyHeadphonesControl";
+ public static final UUID uuid = UUID.fromString("96cc203e-5068-46ad-b32d-e316f5e069ba");
+ public static final String ACTION_FIRE_SETTING = "com.twofortyfouram.locale.intent.action.FIRE_SETTING"; //$NON-NLS-1$
+ public static final String EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"; //$NON-NLS-1$
+ public static final String EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"; //$NON-NLS-1$
+ public static final String EXTRA_STRING_MODE = "mode";
+ public static final String EXTRA_STRING_VOLUME = "volume";
+ public static final String EXTRA_STRING_VOICE = "voice";
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (!ACTION_FIRE_SETTING.equals(intent.getAction()))
+ return;
+
+ final Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE);
+ BundleScrubber.scrub(bundle);
+
+ boolean voice = false;
+ int volume = 0;
+
+ int mode = bundle.getInt(EXTRA_STRING_MODE, 0);
+ volume = bundle.getInt(EXTRA_STRING_VOLUME, 20);
+ voice = bundle.getBoolean(EXTRA_STRING_VOICE, false);
+
+ execute(context, intent, mode, volume, voice);
+ }
+
+ public static void execute(Context context, Intent intent, int mode, int volume, boolean voice) {
+
+ boolean enabled = false;
+ int noiseCancelling = 0;
+ switch (mode) {
+ case 0:
+ enabled = false;
+ break;
+ case 1:
+ enabled = true;
+ noiseCancelling = 2;
+ break;
+ case 2:
+ enabled = true;
+ noiseCancelling = 1;
+ break;
+ case 3:
+ enabled = true;
+ noiseCancelling = 0;
+ }
+
+ try {
+ if (setAmbientSound(context, enabled, noiseCancelling, volume, voice)) {
+ if (intent != null)
+ TaskerPlugin.Setting.signalFinish(context, intent, TaskerPlugin.Setting.RESULT_CODE_OK, null);
+ else
+ Toast.makeText(context, "OK", Toast.LENGTH_SHORT).show();
+ } else {
+ final String message = "Sony Headset is not found";
+ if (intent != null) {
+ Bundle vars = new Bundle();
+ vars.putString(TaskerPlugin.Setting.VARNAME_ERROR_MESSAGE, message);
+ TaskerPlugin.Setting.signalFinish(context, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, vars);
+ } else {
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ final String message = "IO error (another application using a headset?)";
+ if (intent != null) {
+ Bundle vars = new Bundle();
+ vars.putString(TaskerPlugin.Setting.VARNAME_ERROR_MESSAGE, message);
+ TaskerPlugin.Setting.signalFinish(context, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, vars);
+ } else {
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ static boolean setAmbientSound(Context context, boolean enabled, int noiseCancelling, int volume, boolean voice) throws IOException, InterruptedException {
+ return sendData(context, new byte[]{0x00, 0x00, 0x00, 0x08, 0x68, 0x02, (byte) (enabled ? 0x10 : 0x00), 0x02, (byte) (noiseCancelling), 0x01, (byte) (voice ? 1 : 0), (byte) volume});
+ }
+
+ static boolean sendData(Context context, byte[] data) throws IOException, InterruptedException {
+ BluetoothDevice headset = null;
+ BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+ BluetoothAdapter adapter = bluetoothManager.getAdapter();
+
+ Set<BluetoothDevice> devices = adapter.getBondedDevices();
+ for (BluetoothDevice device : devices) {
+ ParcelUuid[] uuids = device.getUuids();
+ for (ParcelUuid u : uuids) {
+ if (u.toString().equals(uuid.toString())) {
+ headset = device;
+ break;
+ }
+ }
+ if (headset != null)
+ break;
+ }
+ if (headset == null) {
+ Log.e(TAG, "Headset not found");
+ return false;
+ } else {
+ Log.d(TAG, "Headset found: " + headset.getAddress() + " " + headset.getName());
+ }
+
+ BluetoothSocket socket = headset.createRfcommSocketToServiceRecord(uuid);
+ try {
+ Log.i(TAG, "Socket connected: " + socket.isConnected());
+ socket.connect();
+ Log.i(TAG, "Socket connected: " + socket.isConnected());
+
+ byte[] packet = new byte[data.length + 2];
+ packet[0] = 0x0c;
+ packet[1] = 0;
+ for (int j = 0; j < data.length; j++) {
+ packet[j + 2] = data[j];
+ }
+ sendPacket(socket, packet);
+ packet[1] = 1;
+ sendPacket(socket, packet);
+
+ return true;
+ } finally {
+ socket.close();
+ }
+ }
+
+ static void sendPacket(BluetoothSocket socket, byte[] data) throws IOException, InterruptedException {
+ OutputStream o = socket.getOutputStream();
+ InputStream i = socket.getInputStream();
+ byte[] packet = new byte[data.length + 3];
+ packet[0] = 0x3e;
+ packet[packet.length - 1] = 0x3c;
+ byte crc = 0;
+ for (int j = 0; j < data.length; j++) {
+ crc += data[j];
+ packet[j + 1] = data[j];
+ }
+ packet[packet.length - 2] = crc;
+ o.write(packet);
+
+ byte[] buffer = new byte[256];
+ Date date = new Date();
+ while (new Date().getTime() - date.getTime() < 200) {
+ if (i.available() > 0) {
+ int r = i.read(buffer);
+ StringBuilder sb = new StringBuilder();
+ for (int j = 0; j < r; j++) {
+ sb.append(String.format(" %02x", buffer[j]));
+ }
+ Log.i(TAG, "Read: " + r + " bytes:" + sb);
+ break;
+ }
+ Thread.sleep(50);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java b/app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java
new file mode 100644
index 0000000..fb00be1
--- /dev/null
+++ b/app/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java
@@ -0,0 +1,1041 @@
+//package com.yourcompany.yourcondition;
+//package com.yourcompany.yoursetting;
+package net.dinglisch.android.tasker;
+
+// Constants and functions for Tasker *extensions* to the plugin protocol
+// See Also: http://tasker.dinglisch.net/plugins.html
+
+// Release Notes
+
+// v1.1 20140202
+// added function variableNameValid()
+// fixed some javadoc entries (thanks to David Stone)
+
+// v1.2 20140211
+// added ACTION_EDIT_EVENT
+
+// v1.3 20140227
+// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
+// requestTimeoutMS(): added range check
+
+// v1.4 20140516
+// support for data pass through in REQUEST_QUERY intent
+// some javadoc entries fixed (thanks again David :-))
+
+// v1.5 20141120
+// added RESULT_CODE_FAILED_PLUGIN_FIRST
+// added Setting.VARNAME_ERROR_MESSAGE
+
+// v1.6 20150213
+// added Setting.getHintTimeoutMS()
+// added Host.addHintTimeoutMS()
+
+// v1.7 20160619
+// null check for getCallingActivity() in hostSupportsOnFireVariableReplacement( Activity editActivity )
+
+// v1.8 20161002
+// added hostSupportsKeyEncoding(), setKeyEncoding() and Host.getKeysWithEncoding()
+
+import java.net.URISyntaxException;
+import java.security.SecureRandom;
+import java.util.regex.Pattern;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+public class TaskerPlugin {
+
+ private final static String TAG = "TaskerPlugin";
+
+ private final static String BASE_KEY = "net.dinglisch.android.tasker";
+
+ private final static String EXTRAS_PREFIX = BASE_KEY + ".extras.";
+
+ private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80;
+
+ public final static String VARIABLE_PREFIX = "%";
+
+ // when generating non-repeating integers, look this far back for repeats
+ // see getPositiveNonRepeatingRandomInteger()
+ private final static int RANDOM_HISTORY_SIZE = 100;
+
+ /**
+ * Action that the EditActivity for an event plugin should be launched by
+ */
+ public final static String ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT";
+
+ private final static String VARIABLE_NAME_START_EXPRESSION = "[\\w&&[^_]]";
+ private final static String VARIABLE_NAME_MID_EXPRESSION = "[\\w0-9]+";
+ private final static String VARIABLE_NAME_END_EXPRESSION = "[\\w0-9&&[^_]]";
+
+ public final static String VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION =
+ VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION
+ ;
+
+ public final static String VARIABLE_NAME_MATCH_EXPRESSION =
+ VARIABLE_PREFIX + "+" +
+ VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION
+ ;
+
+ private static Pattern VARIABLE_NAME_MATCH_PATTERN = null;
+
+ /**
+ * @see #addVariableBundle(Bundle, Bundle)
+ * @see Host#getVariablesBundle(Bundle)
+ */
+ private final static String EXTRA_VARIABLES_BUNDLE = EXTRAS_PREFIX + "VARIABLES";
+
+ /**
+ * Host capabilities, passed to plugin with edit intents
+ */
+ private final static String EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES";
+
+ /**
+ * @see Setting#hostSupportsVariableReturn(Bundle)
+ */
+ public final static int EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2;
+
+ /**
+ * @see Condition#hostSupportsVariableReturn(Bundle)
+ */
+ public final static int EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4;
+
+ /**
+ * @see Setting#hostSupportsOnFireVariableReplacement(Bundle)
+ */
+ public final static int EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8;
+
+ /**
+ * @see Setting#hostSupportsVariableReturn(Bundle)
+ */
+ private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16;
+
+ public final static int EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32;
+
+ public final static int EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64;
+
+ public final static int EXTRA_HOST_CAPABILITY_ENCODING_JSON = 128;
+
+ public final static int EXTRA_HOST_CAPABILITY_ALL =
+ EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES |
+ EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES |
+ EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT |
+ EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES|
+ EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION |
+ EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH |
+ EXTRA_HOST_CAPABILITY_ENCODING_JSON
+ ;
+
+ /**
+ * Possible encodings of text in bundle values
+ *
+ * @see #setKeyEncoding(Bundle,String[],Encoding)
+ */
+ public enum Encoding { JSON };
+
+ private final static String BUNDLE_KEY_ENCODING_JSON_KEYS = BASE_KEY + ".JSON_ENCODED_KEYS";
+
+ public static boolean hostSupportsKeyEncoding( Bundle extrasFromHost, Encoding encoding ) {
+ switch ( encoding ) {
+ case JSON:
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_ENCODING_JSON );
+ default:
+ return false;
+ }
+ }
+
+ /**
+ *
+ * Miscellaneous operational hints going one way or the other
+ * @see Setting#hostSupportsVariableReturn(Bundle)
+ */
+
+ private final static String EXTRA_HINTS_BUNDLE = EXTRAS_PREFIX + "HINTS";
+
+ private final static String BUNDLE_KEY_HINT_PREFIX = ".hints.";
+
+ private final static String BUNDLE_KEY_HINT_TIMEOUT_MS = BUNDLE_KEY_HINT_PREFIX + "TIMEOUT";
+
+ /**
+ *
+ * @see #hostSupportsRelevantVariables(Bundle)
+ * @see #addRelevantVariableList(Intent, String[])
+ * @see #getRelevantVariableList(Bundle)
+ */
+ private final static String BUNDLE_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES";
+
+
+ public static boolean hostSupportsRelevantVariables( Bundle extrasFromHost ) {
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES );
+ }
+
+ /**
+ * Specifies to host which variables might be used by the plugin.
+ *
+ * Used in EditActivity, before setResult().
+ *
+ * @param intentToHost the intent being returned to the host
+ * @param variableNames array of relevant variable names
+ */
+ public static void addRelevantVariableList( Intent intentToHost, String [] variableNames ) {
+ intentToHost.putExtra( BUNDLE_KEY_RELEVANT_VARIABLES, variableNames );
+ }
+
+ /**
+ * Validate a variable name.
+ *
+ * The basic requirement for variables from a plugin is that they must be all lower-case.
+ *
+ * @param varName name to check
+ */
+ public static boolean variableNameValid( String varName ) {
+
+ boolean validFlag = false;
+
+ if ( varName == null )
+ Log.d( TAG, "variableNameValid: null name" );
+ else {
+ if ( VARIABLE_NAME_MATCH_PATTERN == null )
+ VARIABLE_NAME_MATCH_PATTERN = Pattern.compile( VARIABLE_NAME_MATCH_EXPRESSION, 0 );
+
+ if ( VARIABLE_NAME_MATCH_PATTERN.matcher( varName ).matches() ) {
+
+ if ( variableNameIsLocal( varName ) )
+ validFlag = true;
+ else
+ Log.d( TAG, "variableNameValid: name not local: " + varName );
+ }
+ else
+ Log.d( TAG, "variableNameValid: invalid name: " + varName );
+ }
+
+ return validFlag;
+ }
+
+ /**
+ * Allows the plugin/host to indicate to each other a set of variables which they are referencing.
+ * The host may use this to e.g. show a variable selection list in it's UI.
+ * The host should use this if it previously indicated to the plugin that it supports relevant vars
+ *
+ * @param fromHostIntentExtras usually from getIntent().getExtras()
+ * @return variableNames an array of relevant variable names
+ */
+ public static String [] getRelevantVariableList( Bundle fromHostIntentExtras ) {
+
+ String [] relevantVars = (String []) getBundleValueSafe( fromHostIntentExtras, BUNDLE_KEY_RELEVANT_VARIABLES, String [].class, "getRelevantVariableList" );
+
+ if ( relevantVars == null )
+ relevantVars = new String [0];
+
+ return relevantVars;
+ }
+
+ /**
+ * Used by: plugin QueryReceiver, FireReceiver
+ *
+ * Add a bundle of variable name/value pairs.
+ *
+ * Names must be valid Tasker local variable names.
+ * Values must be String, String [] or ArrayList<String>
+ * Null values cause deletion of possible already-existing variables
+ * A null value where the variable does not already exist results in attempted deletion
+ * of any existing array indices (%arr1, %arr2 etc)
+ *
+ * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras())
+ * @param variables the variables to send
+ * @see Setting#hostSupportsVariableReturn(Bundle)
+ * @see #variableNameValid(String)
+ */
+ public static void addVariableBundle( Bundle resultExtras, Bundle variables ) {
+ resultExtras.putBundle( EXTRA_VARIABLES_BUNDLE, variables );
+ }
+
+ /**
+ * Used by: plugin EditActivity
+ *
+ * Specify the encoding for a set of bundle keys.
+ *
+ * This is completely optional and currently only necessary if using Setting#setVariableReplaceKeys
+ * where the corresponding values of some of the keys specified are JSON encoded.
+ *
+ * @param resultBundleToHost the bundle being returned to the host
+ * @param keys the keys being returned to the host which are encoded in some way
+ * @param encoding the encoding of the values corresponding to the specified keys
+ * @see #setVariableReplaceKeys(Bundle,String[])
+ * @see #hostSupportsKeyEncoding(Bundle, Encoding)
+ */
+ public static void setKeyEncoding( Bundle resultBundleToHost, String [] keys, Encoding encoding ) {
+ if ( Encoding.JSON.equals( encoding ) )
+ addStringArrayToBundleAsString(
+ keys, resultBundleToHost, BUNDLE_KEY_ENCODING_JSON_KEYS, "setValueEncoding"
+ );
+ else
+ Log.e( TAG, "unknown encoding: " + encoding );
+ }
+
+ // ----------------------------- SETTING PLUGIN ONLY --------------------------------- //
+
+ public static class Setting {
+
+ /**
+ * Variable name into which a description of any error that occurred can be placed
+ * for the user to process.
+ *
+ * Should *only* be set when the BroadcastReceiver result code indicates a failure.
+ *
+ * Note that the user needs to have configured the task to continue after failure of the plugin
+ * action otherwise they will not be able to make use of the error message.
+ *
+ * For use with #addRelevantVariableList(Intent, String[]) and #addVariableBundle(Bundle, Bundle)
+ *
+ */
+ public final static String VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg";
+
+ /**
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ */
+ private final static String BUNDLE_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS";
+
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+ private final static String EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT";
+
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+
+ public final static int REQUESTED_TIMEOUT_MS_NONE = 0;
+
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+
+ public final static int REQUESTED_TIMEOUT_MS_MAX = 3599000;
+
+ /**
+ * @see #requestTimeoutMS(android.content.Intent, int)
+ */
+
+ public final static int REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000;
+
+ /**
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see #addCompletionIntent(Intent, Intent,ComponentName, boolean)
+ */
+ private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT";
+
+ /**
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see Host#getSettingResultCode(Intent)
+ */
+ public final static String EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE";
+
+ /**
+ *
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see #addCompletionIntent(Intent, Intent,ComponentName, boolean)
+ */
+ public final static String EXTRA_CALL_SERVICE_PACKAGE = BASE_KEY + ".EXTRA_CALL_SERVICE_PACKAGE";
+ public final static String EXTRA_CALL_SERVICE = BASE_KEY + ".EXTRA_CALL_SERVICE";
+ public final static String EXTRA_CALL_SERVICE_FOREGROUND = BASE_KEY + ".EXTRA_CALL_SERVICE_FOREGROUND";
+ /**
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ * @see Host#getSettingResultCode(Intent)
+ */
+
+ public final static int RESULT_CODE_OK = Activity.RESULT_OK;
+ public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
+ public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
+ public final static int RESULT_CODE_PENDING = Activity.RESULT_FIRST_USER + 2;
+ public final static int RESULT_CODE_UNKNOWN = Activity.RESULT_FIRST_USER + 3;
+
+ /**
+ * If a plugin wants to define it's own error codes, start numbering them here.
+ * The code will be placed in an error variable (%err in the case of Tasker) for
+ * the user to process after the plugin action.
+ */
+
+ public final static int RESULT_CODE_FAILED_PLUGIN_FIRST = Activity.RESULT_FIRST_USER + 9;
+
+ /**
+ * Used by: plugin EditActivity.
+ *
+ * Indicates to plugin that host will replace variables in specified bundle keys.
+ *
+ * Replacement takes place every time the setting is fired, before the bundle is
+ * passed to the plugin FireReceiver.
+ *
+ * @param extrasFromHost intent extras from the intent received by the edit activity
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ */
+ public static boolean hostSupportsOnFireVariableReplacement( Bundle extrasFromHost ) {
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT );
+ }
+
+ /**
+ * Used by: plugin EditActivity.
+ *
+ * Description as above.
+ *
+ * This version also includes backwards compatibility with pre 4.2 Tasker versions.
+ * At some point this function will be deprecated.
+ *
+ * @param editActivity the plugin edit activity, needed to test calling Tasker version
+ * @see #setVariableReplaceKeys(Bundle, String[])
+ */
+
+ public static boolean hostSupportsOnFireVariableReplacement( Activity editActivity ) {
+
+ boolean supportedFlag = hostSupportsOnFireVariableReplacement( editActivity.getIntent().getExtras() );
+
+ if ( ! supportedFlag ) {
+
+ ComponentName callingActivity = editActivity.getCallingActivity();
+
+ if ( callingActivity == null )
+ Log.w( TAG, "hostSupportsOnFireVariableReplacement: null callingActivity, defaulting to false" );
+ else {
+ String callerPackage = callingActivity.getPackageName();
+
+ // Tasker only supporteed this from 1.0.10
+ supportedFlag =
+ ( callerPackage.startsWith( BASE_KEY ) ) &&
+ ( getPackageVersionCode( editActivity.getPackageManager(), callerPackage ) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION )
+ ;
+ }
+ }
+
+ return supportedFlag;
+ }
+
+ public static boolean hostSupportsSynchronousExecution( Bundle extrasFromHost ) {
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION );
+ }
+
+ /**
+ * Request the host to wait the specified number of milliseconds before continuing.
+ * Note that the host may choose to ignore the request.
+ *
+ * Maximum value is REQUESTED_TIMEOUT_MS_MAX.
+ * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting
+ * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for
+ * a result).
+ *
+ * Used in EditActivity, before setResult().
+ *
+ * @param intentToHost the intent being returned to the host
+ * @param timeoutMS
+ */
+ public static void requestTimeoutMS( Intent intentToHost, int timeoutMS ) {
+ if ( timeoutMS < 0 )
+ Log.w( TAG, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")" );
+ else {
+ if (
+ ( timeoutMS > REQUESTED_TIMEOUT_MS_MAX ) &&
+ ( timeoutMS != REQUESTED_TIMEOUT_MS_NEVER )
+ ) {
+ Log.w( TAG, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")" );
+ timeoutMS = REQUESTED_TIMEOUT_MS_MAX;
+ }
+ intentToHost.putExtra( EXTRA_REQUESTED_TIMEOUT, timeoutMS );
+ }
+ }
+
+ /**
+ * Used by: plugin EditActivity
+ *
+ * Indicates to host which bundle keys should be replaced.
+ *
+ * @param resultBundleToHost the bundle being returned to the host
+ * @param listOfKeyNames which bundle keys to replace variables in when setting fires
+ * @see #hostSupportsOnFireVariableReplacement(Bundle)
+ * @see #setKeyEncoding(Bundle,String[],Encoding)
+ */
+ public static void setVariableReplaceKeys( Bundle resultBundleToHost, String [] listOfKeyNames ) {
+ addStringArrayToBundleAsString(
+ listOfKeyNames, resultBundleToHost, BUNDLE_KEY_VARIABLE_REPLACE_STRINGS,
+ "setVariableReplaceKeys"
+ );
+ }
+
+ /**
+ * Used by: plugin FireReceiver
+ *
+ * Indicates to plugin whether the host will process variables which it passes back
+ *
+ * @param extrasFromHost intent extras from the intent received by the FireReceiver
+ * @see #signalFinish(Context, Intent, int, Bundle)
+ */
+ public static boolean hostSupportsVariableReturn( Bundle extrasFromHost ) {
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES );
+ }
+
+ /**
+ * Used by: plugin FireReceiver
+ *
+ * Tell the host that the plugin has finished execution.
+ *
+ * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive().
+ *
+ * @param originalFireIntent the intent received from the host (via onReceive())
+ * @param resultCode level of success in performing the settings
+ * @param vars any variables that the plugin wants to set in the host
+ * @see #hostSupportsSynchronousExecution(Bundle)
+ */
+ public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars ) {
+
+ String errorPrefix = "signalFinish: ";
+
+ boolean okFlag = false;
+
+ String completionIntentString = (String) getExtraValueSafe( originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish" );
+
+ if ( completionIntentString != null ) {
+
+ Uri completionIntentUri = null;
+ try {
+ completionIntentUri = Uri.parse( completionIntentString );
+ }
+ // should only throw NullPointer but don't particularly trust it
+ catch ( Exception e ) {
+ Log.w( TAG, errorPrefix + "couldn't parse " + completionIntentString );
+ }
+
+ if ( completionIntentUri != null ) {
+ try {
+ Intent completionIntent = Intent.parseUri( completionIntentString, Intent.URI_INTENT_SCHEME );
+
+ completionIntent.putExtra( EXTRA_RESULT_CODE, resultCode );
+
+ if ( vars != null )
+ completionIntent.putExtra( EXTRA_VARIABLES_BUNDLE, vars );
+
+ String callServicePackage = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_PACKAGE, String.class, "signalFinish");
+ String callService = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE, String.class, "signalFinish");
+ Boolean foreground = (Boolean) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_FOREGROUND, Boolean.class, "signalFinish");
+ if (callServicePackage != null && callService != null && foreground != null) {
+ completionIntent.setComponent(new ComponentName(callServicePackage, callService));
+ if (foreground && android.os.Build.VERSION.SDK_INT >= 26) {
+ context.startForegroundService(completionIntent);
+ } else {
+ context.startService(completionIntent);
+ }
+ } else {
+ context.sendBroadcast(completionIntent);
+ }
+
+ okFlag = true;
+ }
+ catch ( URISyntaxException e ) {
+ Log.w( TAG, errorPrefix + "bad URI: " + completionIntentUri );
+ }
+ }
+ }
+
+ return okFlag;
+ }
+
+ /**
+ * Check for a hint on the timeout value the host is using.
+ * Used by: plugin FireReceiver.
+ * Requires Tasker 4.7+
+ *
+ * @param extrasFromHost intent extras from the intent received by the FireReceiver
+ * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available.
+ *
+ * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
+ */
+ public static int getHintTimeoutMS( Bundle extrasFromHost ) {
+
+ int timeoutMS = -1;
+
+ Bundle hintsBundle = (Bundle) TaskerPlugin.getBundleValueSafe( extrasFromHost, EXTRA_HINTS_BUNDLE, Bundle.class, "getHintTimeoutMS" );
+
+ if ( hintsBundle != null ) {
+
+ Integer val = (Integer) getBundleValueSafe( hintsBundle, BUNDLE_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS" );
+
+ if ( val != null )
+ timeoutMS = val;
+ }
+
+ return timeoutMS;
+ }
+ }
+
+ // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- //
+
+ public static class Condition {
+
+ /**
+ * @see #getResultReceiver(Intent)
+ */
+ public final static String EXTRA_RESULT_RECEIVER = BASE_KEY + ".EXTRA_RESULT_RECEIVER";
+ /**
+ * Used by: plugin QueryReceiver
+ *
+ * Indicates to plugin whether the host will process variables which it passes back
+ *
+ * @param extrasFromHost intent extras from the intent received by the QueryReceiver
+ * @see #addVariableBundle(Bundle, Bundle)
+ */
+ public static boolean hostSupportsVariableReturn( Bundle extrasFromHost ) {
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES );
+ }
+
+ public static ResultReceiver getResultReceiver(Intent intentFromHost) {
+ if (intentFromHost == null) {
+ return null;
+ }
+ return (ResultReceiver) getExtraValueSafe(intentFromHost, EXTRA_RESULT_RECEIVER, ResultReceiver.class, "getResultReceiver");
+
+ }
+ }
+
+ // ----------------------------- EVENT PLUGIN ONLY --------------------------------- //
+
+ public static class Event {
+
+ public final static String PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID";
+
+ private final static String EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA";
+
+ /**
+ * @param extrasFromHost intent extras from the intent received by the QueryReceiver
+ * @see #addPassThroughData(Intent, Bundle)
+ */
+ public static boolean hostSupportsRequestQueryDataPassThrough( Bundle extrasFromHost ) {
+ return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH );
+ }
+
+ /**
+ * Specify a bundle of data (probably representing whatever change happened in the condition)
+ * which will be included in the QUERY_CONDITION broadcast sent by the host for each
+ * event instance of the plugin.
+ *
+ * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
+ * with the REQUEST_QUERY that caused it.
+ *
+ * Note that for security reasons it is advisable to also store a message ID with the bundle
+ * which can be compared to known IDs on receipt. The host cannot validate the source of
+ * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible.
+ * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's
+ * own ID to the pass through bundle.
+ *
+ * Note also that there are several situations where REQUEST_QUERY will not result in a
+ * QUERY_CONDITION intent (e.g. event throttling by the host), so plugin-local data
+ * indexed with a message ID needs to be timestamped and eventually timed-out.
+ *
+ * This function can be called multiple times, each time all keys in data will be added to
+ * that of previous calls.
+ *
+ * @param requestQueryIntent intent being sent to the host
+ * @param data the data to be passed-through
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #retrievePassThroughData(Intent)
+ * @see #addPassThroughMessageID
+ *
+ */
+ public static void addPassThroughData( Intent requestQueryIntent, Bundle data ) {
+
+ Bundle passThroughBundle = retrieveOrCreatePassThroughBundle( requestQueryIntent );
+
+ passThroughBundle.putAll( data );
+ }
+
+ /**
+ * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
+ * by a REQUEST_QUERY from the plugin.
+ *
+ * Note that if addPassThroughMessageID() was previously called, the data will contain an extra
+ * key TaskerPlugin.Event.PASS_THOUGH_BUNDLE_MESSAGE_ID_KEY.
+ *
+ * @param queryConditionIntent QUERY_REQUEST sent from host
+ * @return data previously added to the REQUEST_QUERY intent
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #addPassThroughData(Intent,Bundle)
+ */
+ public static Bundle retrievePassThroughData( Intent queryConditionIntent ) {
+ return (Bundle) getExtraValueSafe(
+ queryConditionIntent,
+ EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA,
+ Bundle.class,
+ "retrievePassThroughData"
+ );
+ }
+
+ /**
+ * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding
+ * QUERY_CONDITION broadcast sent by the host for each event instance of the plugin.
+ *
+ * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
+ * with the REQUEST_QUERY that caused it. It also allows the message to be verified
+ * by the plugin to prevent e.g. replay attacks
+ *
+ * @param requestQueryIntent intent being sent to the host
+ * @return a guaranteed non-repeating within 100 calls message ID
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #retrievePassThroughData(Intent)
+ * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin
+ *
+ */
+ public static int addPassThroughMessageID( Intent requestQueryIntent ) {
+
+ Bundle passThroughBundle = retrieveOrCreatePassThroughBundle( requestQueryIntent );
+
+ int id = getPositiveNonRepeatingRandomInteger();
+
+ passThroughBundle.putInt( PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, id );
+
+ return id;
+ }
+
+ /*
+ * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
+ * by a REQUEST_QUERY from the plugin.
+ *
+ * @param queryConditionIntent QUERY_REQUEST sent from host
+ * @return the ID which was passed through by the host, or -1 if no ID was found
+ * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
+ * @see #addPassThroughData(Intent,Bundle)
+ */
+ public static int retrievePassThroughMessageID( Intent queryConditionIntent ) {
+
+ int toReturn = -1;
+
+ Bundle passThroughData = Event.retrievePassThroughData( queryConditionIntent );
+
+ if ( passThroughData != null ) {
+ Integer id = (Integer) getBundleValueSafe(
+ passThroughData,
+ PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY,
+ Integer.class,
+ "retrievePassThroughMessageID"
+ );
+
+ if ( id != null )
+ toReturn = id;
+ }
+
+ return toReturn;
+ }
+
+ // internal use
+ private static Bundle retrieveOrCreatePassThroughBundle( Intent requestQueryIntent ) {
+
+ Bundle passThroughBundle;
+
+ if ( requestQueryIntent.hasExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA ) )
+ passThroughBundle = requestQueryIntent.getBundleExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA );
+ else {
+ passThroughBundle = new Bundle();
+ requestQueryIntent.putExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughBundle );
+ }
+
+ return passThroughBundle;
+ }
+ }
+ // ---------------------------------- HOST ----------------------------------------- //
+
+ public static class Host {
+
+ /**
+ * Tell the plugin what capabilities the host support. This should be called when sending
+ * intents to any EditActivity, FireReceiver or QueryReceiver.
+ *
+ * @param toPlugin the intent we're sending
+ * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags
+ */
+ public static Intent addCapabilities( Intent toPlugin, int capabilities ) {
+ return toPlugin.putExtra( EXTRA_HOST_CAPABILITIES, capabilities );
+ }
+
+ /**
+ * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin
+ * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true.
+ *
+ * @param fireIntent fire intent going to the plugin
+ * @param completionIntent intent which will signal the host that the plugin is finished.
+ * Implementation is host-dependent.
+ */
+ public static void addCompletionIntent(Intent fireIntent, Intent completionIntent, ComponentName callService, boolean foreground) {
+ if (callService != null) {
+ completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_PACKAGE, callService.getPackageName());
+ completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE, callService.getClassName());
+ completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_FOREGROUND, foreground);
+ }
+ fireIntent.putExtra(
+ Setting.EXTRA_PLUGIN_COMPLETION_INTENT,
+ completionIntent.toUri(Intent.URI_INTENT_SCHEME)
+ );
+ }
+
+ /**
+ * When a setting plugin is finished, it sends the host the intent which was passed to it
+ * via @code{addCompletionIntent}.
+ *
+ * @param completionIntent intent returned from the plugin when it finished.
+ * @return resultCode measure of plugin success, defaults to UNKNOWN
+ */
+ public static int getSettingResultCode( Intent completionIntent ) {
+
+ Integer val = (Integer) getExtraValueSafe( completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode" );
+
+ return ( val == null ) ? Setting.RESULT_CODE_UNKNOWN : val;
+ }
+
+ /**
+ * Extract a bundle of variables from an intent received from the FireReceiver. This
+ * should be called if the host previously indicated to the plugin
+ * that it supports setting variable return.
+ *
+ * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive()
+ * @return variables a bundle of variable name/value pairs
+ * @see #addCapabilities(Intent, int)
+ */
+
+ public static Bundle getVariablesBundle( Bundle resultExtras ) {
+ return (Bundle) getBundleValueSafe(
+ resultExtras, EXTRA_VARIABLES_BUNDLE, Bundle.class, "getVariablesBundle"
+ );
+ }
+
+ /**
+ * Inform a setting plugin of the timeout value the host is using.
+ *
+ * @param toPlugin the intent we're sending
+ * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from
+ * that which the plugin requests.
+ * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
+ */
+ public static void addHintTimeoutMS( Intent toPlugin, int timeoutMS ) {
+ getHintsBundle( toPlugin, "addHintTimeoutMS" ).putInt( BUNDLE_KEY_HINT_TIMEOUT_MS, timeoutMS );
+ }
+
+ private static Bundle getHintsBundle( Intent intent, String funcName ) {
+
+ Bundle hintsBundle = (Bundle) getExtraValueSafe( intent, EXTRA_HINTS_BUNDLE, Bundle.class, funcName );
+
+ if ( hintsBundle == null ) {
+ hintsBundle = new Bundle();
+ intent.putExtra( EXTRA_HINTS_BUNDLE, hintsBundle );
+ }
+
+ return hintsBundle;
+ }
+
+ public static boolean haveRequestedTimeout( Bundle extrasFromPluginEditActivity ) {
+ return extrasFromPluginEditActivity.containsKey( Setting.EXTRA_REQUESTED_TIMEOUT );
+ }
+
+ public static int getRequestedTimeoutMS( Bundle extrasFromPluginEditActivity ) {
+ return
+ (Integer) getBundleValueSafe(
+ extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT, Integer.class, "getRequestedTimeout"
+ )
+ ;
+ }
+
+ public static String [] getSettingVariableReplaceKeys( Bundle fromPluginEditActivity ) {
+ return getStringArrayFromBundleString(
+ fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS,
+ "getSettingVariableReplaceKeys"
+ );
+ }
+
+ public static String [] getKeysWithEncoding( Bundle fromPluginEditActivity, Encoding encoding ) {
+
+ String [] toReturn = null;
+
+ if ( Encoding.JSON.equals( encoding ) )
+ toReturn = getStringArrayFromBundleString(
+ fromPluginEditActivity, TaskerPlugin.BUNDLE_KEY_ENCODING_JSON_KEYS,
+ "getKeyEncoding:JSON"
+ );
+ else
+ Log.w( TAG, "Host.getKeyEncoding: unknown encoding " + encoding );
+
+ return toReturn;
+ }
+
+ public static boolean haveRelevantVariables( Bundle b ) {
+ return b.containsKey( BUNDLE_KEY_RELEVANT_VARIABLES );
+ }
+
+ public static void cleanRelevantVariables( Bundle b ) {
+ b.remove( BUNDLE_KEY_RELEVANT_VARIABLES );
+ }
+
+ public static void cleanHints( Bundle extras ) {
+ extras.remove( TaskerPlugin.EXTRA_HINTS_BUNDLE );
+ }
+
+ public static void cleanRequestedTimeout( Bundle extras ) {
+ extras.remove( Setting.EXTRA_REQUESTED_TIMEOUT );
+ }
+
+ public static void cleanSettingReplaceVariables( Bundle b ) {
+ b.remove( Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS );
+ }
+ }
+
+ // ---------------------------------- HELPER FUNCTIONS -------------------------------- //
+
+ private static Object getBundleValueSafe( Bundle b, String key, Class<?> expectedClass, String funcName ) {
+ Object value = null;
+
+ if ( b != null ) {
+ if ( b.containsKey( key ) ) {
+ Object obj = b.get( key );
+ if ( obj == null )
+ Log.w( TAG, funcName + ": " + key + ": null value" );
+ else if ( obj.getClass() != expectedClass )
+ Log.w( TAG, funcName + ": " + key + ": expected " + expectedClass.getClass().getName() + ", got " + obj.getClass().getName() );
+ else
+ value = obj;
+ }
+ }
+ return value;
+ }
+
+ private static Object getExtraValueSafe( Intent i, String key, Class<?> expectedClass, String funcName ) {
+ return ( i.hasExtra( key ) ) ?
+ getBundleValueSafe( i.getExtras(), key, expectedClass, funcName ) :
+ null;
+ }
+
+ private static boolean hostSupports( Bundle extrasFromHost, int capabilityFlag ) {
+ Integer flags = (Integer) getBundleValueSafe( extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports" );
+ return
+ ( flags != null ) &&
+ ( ( flags & capabilityFlag ) > 0 )
+ ;
+ }
+
+ public static int getPackageVersionCode( PackageManager pm, String packageName ) {
+
+ int code = -1;
+
+ if ( pm != null ) {
+ try {
+ PackageInfo pi = pm.getPackageInfo( packageName, 0 );
+ if ( pi != null )
+ code = pi.versionCode;
+ }
+ catch ( Exception e ) {
+ Log.e( TAG, "getPackageVersionCode: exception getting package info" );
+ }
+ }
+
+ return code;
+ }
+
+ private static boolean variableNameIsLocal( String varName ) {
+
+ int digitCount = 0;
+ int length = varName.length();
+
+ for ( int x = 0; x < length; x++ ) {
+ char ch = varName.charAt( x );
+
+ if ( Character.isUpperCase( ch ) )
+ return false;
+ else if ( Character.isDigit( ch ) )
+ digitCount++;
+ }
+
+ if ( digitCount == ( varName.length() - 1 ) )
+ return false;
+
+ return true;
+ }
+
+ private static String [] getStringArrayFromBundleString( Bundle bundle, String key, String funcName ) {
+
+ String spec = (String) getBundleValueSafe( bundle, key, String.class, funcName );
+
+ String [] toReturn = null;
+
+ if ( spec != null )
+ toReturn = spec.split( " " );
+
+ return toReturn;
+ }
+
+ private static void addStringArrayToBundleAsString( String [] toAdd, Bundle bundle, String key, String callerName ) {
+
+ StringBuilder builder = new StringBuilder();
+
+ if ( toAdd != null ) {
+
+ for ( String keyName : toAdd ) {
+
+ if ( keyName.contains( " " ) )
+ Log.w( TAG, callerName + ": ignoring bad keyName containing space: " + keyName );
+ else {
+ if ( builder.length() > 0 )
+ builder.append( ' ' );
+
+ builder.append( keyName );
+ }
+
+ if ( builder.length() > 0 )
+ bundle.putString( key, builder.toString() );
+ }
+ }
+ }
+
+ // state tracking for random number sequence
+ private static int [] lastRandomsSeen = null;
+ private static int randomInsertPointer = 0;
+ private static SecureRandom sr = null;
+
+ /**
+ * Generate a sequence of secure random positive integers which is guaranteed not to repeat
+ * in the last 100 calls to this function.
+ *
+ * @return a random positive integer
+ */
+ public static int getPositiveNonRepeatingRandomInteger() {
+
+ // initialize on first call
+ if ( sr == null ) {
+ sr = new SecureRandom();
+ lastRandomsSeen = new int[RANDOM_HISTORY_SIZE];
+
+ for ( int x = 0; x < lastRandomsSeen.length; x++ )
+ lastRandomsSeen[x] = -1;
+ }
+
+ int toReturn;
+ do {
+ // pick a number
+ toReturn = sr.nextInt( Integer.MAX_VALUE );
+
+ // check we havn't see it recently
+ for ( int seen : lastRandomsSeen ) {
+ if ( seen == toReturn ) {
+ toReturn = -1;
+ break;
+ }
+ }
+ }
+ while ( toReturn == -1 );
+
+ // update history
+ lastRandomsSeen[randomInsertPointer] = toReturn;
+ randomInsertPointer = ( randomInsertPointer + 1 ) % lastRandomsSeen.length;
+
+ return toReturn;
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..29e6e45
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
+
+ <RadioGroup
+ android:id="@+id/radioGroup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <RadioButton
+ android:id="@+id/radioButtonDisable"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:checked="true"
+ android:text="@string/disable_ambient_sound_control"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <RadioButton
+ android:id="@+id/radioButtonNoiseCancelling"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:text="@string/enable_noise_cancelling"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/radioButtonDisable" />
+
+ <RadioButton
+ android:id="@+id/radioButtonWindCancelling"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:text="@string/enable_wind_cancelling"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/radioButtonNoiseCancelling" />
+
+ <RadioButton
+ android:id="@+id/radioButtonAmbientSound"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="32dp"
+ android:text="@string/ambient_sound"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/radioButtonWindCancelling" />
+
+ </RadioGroup>
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="32dp"
+ android:layout_marginTop="16dp"
+ android:text="@string/volume"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/radioGroup" />
+
+ <SeekBar
+ android:id="@+id/seekBarVolume"
+ style="@style/Widget.AppCompat.SeekBar.Discrete"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="32dp"
+ android:max="20"
+ android:min="1"
+ android:progress="20"
+ app:layout_constraintBottom_toBottomOf="@+id/textView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/textView"
+ app:layout_constraintTop_toTopOf="@+id/textView" />
+
+ <Switch
+ android:id="@+id/switchVoiceOptimized"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:switchPadding="16dp"
+ android:text="@string/voice_optimized"
+ app:layout_constraintStart_toStartOf="@+id/textView"
+ app:layout_constraintTop_toBottomOf="@+id/textView" />
+
+ <Button
+ android:id="@+id/buttonTest"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/test"
+ app:layout_constraintBottom_toTopOf="@+id/buttonSave"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/buttonSave"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/save"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/headphones.png b/app/src/main/res/mipmap-hdpi/headphones.png
new file mode 100644
index 0000000..354847e
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/headphones.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/headphones.png b/app/src/main/res/mipmap-mdpi/headphones.png
new file mode 100644
index 0000000..75427a4
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/headphones.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/headphones.png b/app/src/main/res/mipmap-xhdpi/headphones.png
new file mode 100644
index 0000000..4c66e59
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/headphones.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/headphones.png b/app/src/main/res/mipmap-xxhdpi/headphones.png
new file mode 100644
index 0000000..f3ebc29
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/headphones.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/headphones.png b/app/src/main/res/mipmap-xxxhdpi/headphones.png
new file mode 100644
index 0000000..8e869b0
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/headphones.png
Binary files differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6b3f6ab
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d46044f
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+<resources>
+ <string name="app_name">Sony Headphones Control</string>
+ <string name="disable_ambient_sound_control">Disable ambient sound control</string>
+ <string name="enable_noise_cancelling">Enable noise cancelling</string>
+ <string name="enable_wind_cancelling">Enable wind cancelling</string>
+ <string name="ambient_sound">Ambient sound</string>
+ <string name="volume">Volume:</string>
+ <string name="voice_optimized">Voice optimized</string>
+ <string name="test">Test</string>
+ <string name="save">Save</string>
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..6f19b47
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..5509623
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..ba36cff
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,26 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
+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[
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..944f50f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Aug 25 09:32:00 MSK 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@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
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..cd3249b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='SonyHeadphonesControl'