parent
3a7f3db6cd
commit
dce93966e3
@ -80,6 +80,7 @@ flutter {
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
implementation 'com.google.firebase:firebase-messaging:20.2.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.3.4'
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
@ -1,9 +1,7 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
@ -14,7 +12,6 @@ import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
@ -35,20 +32,8 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocationUpdatesService extends Service {
|
||||
|
||||
private static final String PACKAGE_NAME =
|
||||
"com.keyboardcrumbs.hassclient";
|
||||
|
||||
private static final String TAG = LocationUpdatesService.class.getSimpleName();
|
||||
|
||||
private static final String CHANNEL_ID = "location_service";
|
||||
|
||||
private static final String EXTRA_STARTED_FROM_NOTIFICATION = PACKAGE_NAME +
|
||||
".started_from_notification";
|
||||
|
||||
//private final IBinder mBinder = new LocalBinder();
|
||||
|
||||
private static final int NOTIFICATION_ID = 954311;
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
@ -59,8 +44,6 @@ public class LocationUpdatesService extends Service {
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
private Location mLocation;
|
||||
|
||||
public LocationUpdatesService() {
|
||||
}
|
||||
|
||||
@ -86,7 +69,7 @@ public class LocationUpdatesService extends Service {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
new NotificationChannel(LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
@ -95,15 +78,8 @@ public class LocationUpdatesService extends Service {
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Service started");
|
||||
boolean startedFromNotification = intent.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION,
|
||||
false);
|
||||
|
||||
// We got here because the user decided to remove location updates from the notification.
|
||||
if (startedFromNotification) {
|
||||
stopSelf();
|
||||
} else {
|
||||
requestLocationUpdates();
|
||||
}
|
||||
requestLocationUpdates();
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
@ -132,7 +108,7 @@ public class LocationUpdatesService extends Service {
|
||||
mLocationRequest.setPriority(priority);
|
||||
mLocationRequest.setInterval(requestInterval);
|
||||
mLocationRequest.setFastestInterval(requestInterval);
|
||||
startForeground(NOTIFICATION_ID, getNotification());
|
||||
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
|
||||
try {
|
||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
||||
mLocationCallback, Looper.myLooper());
|
||||
@ -141,40 +117,10 @@ public class LocationUpdatesService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private Notification getNotification() {
|
||||
Intent intent = new Intent(this, LocationUpdatesService.class);
|
||||
|
||||
CharSequence text = LocationUtils.getLocationText(mLocation);
|
||||
|
||||
intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true);
|
||||
|
||||
PendingIntent servicePendingIntent = PendingIntent.getService(this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, MainActivity.class), 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.addAction(R.drawable.blank_icon, "Open app",
|
||||
activityPendingIntent)
|
||||
.addAction(R.drawable.blank_icon, "Stop tracking",
|
||||
servicePendingIntent)
|
||||
.setContentText(text)
|
||||
.setPriority(-1)
|
||||
.setContentTitle(LocationUtils.getLocationTitle(mLocation))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.mini_icon)
|
||||
.setWhen(System.currentTimeMillis());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void onNewLocation(Location location) {
|
||||
Log.i(TAG, "New location: " + location);
|
||||
|
||||
mLocation = location;
|
||||
|
||||
mNotificationManager.notify(NOTIFICATION_ID, getNotification());
|
||||
mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, location, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
@ -182,9 +128,9 @@ public class LocationUpdatesService extends Service {
|
||||
|
||||
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())
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -25,6 +29,8 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
|
||||
public class LocationUpdatesWorker extends ListenableWorker {
|
||||
|
||||
private Context currentContext;
|
||||
@ -78,6 +84,22 @@ public class LocationUpdatesWorker extends ListenableWorker {
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
if (LocationUtils.showNotification(currentContext)) {
|
||||
NotificationManager notificationManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
notificationManager = currentContext.getSystemService(NotificationManager.class);
|
||||
} else {
|
||||
notificationManager = (NotificationManager)currentContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
notificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
notificationManager.notify(LocationUtils.WORKER_NOTIFICATION_ID, LocationUtils.getNotification(currentContext, location, LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID));
|
||||
}
|
||||
finish();
|
||||
completer.set(Result.success());
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
@ -18,6 +21,12 @@ 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 KEY_LOCATION_SHOW_NOTIFICATION = "flutter.location-updates-show-notification";
|
||||
|
||||
static final String WORKER_NOTIFICATION_CHANNEL_ID = "location_worker";
|
||||
static final int WORKER_NOTIFICATION_ID = 954322;
|
||||
static final String SERVICE_NOTIFICATION_CHANNEL_ID = "location_service";
|
||||
static final int SERVICE_NOTIFICATION_ID = 954311;
|
||||
|
||||
static final String LOCATION_WORK_NAME = "HALocationWorker";
|
||||
|
||||
@ -40,6 +49,10 @@ class LocationUtils {
|
||||
return (int) context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_PRIORITY, 102);
|
||||
}
|
||||
|
||||
static boolean showNotification(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_LOCATION_SHOW_NOTIFICATION, true);
|
||||
}
|
||||
|
||||
static void setLocationUpdatesState(Context context, int locationUpdatesState) {
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
@ -70,6 +83,25 @@ class LocationUtils {
|
||||
}
|
||||
}
|
||||
|
||||
static Notification getNotification(Context context, Location location, String channelId) {
|
||||
CharSequence text = LocationUtils.getLocationText(location);
|
||||
|
||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
||||
new Intent(context, MainActivity.class), 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||
.addAction(R.drawable.blank_icon, "Open HA Client",
|
||||
activityPendingIntent)
|
||||
.setContentText(text)
|
||||
.setPriority(-1)
|
||||
.setContentTitle(LocationUtils.getLocationTitle(location))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.mini_icon)
|
||||
.setWhen(System.currentTimeMillis());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static void startWorker(Context context, long interval) {
|
||||
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
@ -9,10 +9,10 @@ import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
|
||||
@ -82,6 +82,10 @@ public class MainActivity extends FlutterActivity {
|
||||
stopLocationUpdates();
|
||||
result.success("");
|
||||
break;
|
||||
case "cancelOldLocationWorker":
|
||||
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
|
||||
result.success("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -107,6 +111,13 @@ public class MainActivity extends FlutterActivity {
|
||||
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
|
||||
stopService(myService);
|
||||
WorkManager.getInstance(this).cancelUniqueWork(LocationUtils.LOCATION_WORK_NAME);
|
||||
NotificationManager notificationManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
notificationManager = getSystemService(NotificationManager.class);
|
||||
} else {
|
||||
notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
notificationManager.cancel(LocationUtils.WORKER_NOTIFICATION_ID);
|
||||
LocationUtils.setLocationUpdatesState(this, LocationUtils.LOCATION_UPDATES_DISABLED);
|
||||
}
|
||||
|
||||
|
@ -22,9 +22,6 @@ import 'package:device_info/device_info.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:battery/battery.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
@ -117,7 +114,6 @@ part 'pages/entity.page.dart';
|
||||
part 'utils/mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'managers/auth_manager.class.dart';
|
||||
part 'managers/location_manager.class.dart';
|
||||
part 'managers/mobile_app_integration_manager.class.dart';
|
||||
part 'managers/connection_manager.class.dart';
|
||||
part 'managers/device_info_manager.class.dart';
|
||||
@ -230,10 +226,6 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
_currentTheme = event.theme;
|
||||
});
|
||||
});
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,8 @@ class AppSettings {
|
||||
|
||||
static const AUTH_TOKEN_KEY = 'llt';
|
||||
|
||||
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||
|
||||
static final AppSettings _instance = AppSettings._internal();
|
||||
|
||||
factory AppSettings() {
|
||||
@ -31,10 +33,7 @@ class AppSettings {
|
||||
bool nextAlarmSensorCreated = false;
|
||||
DisplayMode displayMode;
|
||||
AppTheme appTheme;
|
||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||
final int defaultActiveLocationUpdateIntervalSeconds = 900;
|
||||
Duration locationUpdateInterval;
|
||||
bool locationTrackingEnabled = false;
|
||||
final int defaultLocationUpdateIntervalSeconds = 900;
|
||||
|
||||
bool get isAuthenticated => longLivedToken != null;
|
||||
bool get isTempAuthenticated => tempToken != null;
|
||||
@ -50,6 +49,7 @@ class AppSettings {
|
||||
await Hive.openBox(DEFAULT_HIVE_BOX);
|
||||
Logger.d('Loading settings...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await migrate(prefs);
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
@ -60,10 +60,6 @@ class AppSettings {
|
||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||
httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||
defaultLocationUpdateIntervalMinutes);
|
||||
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false;
|
||||
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
@ -72,6 +68,37 @@ class AppSettings {
|
||||
}
|
||||
}
|
||||
|
||||
Future migrate(SharedPreferences prefs) async {
|
||||
//Migrating to new location tracking. TODO: Remove when no version 1.2.0 (and older) in the wild
|
||||
if (prefs.getBool("location-tracking-migrated") == null) {
|
||||
Logger.d("[MIGRATION] Migrating to new location tracking...");
|
||||
bool oldLocationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
if (oldLocationTrackingEnabled) {
|
||||
await platform.invokeMethod('cancelOldLocationWorker');
|
||||
await prefs.setInt("location-updates-state", 2); //Setting new location tracking mode to worker
|
||||
await prefs.setInt("location-updates-priority", 100); //Setting location accuracy to high
|
||||
int oldLocationTrackingInterval = prefs.getInt("location-interval") ?? 0;
|
||||
if (oldLocationTrackingInterval < 15) {
|
||||
oldLocationTrackingInterval = 15;
|
||||
}
|
||||
await prefs.setInt("location-updates-interval", oldLocationTrackingInterval * 60); //moving old interval in minutes to new interval in seconds
|
||||
try {
|
||||
await platform.invokeMethod('startLocationService');
|
||||
} catch (e) {
|
||||
await prefs.setInt("location-updates-state", 0); //Disabling location tracking if can't start
|
||||
}
|
||||
} else {
|
||||
Logger.d("[MIGRATION] Old location tracking was disabled");
|
||||
await prefs.setInt("location-updates-state", 0); //Setting new location tracking mode to disabled
|
||||
}
|
||||
await prefs.setBool("location-tracking-migrated", true);
|
||||
}
|
||||
//Migrating from integration without next alarm sensor. TODO: remove when no version 1.1.2 (and older) in the wild
|
||||
nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false;
|
||||
//Done
|
||||
Logger.d("[MIGRATION] Done.");
|
||||
}
|
||||
|
||||
Future<dynamic> loadSingle(String key) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.get('$key');
|
||||
|
@ -1,229 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LocationManager {
|
||||
|
||||
static final LocationManager _instance = LocationManager
|
||||
._internal();
|
||||
|
||||
factory LocationManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
LocationManager._internal() {
|
||||
init();
|
||||
}
|
||||
|
||||
final String backgroundTaskId = "haclocationtask0";
|
||||
final String backgroundTaskTag = "haclocation";
|
||||
|
||||
void init() async {
|
||||
if (AppSettings().locationTrackingEnabled) {
|
||||
await _startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
setSettings(bool enabled, int interval) async {
|
||||
if (interval != AppSettings().locationUpdateInterval.inMinutes) {
|
||||
await _stopLocationService();
|
||||
}
|
||||
AppSettings().save({
|
||||
'location-interval': interval,
|
||||
'location-enabled': enabled
|
||||
});
|
||||
AppSettings().locationUpdateInterval = Duration(minutes: interval);
|
||||
AppSettings().locationTrackingEnabled = enabled;
|
||||
if (enabled && !AppSettings().locationTrackingEnabled) {
|
||||
Logger.d("Starting location tracking");
|
||||
await _startLocationService();
|
||||
} else if (!enabled && AppSettings().locationTrackingEnabled) {
|
||||
Logger.d("Stopping location tracking...");
|
||||
await _stopLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
_startLocationService() async {
|
||||
String webhookId = AppSettings().webhookId;
|
||||
String httpWebHost = AppSettings().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Duration interval;
|
||||
int delayFactor;
|
||||
int taskCount;
|
||||
Logger.d("Starting location update for every ${AppSettings().locationUpdateInterval
|
||||
.inMinutes} minutes...");
|
||||
if (AppSettings().locationUpdateInterval.inMinutes == 10) {
|
||||
interval = Duration(minutes: 20);
|
||||
taskCount = 2;
|
||||
delayFactor = 10;
|
||||
} else if (AppSettings().locationUpdateInterval.inMinutes == 5) {
|
||||
interval = Duration(minutes: 15);
|
||||
taskCount = 3;
|
||||
delayFactor = 5;
|
||||
} else {
|
||||
interval = AppSettings().locationUpdateInterval;
|
||||
taskCount = 1;
|
||||
delayFactor = 0;
|
||||
}
|
||||
for (int i = 1; i <= taskCount; i++) {
|
||||
int delay = i*delayFactor;
|
||||
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
|
||||
await workManager.Workmanager.registerPeriodicTask(
|
||||
"$backgroundTaskId$i",
|
||||
"haClientLocationTracking-0$i",
|
||||
tag: backgroundTaskTag,
|
||||
inputData: {
|
||||
"webhookId": webhookId,
|
||||
"httpWebHost": httpWebHost
|
||||
},
|
||||
frequency: interval,
|
||||
initialDelay: Duration(minutes: delay),
|
||||
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
|
||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||
backoffPolicyDelay: interval,
|
||||
constraints: workManager.Constraints(
|
||||
networkType: workManager.NetworkType.connected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelAll();
|
||||
}
|
||||
|
||||
updateDeviceLocation() async {
|
||||
try {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = AppSettings().webhookId;
|
||||
String httpWebHost = AppSettings().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Logger.d("[Foreground location] Getting battery level...");
|
||||
int batteryLevel = await battery.batteryLevel;
|
||||
Logger.d("[Foreground location] Getting device location...");
|
||||
Position position = await geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationPermissionLevel: GeolocationPermission.locationAlways
|
||||
);
|
||||
if (position != null) {
|
||||
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [position.latitude, position.longitude],
|
||||
"gps_accuracy": position.accuracy,
|
||||
"battery": batteryLevel ?? 100
|
||||
}
|
||||
};
|
||||
Logger.d("[Foreground location] Sending data home...");
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(data)
|
||||
);
|
||||
if (response.statusCode >= 300) {
|
||||
Logger.e('Foreground location update error: ${response.body}');
|
||||
}
|
||||
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
|
||||
} else {
|
||||
Logger.d("[Foreground location] No location. Aborting.");
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void updateDeviceLocationIsolate() {
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||
//print("[Background $backgroundTask] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = data["webhookId"];
|
||||
String httpWebHost = data["httpWebHost"];
|
||||
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
|
||||
//print("[Background $backgroundTask] Getting path for log file...");
|
||||
//final logFileDirectory = await getExternalStorageDirectory();
|
||||
//print("[Background $backgroundTask] Opening log file...");
|
||||
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
|
||||
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
//print("[Background $backgroundTask] Getting battery level...");
|
||||
int batteryLevel;
|
||||
try {
|
||||
batteryLevel = await battery.batteryLevel;
|
||||
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||
} catch(e) {
|
||||
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||
batteryLevel = 0;
|
||||
//logData += 'Battery: error, $e';
|
||||
}
|
||||
if (batteryLevel != null) {
|
||||
data["data"]["battery"] = batteryLevel;
|
||||
//logData += 'Battery: success, $batteryLevel';
|
||||
}/* else {
|
||||
logData += 'Battery: error, level is null';
|
||||
}*/
|
||||
Position location;
|
||||
try {
|
||||
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||
if (location != null && location.latitude != null) {
|
||||
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
/*if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
logData += ' || Post: success, ${response.statusCode}';
|
||||
} else {
|
||||
logData += ' || Post: error, ${response.statusCode}';
|
||||
}*/
|
||||
} catch(e) {
|
||||
//logData += ' || Post: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += ' || Location: error, location is null';
|
||||
}*/
|
||||
} catch (e) {
|
||||
//print("[Background $backgroundTask] Location error: $e");
|
||||
//logData += ' || Location: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += 'Not configured';
|
||||
}*/
|
||||
//print("[Background $backgroundTask] Writing log data...");
|
||||
/*try {
|
||||
var fileMode;
|
||||
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
|
||||
fileMode = FileMode.append;
|
||||
} else {
|
||||
fileMode = FileMode.write;
|
||||
}
|
||||
await logFile.writeAsString('$logData\n', mode: fileMode);
|
||||
} catch (e) {
|
||||
print("[Background $backgroundTask] Error writing log: $e");
|
||||
}
|
||||
print("[Background $backgroundTask] Finished.");*/
|
||||
return true;
|
||||
});
|
||||
}
|
@ -56,7 +56,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
_fetchData(useCache: true);
|
||||
LocationManager();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
});
|
||||
|
@ -19,12 +19,10 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
105: "Passive (last known location)",
|
||||
};
|
||||
|
||||
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
int _activeLocationInterval = AppSettings().defaultActiveLocationUpdateIntervalSeconds;
|
||||
Duration _locationInterval;
|
||||
bool _locationTrackingEnabled = false;
|
||||
bool _foregroundLocationTrackingEnabled = false;
|
||||
bool _wait = false;
|
||||
bool _changedHere = false;
|
||||
bool _showNotification = true;
|
||||
int _accuracy = 102;
|
||||
|
||||
@override
|
||||
@ -39,75 +37,72 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
await prefs.reload();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? 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("location-updates-interval") ??
|
||||
AppSettings().defaultActiveLocationUpdateIntervalSeconds;
|
||||
if (_locationInterval < 15) {
|
||||
_locationInterval = 15;
|
||||
} else if (_locationInterval % 5 != 0) {
|
||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||
}
|
||||
_locationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0;
|
||||
_showNotification = prefs.getBool("location-updates-show-notification") ?? true;
|
||||
_locationInterval = Duration(seconds: prefs.getInt("location-updates-interval") ??
|
||||
AppSettings().defaultLocationUpdateIntervalSeconds);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _incLocationInterval() {
|
||||
if (_locationInterval < 720) {
|
||||
if (_locationInterval.inSeconds < 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + 5;
|
||||
_changedHere = true;
|
||||
_locationInterval = _locationInterval + Duration(seconds: 5);
|
||||
});
|
||||
} else if (_locationInterval.inMinutes < 15) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(minutes: 1);
|
||||
});
|
||||
} else if (_locationInterval.inMinutes < 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(minutes: 5);
|
||||
});
|
||||
} else if (_locationInterval.inHours < 4) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(minutes: 10);
|
||||
});
|
||||
} else if (_locationInterval.inHours < 48) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(hours: 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _decLocationInterval() {
|
||||
if (_locationInterval > 15) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - 5;
|
||||
_changedHere = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _incActiveLocationInterval() {
|
||||
if (_activeLocationInterval < 7200) {
|
||||
setState(() {
|
||||
_activeLocationInterval = _activeLocationInterval + 5;
|
||||
_changedHere = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _decActiveLocationInterval() {
|
||||
if (_activeLocationInterval > 5) {
|
||||
setState(() {
|
||||
_activeLocationInterval = _activeLocationInterval - 5;
|
||||
_changedHere = true;
|
||||
});
|
||||
if (_locationInterval.inSeconds > 5) {
|
||||
if (_locationInterval.inSeconds <= 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - Duration(seconds: 5);
|
||||
});
|
||||
} else if (_locationInterval.inMinutes <= 15) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 1);
|
||||
});
|
||||
} else if (_locationInterval.inMinutes <= 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 5);
|
||||
});
|
||||
} else if (_locationInterval.inHours <= 4) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 10);
|
||||
});
|
||||
} else if (_locationInterval.inHours > 4) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - Duration(hours: 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_switchLocationTrackingState(bool state) async {
|
||||
if (state) {
|
||||
await LocationManager().updateDeviceLocation();
|
||||
}
|
||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
setState(() {
|
||||
_wait = false;
|
||||
});
|
||||
}
|
||||
|
||||
_switchForegroundLocationTrackingState(bool state) async {
|
||||
await AppSettings().save({'location-updates-interval': _activeLocationInterval, 'location-updates-priority': _accuracy});
|
||||
await AppSettings().save({'location-updates-interval': _locationInterval.inSeconds, 'location-updates-priority': _accuracy, 'location-updates-show-notification': _showNotification});
|
||||
if (state) {
|
||||
try {
|
||||
await platform.invokeMethod('startLocationService');
|
||||
} catch (e) {
|
||||
_foregroundLocationTrackingEnabled = false;
|
||||
_locationTrackingEnabled = false;
|
||||
}
|
||||
} else {
|
||||
await platform.invokeMethod('stopLocationService');
|
||||
@ -117,18 +112,61 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
String _formatInterval() {
|
||||
String result = "";
|
||||
Duration leftToShow = Duration(seconds: _locationInterval?.inSeconds ?? 0);
|
||||
if (leftToShow.inHours > 0) {
|
||||
result += "${leftToShow.inHours} h ";
|
||||
leftToShow -= Duration(hours: leftToShow.inHours);
|
||||
}
|
||||
if (leftToShow.inMinutes > 0) {
|
||||
result += "${leftToShow.inMinutes} m";
|
||||
leftToShow -= Duration(hours: leftToShow.inMinutes);
|
||||
}
|
||||
if (leftToShow.inSeconds > 0) {
|
||||
result += "${leftToShow.inSeconds} s";
|
||||
leftToShow -= Duration(hours: leftToShow.inSeconds);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget _getNoteWidget(String text, bool important) {
|
||||
return Text(
|
||||
text,
|
||||
style: important ? Theme.of(context).textTheme.caption.copyWith(color: Theme.of(context).errorColor) : Theme.of(context).textTheme.caption,
|
||||
softWrap: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getNotes() {
|
||||
List<Widget> notes = [];
|
||||
if (_locationTrackingEnabled) {
|
||||
notes.add(_getNoteWidget('* Stop location tracking to change settings', false));
|
||||
}
|
||||
if ((_locationInterval?.inMinutes ?? 15) < 15) {
|
||||
notes.add(_getNoteWidget('* Notification is mandatory for location updates with interval less than every 15 minutes', false));
|
||||
if (_accuracy < 102) {
|
||||
notes.add(_getNoteWidget('* Battery consumption will be noticeable', true));
|
||||
}
|
||||
}
|
||||
if (notes.isEmpty) {
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: notes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
Text("Passive location tracking", style: Theme.of(context).textTheme.title),
|
||||
Text("Works in background not affecting phone battery. Usually sends last known device location. Can't be more frequent than once in 15 minutes.",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
softWrap: true,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable"),
|
||||
@ -145,64 +183,27 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Text("Send device location every"),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
//Expanded(child: Container(),),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("-", style: Theme.of(context).textTheme.title),
|
||||
onPressed: () => _decLocationInterval(),
|
||||
),
|
||||
Text("$_locationInterval minutes", style: Theme.of(context).textTheme.title),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("+", style: Theme.of(context).textTheme.title),
|
||||
onPressed: () => _incLocationInterval(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Text("Active location tracking", style: Theme.of(context).textTheme.title),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable"),
|
||||
Switch(
|
||||
value: _foregroundLocationTrackingEnabled,
|
||||
onChanged: _wait ? null : (value) {
|
||||
setState(() {
|
||||
_foregroundLocationTrackingEnabled = value;
|
||||
_wait = true;
|
||||
});
|
||||
_switchForegroundLocationTrackingState(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Text("Accuracy:", style: Theme.of(context).textTheme.body2),
|
||||
Container(height: Sizes.rowPadding),
|
||||
DropdownButton<int>(
|
||||
value: _accuracy,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
disabledHint: Text(locationAccuracy[_accuracy]),
|
||||
items: locationAccuracy.keys.map((value) {
|
||||
return new DropdownMenuItem<int>(
|
||||
value: value,
|
||||
child: Text('${locationAccuracy[value]}'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: _foregroundLocationTrackingEnabled ? null : (val) {
|
||||
onChanged: _locationTrackingEnabled ? null : (val) {
|
||||
setState(() {
|
||||
_accuracy = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Text("Update intervals"),
|
||||
Text("Update interval"),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@ -210,25 +211,43 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
//Expanded(child: Container(),),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("-", style: Theme.of(context).textTheme.title),
|
||||
onPressed: _foregroundLocationTrackingEnabled ? null : () => _decActiveLocationInterval(),
|
||||
child: Text("-", style: Theme.of(context).textTheme.headline4),
|
||||
onPressed: _locationTrackingEnabled ? null : () => _decLocationInterval(),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(_formatInterval(),
|
||||
textAlign: TextAlign.center,
|
||||
style: _locationTrackingEnabled ? Theme.of(context).textTheme.title.copyWith(color: HAClientTheme().getDisabledStateColor(context)) : Theme.of(context).textTheme.title),
|
||||
),
|
||||
Text("$_activeLocationInterval seconds",
|
||||
style: _foregroundLocationTrackingEnabled ? Theme.of(context).textTheme.title.copyWith(color: HAClientTheme().getDisabledStateColor(context)) : Theme.of(context).textTheme.title),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("+", style: Theme.of(context).textTheme.title),
|
||||
onPressed: _foregroundLocationTrackingEnabled ? null : () => _incActiveLocationInterval(),
|
||||
child: Text("+", style: Theme.of(context).textTheme.headline4),
|
||||
onPressed: _locationTrackingEnabled ? null : () => _incLocationInterval(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Show notification"),
|
||||
Switch(
|
||||
value: _showNotification,
|
||||
onChanged: (_locationTrackingEnabled || (_locationInterval?.inMinutes ?? 0) < 15) ? null : (value) {
|
||||
setState(() {
|
||||
_showNotification = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
_getNotes()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 1.2.0+1200
|
||||
version: 1.3.0+1300
|
||||
|
||||
|
||||
environment:
|
||||
@ -26,9 +26,6 @@ dependencies:
|
||||
hive: ^1.4.1+1
|
||||
hive_flutter: ^0.3.0+2
|
||||
device_info: ^0.4.2+4
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
battery: ^1.0.0
|
||||
firebase_crashlytics: ^0.1.3+3
|
||||
syncfusion_flutter_core: ^18.1.52
|
||||
syncfusion_flutter_gauges: ^18.1.52
|
||||
|
Reference in New Issue
Block a user