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: