diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fb61996..688d50b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -60,6 +60,10 @@
android:name=".LocationUpdatesService"
android:enabled="true"
android:exported="false" />
+
@@ -72,9 +76,10 @@
-
+
+
= Build.VERSION_CODES.O) {
+ CharSequence name = "Location requests";
+ NotificationChannel mChannel =
+ new NotificationChannel(LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
+
+ mNotificationManager.createNotificationChannel(mChannel);
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i(TAG, "Service started. startId="+startId);
+
+ requestLocationUpdates();
+
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ try {
+ mFusedLocationClient.removeLocationUpdates(mLocationCallback);
+ } catch (SecurityException unlikely) {
+ //When we lost permission
+ Log.i(TAG, "No location permission");
+ }
+ mServiceHandler.removeCallbacksAndMessages(null);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void requestLocationUpdates() {
+ Log.i(TAG, "Requesting location update in 5 seconds.");
+ mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
+ mLocationRequest.setInterval(5000);
+
+ startForeground(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(this, null, LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID));
+ try {
+ mFusedLocationClient.requestLocationUpdates(mLocationRequest,
+ mLocationCallback, Looper.myLooper());
+ } catch (SecurityException unlikely) {
+ stopSelf();
+ }
+ }
+
+ private void onNewLocation(Location location) {
+ Log.i(TAG, "New location: " + location);
+
+ mNotificationManager.notify(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(
+ this,
+ location,
+ LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID
+ ));
+
+ 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)
+ .setConstraints(constraints)
+ .setInputData(locationData)
+ .build();
+
+ WorkManager
+ .getInstance(getApplicationContext())
+ .enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
+ stopSelf();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesService.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesService.java
index 975bdf8..fed23c9 100644
--- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesService.java
+++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesService.java
@@ -12,7 +12,6 @@ import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.Nullable;
-import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
@@ -28,8 +27,6 @@ import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
-import java.util.concurrent.TimeUnit;
-
public class LocationUpdatesService extends Service {
private static final String TAG = LocationUpdatesService.class.getSimpleName();
@@ -77,7 +74,7 @@ public class LocationUpdatesService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- Log.i(TAG, "Service started");
+ Log.i(TAG, "Service started. startId="+startId);
requestLocationUpdates();
@@ -103,11 +100,17 @@ public class LocationUpdatesService extends Service {
private void requestLocationUpdates() {
long requestInterval = LocationUtils.getLocationUpdateIntervals(getApplicationContext());
- int priority = LocationUtils.getLocationUpdatesPriority(getApplicationContext());
+ int priority;
+ if (requestInterval >= 600000) {
+ mLocationRequest.setFastestInterval(60000);
+ priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
+ } else {
+ priority = LocationRequest.PRIORITY_HIGH_ACCURACY;
+ }
Log.i(TAG, "Requesting location updates. Every " + requestInterval + "ms with priority of " + priority);
mLocationRequest.setPriority(priority);
mLocationRequest.setInterval(requestInterval);
- mLocationRequest.setFastestInterval(requestInterval);
+
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
try {
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
@@ -120,7 +123,11 @@ public class LocationUpdatesService extends Service {
private void onNewLocation(Location location) {
Log.i(TAG, "New location: " + location);
- mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, location, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
+ mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(
+ this,
+ location,
+ LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID
+ ));
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
@@ -136,10 +143,6 @@ public class LocationUpdatesService extends Service {
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
- .setBackoffCriteria(
- BackoffPolicy.EXPONENTIAL,
- 10,
- TimeUnit.SECONDS)
.setConstraints(constraints)
.setInputData(locationData)
.build();
diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesWorker.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesWorker.java
index 7dcee13..1ed5db3 100644
--- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesWorker.java
+++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesWorker.java
@@ -1,6 +1,5 @@
package com.keyboardcrumbs.hassclient;
-import android.app.AlarmManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
@@ -10,7 +9,6 @@ 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;
@@ -29,8 +27,6 @@ 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;
@@ -73,10 +69,6 @@ public class LocationUpdatesWorker extends ListenableWorker {
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
- .setBackoffCriteria(
- BackoffPolicy.EXPONENTIAL,
- 10,
- TimeUnit.SECONDS)
.setConstraints(constraints)
.setInputData(locationData)
.build();
@@ -106,10 +98,8 @@ public class LocationUpdatesWorker extends ListenableWorker {
};
LocationRequest locationRequest = new LocationRequest();
- int accuracy = LocationUtils.getLocationUpdatesPriority(getApplicationContext());
- locationRequest.setPriority(accuracy);
+ locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
locationRequest.setInterval(5000);
- locationRequest.setFastestInterval(1000);
try {
fusedLocationClient.requestLocationUpdates(locationRequest,
callback, Looper.myLooper());
diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUtils.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUtils.java
index 60521e7..e651176 100644
--- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUtils.java
+++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUtils.java
@@ -9,6 +9,8 @@ import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
@@ -20,21 +22,25 @@ 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 ONETIME_NOTIFICATION_CHANNEL_ID = "location_request";
+ static final int ONETIME_NOTIFICATION_ID = 954333;
+
+ static final String REQUEST_LOCATION_NOTIFICATION = "request_location_update";
static final String LOCATION_WORK_NAME = "HALocationWorker";
+ static final String LOCATION_REQUEST_NAME = "HALocationRequest";
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 int DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
static int getLocationUpdatesState(Context context) {
@@ -42,11 +48,7 @@ class LocationUtils {
}
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);
+ return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_MS);
}
static boolean showNotification(Context context) {
@@ -60,13 +62,12 @@ class LocationUtils {
.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 setLocationUpdatesSettings(Context context, long interval, boolean showNotification) {
+ context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(KEY_LOCATION_SHOW_NOTIFICATION, showNotification)
+ .putLong(KEY_LOCATION_UPDATE_INTERVAL, interval)
+ .apply();
}
static void startService(Context context) {
@@ -83,28 +84,64 @@ 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();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
}
+
+ static void requestLocationOnce(Context context) {
+ Intent myService = new Intent(context, LocationRequestService.class);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(myService);
+ } else {
+ context.startService(myService);
+ }
+ //OneTimeWorkRequest oneTimeWork = new OneTimeWorkRequest.Builder(LocationUpdatesWorker.class)
+ // .build();
+ //WorkManager.getInstance(context).enqueueUniqueWork(LocationUtils.LOCATION_REQUEST_NAME, ExistingWorkPolicy.REPLACE, oneTimeWork);
+ }
+
+ static Notification getNotification(Context context, Location location, String channelId) {
+ CharSequence title = "Location tracking";
+ CharSequence text = location == null ? "Accuracy: unknown" : "Accuracy: " + location.getAccuracy() + " m";
+ CharSequence bigText = location == null ? "Waiting for location..." : "Time: " + DateFormat.getDateTimeInstance().format(new Date(location.getTime())) +
+ System.getProperty("line.separator") + "Accuracy: " + location.getAccuracy() + " m" +
+ System.getProperty("line.separator") + "Location: " + location.getLatitude() + ", " + location.getLongitude();
+
+ PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
+ new Intent(context, MainActivity.class), 0);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
+ .setContentIntent(activityPendingIntent)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(bigText))
+ .setPriority(-1)
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.mini_icon_location)
+ .setWhen(System.currentTimeMillis());
+
+ return builder.build();
+ }
+
+ static Notification getRequestNotification(Context context, Location location, String channelId) {
+ CharSequence title = "Updating location...";
+ CharSequence text = location == null ? "Waiting for location..." : "Accuracy: " + location.getAccuracy() + " m";
+
+ PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
+ new Intent(context, MainActivity.class), 0);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
+ .setContentIntent(activityPendingIntent)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setPriority(-1)
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.mini_icon_location)
+ .setWhen(System.currentTimeMillis());
+
+ return builder.build();
+ }
}
diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java
index a598237..f8d07b3 100644
--- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java
+++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/MainActivity.java
@@ -14,6 +14,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.location.Location;
import android.os.Bundle;
import io.flutter.plugin.common.MethodChannel;
@@ -29,7 +30,7 @@ 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;
+ private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_MS;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
@@ -39,35 +40,42 @@ public class MainActivity extends FlutterActivity {
Context context = getActivity();
switch (call.method) {
case "getFCMToken":
- if (checkPlayServices()) {
- FirebaseInstanceId.getInstance().getInstanceId()
- .addOnCompleteListener(task -> {
- if (task.isSuccessful()) {
- String token = task.getResult().getToken();
- context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
- result.success(token);
- } else {
- Exception ex = task.getException();
- if (ex != null) {
- result.error("fcm_error", ex.getMessage(), null);
+ try {
+ if (checkPlayServices()) {
+ FirebaseInstanceId.getInstance().getInstanceId()
+ .addOnCompleteListener(task -> {
+ if (task.isSuccessful()) {
+ String token = task.getResult().getToken();
+ context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
+ result.success(token);
} else {
- result.error("fcm_error", "Unknown", null);
- }
+ Exception ex = task.getException();
+ if (ex != null) {
+ result.error("fcm_error", ex.getMessage(), null);
+ } else {
+ result.error("fcm_error", "Unknown", null);
+ }
- }
- });
- } else {
- result.error("google_play_service_error", "Google Play Services unavailable", null);
+ }
+ });
+ } else {
+ result.error("google_play_service_error", "Google Play Services unavailable", null);
+ }
+ } catch (Exception e) {
+ result.error("get_token_exception", e.getMessage(), e);
}
break;
case "startLocationService":
try {
- locationUpdatesInterval = LocationUtils.getLocationUpdateIntervals(this);
- if (locationUpdatesInterval >= LocationUtils.MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS) {
- locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
- } else {
+ locationUpdatesInterval = ((Number)call.argument("location-updates-interval")).longValue();
+ boolean useForegroundService = (boolean)call.argument("foreground-location-tracking");
+
+ if (useForegroundService) {
locationUpdatesType = LocationUtils.LOCATION_UPDATES_SERVICE;
+ } else {
+ locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
}
+ LocationUtils.setLocationUpdatesSettings(this, locationUpdatesInterval, (boolean)call.argument("location-updates-show-notification"));
if (isNoLocationPermissions()) {
requestLocationPermissions();
} else {
@@ -75,12 +83,16 @@ public class MainActivity extends FlutterActivity {
}
result.success("");
} catch (Exception e) {
- result.error("location_error", e.getMessage(), null);
+ result.error("location_error", e.getMessage(), e);
}
break;
case "stopLocationService":
- stopLocationUpdates();
- result.success("");
+ try {
+ stopLocationUpdates();
+ result.success("");
+ } catch (Exception e) {
+ result.error("location_error", e.getMessage(), e);
+ }
break;
case "cancelOldLocationWorker":
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/MessagingService.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/MessagingService.java
index 525588d..7055e30 100644
--- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/MessagingService.java
+++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/MessagingService.java
@@ -22,11 +22,14 @@ import com.google.firebase.messaging.RemoteMessage;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.util.Log;
import android.webkit.URLUtil;
public class MessagingService extends FirebaseMessagingService {
+ private static final String TAG = MessagingService.class.getSimpleName();
+
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
@Override
@@ -47,6 +50,16 @@ public class MessagingService extends FirebaseMessagingService {
private void sendNotification(Map data) {
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
boolean autoCancel;
+ if (!data.containsKey("body")) {
+ messageBody = "";
+ } else {
+ messageBody = data.get("body");
+ }
+ if (messageBody != null && messageBody.equals(LocationUtils.REQUEST_LOCATION_NOTIFICATION)) {
+ Log.d(TAG, "Location update request received");
+ LocationUtils.requestLocationOnce(this);
+ return;
+ }
String customChannelId = data.get("channelId");
if (customChannelId == null) {
channelId = "ha_notify";
@@ -55,11 +68,6 @@ public class MessagingService extends FirebaseMessagingService {
channelId = customChannelId;
channelDescription = channelId;
}
- if (!data.containsKey("body")) {
- messageBody = "";
- } else {
- messageBody = data.get("body");
- }
if (!data.containsKey("title")) {
messageTitle = "HA Client";
} else {
@@ -106,12 +114,16 @@ public class MessagingService extends FirebaseMessagingService {
.setAutoCancel(autoCancel)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
+ Bitmap image = null;
if (URLUtil.isValidUrl(imageUrl)) {
- Bitmap image = getBitmapFromURL(imageUrl);
- if (image != null) {
- notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
- notificationBuilder.setLargeIcon(image);
- }
+ image = getBitmapFromURL(imageUrl);
+ }
+ if (image != null) {
+ notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
+ notificationBuilder.setLargeIcon(image);
+ } else {
+ notificationBuilder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(messageBody));
}
for (int i = 1; i <= 3; i++) {
if (data.containsKey("action" + i)) {
diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesAfterReboot.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/RestartLocationUpdate.java
similarity index 56%
rename from android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesAfterReboot.java
rename to android/app/src/main/java/com/keyboardcrumbs/hassclient/RestartLocationUpdate.java
index 68029c1..3817f0a 100644
--- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/LocationUpdatesAfterReboot.java
+++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/RestartLocationUpdate.java
@@ -4,10 +4,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-public class LocationUpdatesAfterReboot extends BroadcastReceiver {
+public class RestartLocationUpdate extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
- if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE && Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction())) {
+ if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE &&
+ (Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()) || Intent.ACTION_MY_PACKAGE_REPLACED.equalsIgnoreCase(intent.getAction()))) {
LocationUtils.startServiceFromBroadcast(context);
}
}
diff --git a/android/app/src/main/res/drawable/mini_icon_location.png b/android/app/src/main/res/drawable/mini_icon_location.png
new file mode 100644
index 0000000..08dbb4e
Binary files /dev/null and b/android/app/src/main/res/drawable/mini_icon_location.png differ
diff --git a/lib/cards/card.class.dart b/lib/cards/card.class.dart
index b7c6b73..98bc6ef 100644
--- a/lib/cards/card.class.dart
+++ b/lib/cards/card.class.dart
@@ -21,7 +21,6 @@ class CardData {
switch (rawData['type']) {
case CardType.ENTITIES:
case CardType.HISTORY_GRAPH:
- case CardType.MAP:
case CardType.PICTURE_GLANCE:
case CardType.SENSOR:
case CardType.ENTITY:
@@ -47,6 +46,9 @@ class CardData {
return CardData(null);
}
break;
+ case CardType.MAP:
+ return MapCardData(rawData);
+ break;
case CardType.ENTITY_BUTTON:
case CardType.BUTTON:
case CardType.PICTURE_ENTITY:
@@ -644,6 +646,52 @@ class MarkdownCardData extends CardData {
}
+class MapCardData extends CardData {
+
+ String title;
+
+ @override
+ Widget buildCardWidget() {
+ return MapCard(card: this);
+ }
+
+ MapCardData(rawData) : super(rawData) {
+ //Parsing card data
+ title = rawData['title'];
+ List geoLocationSources = rawData['geo_location_sources'] ?? [];
+ if (geoLocationSources.isNotEmpty) {
+ //TODO add entities by source
+ }
+ var rawEntities = rawData["entities"] ?? [];
+ rawEntities.forEach((rawEntity) {
+ if (rawEntity is String) {
+ if (HomeAssistant().entities.isExist(rawEntity)) {
+ entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
+ } else {
+ entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
+ }
+ } else {
+ if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
+ Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
+ entities.add(
+ EntityWrapper(
+ entity: e,
+ stateColor: stateColor,
+ overrideName: rawEntity["name"]?.toString(),
+ overrideIcon: rawEntity["icon"],
+ stateFilter: rawEntity['state_filter'] ?? [],
+ uiAction: EntityUIAction(rawEntityData: rawEntity)
+ )
+ );
+ } else {
+ entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
+ }
+ }
+ });
+ }
+
+}
+
class MediaControlCardData extends CardData {
@override
diff --git a/lib/cards/map_card.dart b/lib/cards/map_card.dart
new file mode 100644
index 0000000..f298bfe
--- /dev/null
+++ b/lib/cards/map_card.dart
@@ -0,0 +1,88 @@
+part of '../main.dart';
+
+class MapCard extends StatefulWidget {
+ final MapCardData card;
+
+ const MapCard({Key key, this.card}) : super(key: key);
+
+ @override
+ _MapCardState createState() => _MapCardState();
+}
+
+class _MapCardState extends State {
+
+ void _openMap(BuildContext context) {
+ Navigator.of(context).push(MaterialPageRoute(
+ builder: (bc) {
+ return Scaffold(
+ primary: false,
+ /*appBar: new AppBar(
+ backgroundColor: Colors.transparent,
+ leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
+ Navigator.pop(context);
+ }),
+ actions: [
+ IconButton(
+ icon: Icon(Icons.fullscreen),
+ onPressed: () {},
+ )
+ ],
+ // Here we take the value from the MyHomePage object that was created by
+ // the App.build method, and use it to set our appbar title.
+ title: new Text("${widget.card.title ?? ""}"),
+ ),*/
+ body: Container(
+ color: Theme.of(context).primaryColor,
+ child: SafeArea(
+ child: Stack(
+ children: [
+ EntitiesMap(
+ entities: widget.card.entities,
+ interactive: true
+ ),
+ Positioned(
+ top: 0,
+ left: 0,
+ child: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
+ Navigator.pop(context);
+ })
+ )
+ ],
+ )
+ )
+ )
+ );
+ }
+ ));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return CardWrapper(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ CardHeader(name: widget.card.title),
+ Stack(
+ children: [
+ GestureDetector(
+ onTap: () => _openMap(context),
+ child: EntitiesMap(
+ aspectRatio: 1,
+ interactive: false,
+ entities: widget.card.entities,
+ )
+ ),
+ Positioned(
+ bottom: 0,
+ left: 0,
+ child: Text('Tap to open interactive map', style: Theme.of(context).textTheme.caption)
+ )
+ ],
+ ),
+ ],
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/cards/widgets/entities_map.dart b/lib/cards/widgets/entities_map.dart
new file mode 100644
index 0000000..2ed5faa
--- /dev/null
+++ b/lib/cards/widgets/entities_map.dart
@@ -0,0 +1,73 @@
+part of '../../main.dart';
+
+
+class EntitiesMap extends StatelessWidget {
+
+ final List entities;
+ final bool interactive;
+ final double aspectRatio;
+ final LatLng center;
+ final double zoom;
+
+ const EntitiesMap({Key key, this.entities: const [], this.aspectRatio, this.interactive: true, this.center, this.zoom}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ List markers = [];
+ List points = [];
+ entities.forEach((entityWrapper) {
+ double lat = entityWrapper.entity._getDoubleAttributeValue("latitude");
+ double long = entityWrapper.entity._getDoubleAttributeValue("longitude");
+ if (lat != null && long != null) {
+ points.add(LatLng(lat, long));
+ markers.add(
+ Marker(
+ width: 36,
+ height: 36,
+ point: LatLng(lat, long),
+ builder: (ctx) => EntityModel(
+ handleTap: true,
+ entityWrapper: entityWrapper,
+ child: EntityIcon(
+ size: 36,
+ ),
+ )
+ )
+ );
+ }
+ });
+ MapOptions mapOptions;
+ if (center != null) {
+ mapOptions = MapOptions(
+ interactive: interactive,
+ center: center,
+ zoom: zoom ?? 10,
+ );
+ } else {
+ mapOptions = MapOptions(
+ interactive: interactive,
+ bounds: LatLngBounds.fromPoints(points),
+ boundsOptions: FitBoundsOptions(padding: EdgeInsets.all(40)),
+ );
+ }
+ Widget map = FlutterMap(
+ options: mapOptions,
+ layers: [
+ new TileLayerOptions(
+ urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ subdomains: ['a', 'b', 'c']
+ ),
+ new MarkerLayerOptions(
+ markers: markers,
+ ),
+ ],
+ );
+ if (aspectRatio != null) {
+ return AspectRatio(
+ aspectRatio: aspectRatio,
+ child: map
+ );
+ }
+ return map;
+ }
+}
\ No newline at end of file
diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart
index 885ff9d..4f2ae23 100644
--- a/lib/home_assistant.class.dart
+++ b/lib/home_assistant.class.dart
@@ -278,6 +278,7 @@ class HomeAssistant {
_rawPanels = data;
List dashboards = [];
data.forEach((k,v) {
+ Logger.d('[HA] Panel $k: title=${v['title']}; component=${v['component_name']}');
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
dashboards.add(
diff --git a/lib/main.dart b/lib/main.dart
index 3539c9a..92d0e13 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -27,12 +27,16 @@ import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standalon
import 'package:webview_flutter/webview_flutter.dart';
import 'package:syncfusion_flutter_core/core.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
+import 'package:flutter_map/flutter_map.dart';
+import 'package:latlong/latlong.dart';
+import 'package:flutter/gestures.dart';
import 'utils/logger.dart';
import '.secrets.dart';
part 'const.dart';
part 'utils/launcher.dart';
+part 'utils/RandomColorGenerator.dart';
part 'entities/entity.class.dart';
part 'entities/entity_wrapper.class.dart';
part 'entities/timer/timer_entity.class.dart';
@@ -45,6 +49,7 @@ part 'entities/date_time/date_time_entity.class.dart';
part 'entities/light/light_entity.class.dart';
part 'entities/select/select_entity.class.dart';
part 'entities/sun/sun_entity.class.dart';
+part 'cards/widgets/entities_map.dart';
part 'entities/sensor/sensor_entity.class.dart';
part 'entities/slider/slider_entity.dart';
part 'entities/media_player/media_player_entity.class.dart';
@@ -58,6 +63,7 @@ part 'entities/entity_model.widget.dart';
part 'entities/default_entity_container.widget.dart';
part 'entities/missed_entity.widget.dart';
part 'cards/entity_button_card.dart';
+part 'cards/map_card.dart';
part 'pages/widgets/entity_attributes_list.dart';
part 'entities/entity_icon.widget.dart';
part 'entities/entity_name.widget.dart';
@@ -154,7 +160,7 @@ part 'managers/app_settings.dart';
EventBus eventBus = new EventBus();
const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
-const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.2.0.md';
+const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.3.0.md';
Future _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.
diff --git a/lib/managers/app_settings.dart b/lib/managers/app_settings.dart
index 9a181a9..9754340 100644
--- a/lib/managers/app_settings.dart
+++ b/lib/managers/app_settings.dart
@@ -33,7 +33,6 @@ class AppSettings {
bool nextAlarmSensorCreated = false;
DisplayMode displayMode;
AppTheme appTheme;
- final int defaultLocationUpdateIntervalSeconds = 900;
bool get isAuthenticated => longLivedToken != null;
bool get isTempAuthenticated => tempToken != null;
@@ -75,21 +74,22 @@ class AppSettings {
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
+ await platform.invokeMethod('startLocationService', {
+ 'location-updates-interval': oldLocationTrackingInterval * 60 * 1000,
+ //'location-updates-priority': 100,
+ 'location-updates-show-notification': true,
+ 'foreground-location-tracking': false
+ });
+ } catch (e, stack) {
+ Logger.e("[MIGRATION] Can't start new location tracking: $e", stacktrace: stack);
}
} 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);
}
diff --git a/lib/pages/main/main.page.dart b/lib/pages/main/main.page.dart
index 03de9c3..a726915 100644
--- a/lib/pages/main/main.page.dart
+++ b/lib/pages/main/main.page.dart
@@ -408,7 +408,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
- Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
+ //Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
diff --git a/lib/pages/settings/integration_settings.part.dart b/lib/pages/settings/integration_settings.part.dart
index 972f704..e0379d9 100644
--- a/lib/pages/settings/integration_settings.part.dart
+++ b/lib/pages/settings/integration_settings.part.dart
@@ -12,18 +12,17 @@ class IntegrationSettingsPage extends StatefulWidget {
class _IntegrationSettingsPageState extends State {
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)",
- };
+ /*static final locationAccuracy = {
+ 100: "High",
+ 102: "Balanced"
+ };*/
Duration _locationInterval;
bool _locationTrackingEnabled = false;
bool _wait = false;
bool _showNotification = true;
- int _accuracy = 102;
+ //int _accuracy = 100;
+ bool _useForegroundService = false;
@override
void initState() {
@@ -37,11 +36,12 @@ class _IntegrationSettingsPageState extends State {
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
- _accuracy = prefs.getInt("location-updates-priority") ?? 102;
+ //_accuracy = prefs.getInt("location-updates-priority") ?? 100;
_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);
+ _useForegroundService = prefs.getBool("foreground-location-tracking") ?? false;
+ _locationInterval = Duration(milliseconds: prefs.getInt("location-updates-interval") ??
+ 900000);
});
});
}
@@ -71,36 +71,32 @@ class _IntegrationSettingsPageState extends State {
}
void _decLocationInterval() {
- if (_locationInterval.inSeconds > 5) {
- if (_locationInterval.inSeconds <= 60) {
- setState(() {
+ if ((_useForegroundService && _locationInterval.inSeconds > 5) || (!_useForegroundService && _locationInterval.inMinutes > 15)) {
+ setState(() {
+ if (_locationInterval.inSeconds <= 60) {
_locationInterval = _locationInterval - Duration(seconds: 5);
- });
- } else if (_locationInterval.inMinutes <= 15) {
- setState(() {
+ } else if (_locationInterval.inMinutes <= 15) {
_locationInterval = _locationInterval - Duration(minutes: 1);
- });
- } else if (_locationInterval.inMinutes <= 60) {
- setState(() {
+ } else if (_locationInterval.inMinutes <= 60) {
_locationInterval = _locationInterval - Duration(minutes: 5);
- });
- } else if (_locationInterval.inHours <= 4) {
- setState(() {
+ } else if (_locationInterval.inHours <= 4) {
_locationInterval = _locationInterval - Duration(minutes: 10);
- });
- } else if (_locationInterval.inHours > 4) {
- setState(() {
+ } else if (_locationInterval.inHours > 4) {
_locationInterval = _locationInterval - Duration(hours: 1);
- });
- }
+ }
+ });
}
}
_switchLocationTrackingState(bool state) async {
- await AppSettings().save({'location-updates-interval': _locationInterval.inSeconds, 'location-updates-priority': _accuracy, 'location-updates-show-notification': _showNotification});
if (state) {
try {
- await platform.invokeMethod('startLocationService');
+ await platform.invokeMethod('startLocationService', {
+ 'location-updates-interval': _locationInterval.inMilliseconds,
+ //'location-updates-priority': _accuracy,
+ 'foreground-location-tracking': _useForegroundService,
+ 'location-updates-show-notification': _showNotification
+ });
} catch (e) {
_locationTrackingEnabled = false;
}
@@ -143,11 +139,13 @@ class _IntegrationSettingsPageState extends State {
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 (_useForegroundService) {
+ notes.add(_getNoteWidget('* Notification is mandatory for foreground service', false));
+ } else {
+ notes.add(_getNoteWidget('* Use foreground service for intervals less then 15 minutes', false));
+ }
+ if (_useForegroundService && _locationInterval.inMinutes < 10) {
+ notes.add(_getNoteWidget('* Battery consumption will be noticeable', true));
}
if (notes.isEmpty) {
return Container(width: 0, height: 0);
@@ -183,7 +181,26 @@ class _IntegrationSettingsPageState extends State {
],
),
Container(height: Sizes.rowPadding),
- Text("Accuracy:", style: Theme.of(context).textTheme.body2),
+ Row(
+ children: [
+ Text("Use foreground service"),
+ Switch(
+ value: _useForegroundService,
+ onChanged: _locationTrackingEnabled ? null : (value) {
+ setState(() {
+ _useForegroundService = value;
+ if (!_useForegroundService && _locationInterval.inMinutes < 15) {
+ _locationInterval = Duration(minutes: 15);
+ } else if (_useForegroundService) {
+ _showNotification = true;
+ }
+ });
+ },
+ ),
+ ],
+ ),
+ Container(height: Sizes.rowPadding),
+ /*Text("Accuracy:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton(
value: _accuracy,
@@ -202,7 +219,7 @@ class _IntegrationSettingsPageState extends State {
});
},
),
- Container(height: Sizes.rowPadding),
+ Container(height: Sizes.rowPadding),*/
Text("Update interval"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -232,7 +249,7 @@ class _IntegrationSettingsPageState extends State {
Text("Show notification"),
Switch(
value: _showNotification,
- onChanged: (_locationTrackingEnabled || (_locationInterval?.inMinutes ?? 0) < 15) ? null : (value) {
+ onChanged: (_locationTrackingEnabled || _useForegroundService) ? null : (value) {
setState(() {
_showNotification = value;
});
diff --git a/lib/panels/panel_class.dart b/lib/panels/panel_class.dart
index 745bdb1..bde9040 100644
--- a/lib/panels/panel_class.dart
+++ b/lib/panels/panel_class.dart
@@ -39,7 +39,7 @@ class Panel {
eventBus.fire(ReloadUIEvent());
});
} else {
- Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "${this.title}");
+ Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "Back to app");
}
}
diff --git a/lib/utils/RandomColorGenerator.dart b/lib/utils/RandomColorGenerator.dart
new file mode 100644
index 0000000..01dcfec
--- /dev/null
+++ b/lib/utils/RandomColorGenerator.dart
@@ -0,0 +1,28 @@
+part of '../main.dart';
+
+class RandomColorGenerator {
+ static const colorsList = [
+ Colors.green,
+ Colors.purple,
+ Colors.indigo,
+ Colors.red,
+ Colors.orange,
+ Colors.cyan
+ ];
+
+ int _index = 0;
+
+ Color getCurrent() {
+ return colorsList[_index];
+ }
+
+ Color getNext() {
+ if (_index < colorsList.length - 1) {
+ _index += 1;
+ } else {
+ _index = 1;
+ }
+ return getCurrent();
+ }
+
+}
\ No newline at end of file
diff --git a/lib/view.class.dart b/lib/view.class.dart
index 599fc2c..78f4fcc 100644
--- a/lib/view.class.dart
+++ b/lib/view.class.dart
@@ -74,7 +74,7 @@ class HAView {
Widget build(BuildContext context) {
return ViewWidget(
- view: this,
+ view: this
);
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index a0b30dc..78ab137 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
-version: 1.3.0+1300
+version: 1.3.0+1309
environment:
@@ -27,8 +27,9 @@ dependencies:
hive_flutter: ^0.3.0+2
device_info: ^0.4.2+4
firebase_crashlytics: ^0.1.3+3
- syncfusion_flutter_core: ^18.1.52
- syncfusion_flutter_gauges: ^18.1.52
+ syncfusion_flutter_core: ^18.2.44
+ syncfusion_flutter_gauges: ^18.2.44
+ flutter_map: ^0.10.1
dev_dependencies: