#571 Finish native part

This commit is contained in:
estevez-dev 2020-07-07 18:37:44 +03:00
parent f39dbe3b24
commit 3a7f3db6cd
19 changed files with 547 additions and 495 deletions

View File

@ -81,6 +81,7 @@ dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'androidx.work:work-runtime:2.3.4'
implementation "androidx.concurrent:concurrent-futures:1.0.0"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -72,7 +72,7 @@
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
</intent-filter>
</receiver>
<receiver android:name=".Autostart">
<receiver android:name=".LocationUpdatesAfterReboot">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>

View File

@ -1,23 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.IBinder;
public class Autostart extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (Utils.requestingLocationUpdates(context) && Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction())) {
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
}
}
}

View File

@ -0,0 +1,14 @@
package com.keyboardcrumbs.hassclient;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class LocationUpdatesAfterReboot extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE && Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction())) {
LocationUtils.startServiceFromBroadcast(context);
}
}
}

View File

@ -7,7 +7,6 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.location.Location;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@ -78,9 +77,6 @@ public class LocationUpdatesService extends Service {
};
mLocationRequest = new LocationRequest();
mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
getLastLocation();
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
@ -130,8 +126,10 @@ public class LocationUpdatesService extends Service {
}
private void requestLocationUpdates() {
long requestInterval = Utils.getLocationUpdateIntervals(getApplicationContext());
Log.i(TAG, "Requesting location updates. Interval is " + requestInterval);
long requestInterval = LocationUtils.getLocationUpdateIntervals(getApplicationContext());
int priority = LocationUtils.getLocationUpdatesPriority(getApplicationContext());
Log.i(TAG, "Requesting location updates. Every " + requestInterval + "ms with priority of " + priority);
mLocationRequest.setPriority(priority);
mLocationRequest.setInterval(requestInterval);
mLocationRequest.setFastestInterval(requestInterval);
startForeground(NOTIFICATION_ID, getNotification());
@ -146,7 +144,7 @@ public class LocationUpdatesService extends Service {
private Notification getNotification() {
Intent intent = new Intent(this, LocationUpdatesService.class);
CharSequence text = Utils.getLocationText(mLocation);
CharSequence text = LocationUtils.getLocationText(mLocation);
intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true);
@ -157,13 +155,13 @@ public class LocationUpdatesService extends Service {
new Intent(this, MainActivity.class), 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.addAction(R.drawable.blank_icon, "Open HA Client",
.addAction(R.drawable.blank_icon, "Open app",
activityPendingIntent)
.addAction(R.drawable.blank_icon, "Stop",
.addAction(R.drawable.blank_icon, "Stop tracking",
servicePendingIntent)
.setContentText(text)
.setPriority(-1)
.setContentTitle(Utils.getLocationTitle(mLocation))
.setContentTitle(LocationUtils.getLocationTitle(mLocation))
.setOngoing(true)
.setSmallIcon(R.drawable.mini_icon)
.setWhen(System.currentTimeMillis());
@ -171,21 +169,6 @@ public class LocationUpdatesService extends Service {
return builder.build();
}
private void getLastLocation() {
try {
mFusedLocationClient.getLastLocation()
.addOnCompleteListener(task -> {
if (task.isSuccessful() && task.getResult() != null) {
mLocation = task.getResult();
} else {
Log.w(TAG, "Failed to get location.");
}
});
} catch (SecurityException unlikely) {
Log.e(TAG, "Lost location permission." + unlikely);
}
}
private void onNewLocation(Location location) {
Log.i(TAG, "New location: " + location);
@ -198,6 +181,7 @@ public class LocationUpdatesService extends Service {
.build();
Data locationData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
.putDouble("Lat", mLocation.getLatitude())
.putDouble("Long", mLocation.getLongitude())
.putFloat("Acc", mLocation.getAccuracy())
@ -205,7 +189,7 @@ public class LocationUpdatesService extends Service {
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendLocationWorker.class)
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,

View File

@ -0,0 +1,100 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import android.location.Location;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.ListenableWorker;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkerParameters;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.TimeUnit;
public class LocationUpdatesWorker extends ListenableWorker {
private Context currentContext;
private LocationCallback callback;
private FusedLocationProviderClient fusedLocationClient;
public LocationUpdatesWorker(Context context, WorkerParameters params) {
super(context, params);
currentContext = context;
}
private void finish() {
fusedLocationClient.removeLocationUpdates(callback);
}
@NonNull
@Override
public ListenableFuture<Result> startWork() {
return CallbackToFutureAdapter.getFuture(completer -> {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(currentContext);
callback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
Location location = locationResult.getLastLocation();
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data locationData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
.putDouble("Lat", location.getLatitude())
.putDouble("Long", location.getLongitude())
.putFloat("Acc", location.getAccuracy())
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setConstraints(constraints)
.setInputData(locationData)
.build();
WorkManager
.getInstance(getApplicationContext())
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
finish();
completer.set(Result.success());
}
};
LocationRequest locationRequest = new LocationRequest();
int accuracy = LocationUtils.getLocationUpdatesPriority(getApplicationContext());
locationRequest.setPriority(accuracy);
locationRequest.setInterval(5000);
locationRequest.setFastestInterval(1000);
try {
fusedLocationClient.requestLocationUpdates(locationRequest,
callback, Looper.myLooper());
} catch (SecurityException e) {
completer.setException(e);
}
return callback;
});
}
}

View File

@ -0,0 +1,78 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Build;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
class LocationUtils {
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.location-updates-state";
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.location-updates-interval";
static final String KEY_LOCATION_UPDATE_PRIORITY = "flutter.location-updates-priority";
static final String LOCATION_WORK_NAME = "HALocationWorker";
static final int LOCATION_UPDATES_DISABLED = 0;
static final int LOCATION_UPDATES_SERVICE = 1;
static final int LOCATION_UPDATES_WORKER = 2;
static final int DEFAULT_LOCATION_UPDATE_INTERVAL_S = 900; //15 minutes
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
static int getLocationUpdatesState(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getInt(KEY_REQUESTING_LOCATION_UPDATES, LOCATION_UPDATES_DISABLED);
}
static long getLocationUpdateIntervals(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_S) * 1000;
}
static int getLocationUpdatesPriority(Context context) {
return (int) context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_PRIORITY, 102);
}
static void setLocationUpdatesState(Context context, int locationUpdatesState) {
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
.edit()
.putInt(KEY_REQUESTING_LOCATION_UPDATES, locationUpdatesState)
.apply();
}
static String getLocationText(Location location) {
return location == null ? "Accuracy: unknown" :
"Accuracy: " + location.getAccuracy();
}
static String getLocationTitle(Location location) {
return location == null ? "Requesting location..." : "Location updated at " + DateFormat.getDateTimeInstance().format(new Date(location.getTime()));
}
static void startService(Context context) {
Intent myService = new Intent(context, LocationUpdatesService.class);
context.startService(myService);
}
static void startServiceFromBroadcast(Context context) {
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
}
static void startWorker(Context context, long interval) {
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
}
}

View File

@ -2,24 +2,19 @@ package com.keyboardcrumbs.hassclient;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.work.WorkManager;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.os.IBinder;
import io.flutter.plugin.common.MethodChannel;
@ -33,6 +28,9 @@ public class MainActivity extends FlutterActivity {
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
private int locationUpdatesType = LocationUtils.LOCATION_UPDATES_DISABLED;
private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_S * 1000;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
@ -46,8 +44,7 @@ public class MainActivity extends FlutterActivity {
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
String token = task.getResult().getToken();
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
updateTokenTask.execute(token);
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
result.success(token);
} else {
Exception ex = task.getException();
@ -64,17 +61,25 @@ public class MainActivity extends FlutterActivity {
}
break;
case "startLocationService":
Utils.setRequestingLocationUpdates(this, true);
if (isNoLocationPermissions()) {
requestLocationPermissions();
} else {
startLocationService();
try {
locationUpdatesInterval = LocationUtils.getLocationUpdateIntervals(this);
if (locationUpdatesInterval >= LocationUtils.MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS) {
locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
} else {
locationUpdatesType = LocationUtils.LOCATION_UPDATES_SERVICE;
}
if (isNoLocationPermissions()) {
requestLocationPermissions();
} else {
startLocationUpdates();
}
result.success("");
} catch (Exception e) {
result.error("location_error", e.getMessage(), null);
}
result.success("");
break;
case "stopLocationService":
Utils.setRequestingLocationUpdates(this, false);
stopLocationService();
stopLocationUpdates();
result.success("");
break;
}
@ -86,24 +91,28 @@ public class MainActivity extends FlutterActivity {
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
}
private void startLocationService() {
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
startService(myService);
private void startLocationUpdates() {
if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_SERVICE) {
LocationUtils.startService(this);
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
} else if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_WORKER) {
LocationUtils.startWorker(this, locationUpdatesInterval);
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
} else {
stopLocationUpdates();
}
}
private void stopLocationService() {
private void stopLocationUpdates() {
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
stopService(myService);
WorkManager.getInstance(this).cancelUniqueWork(LocationUtils.LOCATION_WORK_NAME);
LocationUtils.setLocationUpdatesState(this, LocationUtils.LOCATION_UPDATES_DISABLED);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*if (Utils.requestingLocationUpdates(this)) {
if (isNoLocationPermissions()) {
requestLocationPermissions();
}
}*/
}
@Override
@ -142,7 +151,9 @@ public class MainActivity extends FlutterActivity {
@NonNull int[] grantResults) {
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLocationService();
startLocationUpdates();
} else {
stopLocationUpdates();
}
}
}

View File

@ -3,7 +3,6 @@ package com.keyboardcrumbs.hassclient;
import java.util.Map;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import java.io.InputStream;
import android.app.NotificationChannel;
@ -14,6 +13,8 @@ import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import com.google.firebase.messaging.FirebaseMessagingService;
@ -26,7 +27,7 @@ import android.webkit.URLUtil;
public class MessagingService extends FirebaseMessagingService {
private static final String TAG = "MessagingService";
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
@ -39,19 +40,19 @@ public class MessagingService extends FirebaseMessagingService {
}
@Override
public void onNewToken(String token) {
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
updateTokenTask.execute(token);
public void onNewToken(@NonNull String token) {
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
}
private void sendNotification(Map<String, String> data) {
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
boolean autoCancel;
if (!data.containsKey("channelId")) {
String customChannelId = data.get("channelId");
if (customChannelId == null) {
channelId = "ha_notify";
channelDescription = "Default notification channel";
} else {
channelId = data.get("channelId");
channelId = customChannelId;
channelDescription = channelId;
}
if (!data.containsKey("body")) {
@ -114,7 +115,7 @@ public class MessagingService extends FirebaseMessagingService {
}
for (int i = 1; i <= 3; i++) {
if (data.containsKey("action" + i)) {
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class).setAction(NOTIFICATION_ACTION_BROADCAST);
if (autoCancel) {
broadcastIntent.putExtra("tag", nTag);
}

View File

@ -7,19 +7,17 @@ import android.content.Intent;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.util.concurrent.TimeUnit;
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "NextAlarmReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
@ -35,12 +33,17 @@ public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data workerData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NEXT_ALARM)
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class)
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setInputData(workerData)
.setConstraints(constraints)
.build();

View File

@ -1,73 +1,58 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.app.NotificationManager;
import android.webkit.URLUtil;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import org.json.JSONObject;
import android.content.SharedPreferences;
import java.util.concurrent.TimeUnit;
public class NotificationActionReceiver extends BroadcastReceiver {
private static final String TAG = "NotificationAction";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
String intentAction = intent.getAction();
if (intentAction == null || !intentAction.equalsIgnoreCase(MessagingService.NOTIFICATION_ACTION_BROADCAST)) {
return;
}
String rawActionData = intent.getStringExtra("actionData");
if (intent.hasExtra("tag")) {
String notificationTag = intent.getStringExtra("tag");
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationTag, 0);
}
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
JSONObject actionData = new JSONObject(rawActionData);
if (URLUtil.isValidUrl(requestUrl)) {
JSONObject dataToSend = new JSONObject();
JSONObject requestData = new JSONObject();
if (actionData.getString("action").equals("call-service")) {
dataToSend.put("type", "call_service");
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
requestData.put("service", actionData.getString("service").split("\\.")[1]);
if (actionData.has("service_data")) {
requestData.put("service_data", actionData.get("service_data"));
}
} else {
dataToSend.put("type", "fire_event");
requestData.put("event_type", "ha_client_event");
JSONObject eventData = new JSONObject();
eventData.put("action", actionData.getString("action"));
requestData.put("event_data", eventData);
}
dataToSend.put("data", requestData);
String stringRequest = dataToSend.toString();
SendTask sendTask = new SendTask();
sendTask.execute(requestUrl, stringRequest);
} else {
Log.w(TAG, "Invalid HA url");
}
} catch (Exception e) {
Log.e(TAG, "Error handling notification action", e);
}
} else {
Log.w(TAG, "Webhook id not found");
}
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data workerData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
.putString("rawActionData", rawActionData)
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setInputData(workerData)
.setConstraints(constraints)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
}
}

View File

@ -0,0 +1,218 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.BatteryManager;
import android.util.Log;
import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public class SendDataHomeWorker extends Worker {
public static final String DATA_TYPE_KEY = "dataType";
public static final int DATA_TYPE_LOCATION = 1;
public static final int DATA_TYPE_NEXT_ALARM = 2;
public static final int DATA_TYPE_NOTIFICATION_ACTION = 3;
private Context currentContext;
private static final String TAG = "SendDataHomeWorker";
public static final String KEY_LAT_ARG = "Lat";
public static final String KEY_LONG_ARG = "Long";
public static final String KEY_ACC_ARG = "Acc";
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
public SendDataHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
currentContext = context;
}
@NonNull
@Override
public Result doWork() {
Log.d(TAG, "Start sending data home");
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
if (URLUtil.isValidUrl(requestUrl)) {
int dataType = getInputData().getInt(DATA_TYPE_KEY, 0);
String stringRequest;
if (dataType == DATA_TYPE_LOCATION) {
Log.d(TAG, "Location data");
stringRequest = getLocationDataToSend();
} else if (dataType == DATA_TYPE_NEXT_ALARM) {
Log.d(TAG, "Next alarm data");
stringRequest = getNextAlarmDataToSend();
} else if (dataType == DATA_TYPE_NOTIFICATION_ACTION) {
Log.d(TAG, "Notification action data");
stringRequest = getNotificationActionData();
} else {
Log.e(TAG, "doWork() unknown data type: " + dataType);
return Result.failure();
}
try {
URL url = new URL(requestUrl);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
assert stringRequest != null;
byte[] outputBytes = stringRequest.getBytes(StandardCharsets.UTF_8);
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
if (responseCode >= 300) {
return Result.retry();
}
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
return Result.retry();
}
} else {
Log.w(TAG, "Invalid HA url");
return Result.failure();
}
} catch (Exception e) {
Log.e(TAG, "Error =(", e);
return Result.failure();
}
} else {
Log.w(TAG, "Webhook id not found");
return Result.failure();
}
return Result.success();
}
private String getLocationDataToSend() {
try {
JSONObject dataToSend = new JSONObject();
dataToSend.put("type", "update_location");
JSONObject dataObject = new JSONObject();
JSONArray gps = new JSONArray();
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
dataObject.put("gps", gps);
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
BatteryManager bm;
if (android.os.Build.VERSION.SDK_INT >= 23) {
bm = currentContext.getSystemService(BatteryManager.class);
} else {
bm = (BatteryManager)currentContext.getSystemService(Context.BATTERY_SERVICE);
}
int batLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
dataObject.put("battery", batLevel);
dataToSend.put("data", dataObject);
return dataToSend.toString();
} catch (Exception e) {
Log.e(TAG,"getLocationDataToSend", e);
return null;
}
}
private String getNotificationActionData() {
try {
String rawActionData = getInputData().getString("rawActionData");
if (rawActionData == null || rawActionData.length() == 0) {
Log.e(TAG,"getNotificationActionData rawAction data is empty");
return null;
}
JSONObject actionData = new JSONObject(rawActionData);
JSONObject dataToSend = new JSONObject();
JSONObject requestData = new JSONObject();
if (actionData.getString("action").equals("call-service")) {
dataToSend.put("type", "call_service");
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
requestData.put("service", actionData.getString("service").split("\\.")[1]);
if (actionData.has("service_data")) {
requestData.put("service_data", actionData.get("service_data"));
}
} else {
dataToSend.put("type", "fire_event");
requestData.put("event_type", "ha_client_event");
JSONObject eventData = new JSONObject();
eventData.put("action", actionData.getString("action"));
requestData.put("event_data", eventData);
}
dataToSend.put("data", requestData);
return dataToSend.toString();
} catch (Exception e) {
Log.e(TAG,"getNotificationActionData", e);
return null;
}
}
private String getNextAlarmDataToSend() {
try {
final AlarmManager alarmManager;
if (android.os.Build.VERSION.SDK_INT >= 23) {
alarmManager = currentContext.getSystemService(AlarmManager.class);
} else {
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
}
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
JSONObject dataToSend = new JSONObject();
dataToSend.put("type", "update_sensor_states");
JSONArray dataArray = new JSONArray();
JSONObject sensorData = new JSONObject();
JSONObject sensorAttrs = new JSONObject();
sensorData.put("unique_id", "next_alarm");
sensorData.put("type", "sensor");
final long triggerTimestamp;
if (alarmClockInfo != null) {
triggerTimestamp = alarmClockInfo.getTriggerTime();
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(triggerTimestamp);
sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime()));
sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("timestamp", triggerTimestamp);
} else {
sensorData.put("state", "");
sensorAttrs.put("date", "");
sensorAttrs.put("time", "");
sensorAttrs.put("timestamp", 0);
}
sensorData.put("icon", "mdi:alarm");
sensorData.put("attributes", sensorAttrs);
dataArray.put(0, sensorData);
dataToSend.put("data", dataArray);
return dataToSend.toString();
} catch (Exception e) {
Log.e(TAG,"getNextAlarmDataToSend", e);
return null;
}
}
}

View File

@ -1,98 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public class SendLocationWorker extends Worker {
private Context currentContext;
private static final String TAG = "SendLocationWorker";
public static final String KEY_LAT_ARG = "Lat";
public static final String KEY_LONG_ARG = "Long";
public static final String KEY_ACC_ARG = "Acc";
public SendLocationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
currentContext = context;
}
@NonNull
@Override
public Result doWork() {
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
JSONObject dataToSend = new JSONObject();
if (URLUtil.isValidUrl(requestUrl)) {
dataToSend.put("type", "update_location");
JSONObject dataObject = new JSONObject();
JSONArray gps = new JSONArray();
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
dataObject.put("gps", gps);
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
dataObject.put("battery", 41);
dataToSend.put("data", dataObject);
String stringRequest = dataToSend.toString();
try {
URL url = new URL(requestUrl);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = stringRequest.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
if (responseCode >= 300) {
return Result.retry();
}
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
return Result.retry();
}
} else {
Log.w(TAG, "Invalid HA url");
return Result.failure();
}
} catch (Exception e) {
Log.e(TAG, "Error =(", e);
return Result.failure();
}
} else {
Log.w(TAG, "Webhook id not found");
return Result.failure();
}
return Result.success();
}
}

View File

@ -1,46 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
public class SendTask extends AsyncTask<String, String, String> {
private static final String TAG = "SendTask";
public SendTask(){
//set context variables if required
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
String urlString = params[0];
String data = params[1];
try {
URL url = new URL(urlString);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = data.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
}
return null;
}
}

View File

@ -1,119 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public class UpdateNextAlarmWorker extends Worker {
private Context currentContext;
private static final String TAG = "NextAlarmWorker";
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
public UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
currentContext = context;
}
@NonNull
@Override
public Result doWork() {
final AlarmManager alarmManager;
if (android.os.Build.VERSION.SDK_INT >= 23) {
alarmManager = currentContext.getSystemService(AlarmManager.class);
} else {
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
}
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
JSONObject dataToSend = new JSONObject();
if (URLUtil.isValidUrl(requestUrl)) {
dataToSend.put("type", "update_sensor_states");
JSONArray dataArray = new JSONArray();
JSONObject sensorData = new JSONObject();
JSONObject sensorAttrs = new JSONObject();
sensorData.put("unique_id", "next_alarm");
sensorData.put("type", "sensor");
final long triggerTimestamp;
if (alarmClockInfo != null) {
triggerTimestamp = alarmClockInfo.getTriggerTime();
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(triggerTimestamp);
sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime()));
sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("timestamp", triggerTimestamp);
} else {
sensorData.put("state", "");
sensorAttrs.put("date", "");
sensorAttrs.put("time", "");
sensorAttrs.put("timestamp", 0);
}
sensorData.put("icon", "mdi:alarm");
sensorData.put("attributes", sensorAttrs);
dataArray.put(0, sensorData);
dataToSend.put("data", dataArray);
String stringRequest = dataToSend.toString();
try {
URL url = new URL(requestUrl);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = stringRequest.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
if (responseCode >= 300) {
return Result.retry();
}
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
return Result.retry();
}
} else {
Log.w(TAG, "Invalid HA url");
return Result.failure();
}
} catch (Exception e) {
Log.e(TAG, "Error setting next alarm", e);
return Result.failure();
}
} else {
Log.w(TAG, "Webhook id not found");
return Result.failure();
}
return Result.success();
}
}

View File

@ -1,46 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
import android.content.Context;
import java.lang.ref.WeakReference;
public class UpdateTokenTask extends AsyncTask<String, String, String> {
private static final String TAG = "UpdateTokenTask";
private WeakReference<Context> contextRef;
public UpdateTokenTask(Context context){
contextRef = new WeakReference<>(context);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
Log.d(TAG, "Updating push token");
Context context = contextRef.get();
if (context != null) {
String token = params[0];
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("flutter.npush-token", token);
editor.commit();
}
return null;
}
}

View File

@ -1,38 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import android.location.Location;
import android.preference.PreferenceManager;
import java.text.DateFormat;
import java.util.Date;
class Utils {
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.foreground-location-service";
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.active-location-interval";
static boolean requestingLocationUpdates(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_REQUESTING_LOCATION_UPDATES, false);
}
static long getLocationUpdateIntervals(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, 90) * 1000;
}
static void setRequestingLocationUpdates(Context context, boolean requestingLocationUpdates) {
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_REQUESTING_LOCATION_UPDATES, requestingLocationUpdates)
.apply();
}
static String getLocationText(Location location) {
return location == null ? "Accuracy: unknown" :
"Accuracy: " + location.getAccuracy();
}
static String getLocationTitle(Location location) {
return location == null ? "Requesting location..." : "Location updated at " + DateFormat.getDateTimeInstance().format(new Date(location.getTime()));
}
}

View File

@ -32,7 +32,7 @@ class AppSettings {
DisplayMode displayMode;
AppTheme appTheme;
final int defaultLocationUpdateIntervalMinutes = 20;
final int defaultActiveLocationUpdateIntervalSeconds = 90;
final int defaultActiveLocationUpdateIntervalSeconds = 900;
Duration locationUpdateInterval;
bool locationTrackingEnabled = false;

View File

@ -12,6 +12,12 @@ class IntegrationSettingsPage extends StatefulWidget {
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
static final locationAccuracy = {
100: "Highest",
102: "Balanced (about 100 meters)",
104: "Low (up to 10 kilometers)",
105: "Passive (last known location)",
};
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
int _activeLocationInterval = AppSettings().defaultActiveLocationUpdateIntervalSeconds;
@ -19,6 +25,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
bool _foregroundLocationTrackingEnabled = false;
bool _wait = false;
bool _changedHere = false;
int _accuracy = 102;
@override
void initState() {
@ -33,10 +40,11 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
SharedPreferences.getInstance().then((prefs) {
setState(() {
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_foregroundLocationTrackingEnabled = prefs.getBool("foreground-location-service") ?? false;
_accuracy = prefs.getInt("location-updates-priority") ?? 102;
_foregroundLocationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0;
_locationInterval = prefs.getInt("location-interval") ??
AppSettings().defaultLocationUpdateIntervalMinutes;
_activeLocationInterval = prefs.getInt("active-location-interval") ??
_activeLocationInterval = prefs.getInt("location-updates-interval") ??
AppSettings().defaultActiveLocationUpdateIntervalSeconds;
if (_locationInterval < 15) {
_locationInterval = 15;
@ -94,9 +102,13 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
}
_switchForegroundLocationTrackingState(bool state) async {
await AppSettings().save({'active-location-interval': _activeLocationInterval});
await AppSettings().save({'location-updates-interval': _activeLocationInterval, 'location-updates-priority': _accuracy});
if (state) {
await platform.invokeMethod('startLocationService');
try {
await platform.invokeMethod('startLocationService');
} catch (e) {
_foregroundLocationTrackingEnabled = false;
}
} else {
await platform.invokeMethod('stopLocationService');
}
@ -154,11 +166,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
),
Container(height: Sizes.rowPadding),
Text("Active location tracking", style: Theme.of(context).textTheme.title),
Text("Works in foreground noticeably affecting phone battery. Sends most accurate location getting it from phone if possible. Can be very frequent.",
style: Theme.of(context).textTheme.caption,
softWrap: true,
),
Container(height: Sizes.rowPadding,),
Container(height: Sizes.rowPadding),
Row(
children: <Widget>[
Text("Enable"),
@ -175,7 +183,26 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
],
),
Container(height: Sizes.rowPadding),
Text("Update device location every"),
Text("Accuracy:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<int>(
value: _accuracy,
iconSize: 30.0,
isExpanded: true,
items: locationAccuracy.keys.map((value) {
return new DropdownMenuItem<int>(
value: value,
child: Text('${locationAccuracy[value]}'),
);
}).toList(),
onChanged: _foregroundLocationTrackingEnabled ? null : (val) {
setState(() {
_accuracy = val;
});
},
),
Container(height: Sizes.rowPadding),
Text("Update intervals"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,