Compare commits
34 Commits
release/1.
...
rc/1.3.0-b
Author | SHA1 | Date | |
---|---|---|---|
8f4edd5448 | |||
10f4c71a49 | |||
a663838283 | |||
3d27f20798 | |||
f7d8bf9ae7 | |||
9b0b90dba1 | |||
5ab34cd32f | |||
f4b6d7a332 | |||
f87cff7a7e | |||
d2d037e468 | |||
bf7983d72e | |||
40eb564c29 | |||
bc72956365 | |||
595406bb7e | |||
01b8ec9b97 | |||
2126bc4f02 | |||
d94cdf0d1b | |||
3215556440 | |||
a8e7ab6f06 | |||
5aa6171c50 | |||
2fb296b7a8 | |||
e5fe853f0b | |||
819dfc725d | |||
ccbc2eec47 | |||
dce93966e3 | |||
3a7f3db6cd | |||
f39dbe3b24 | |||
2ec549af7e | |||
7ebaf736b5 | |||
71dcf16d70 | |||
6ba1e88b09 | |||
08e1327a29 | |||
da8efcd43f | |||
7ebf5f7c18 |
@ -80,7 +80,9 @@ 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'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
|
@ -56,6 +56,14 @@
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".LocationUpdatesService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".LocationRequestService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
@ -68,6 +76,12 @@
|
||||
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".RestartLocationUpdate">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
|
@ -0,0 +1,146 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
|
||||
public class LocationRequestService extends Service {
|
||||
|
||||
private static final String TAG = LocationRequestService.class.getSimpleName();
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
|
||||
private FusedLocationProviderClient mFusedLocationClient;
|
||||
|
||||
private LocationCallback mLocationCallback;
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
public LocationRequestService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
mLocationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
onNewLocation(locationResult.getLastLocation());
|
||||
}
|
||||
};
|
||||
|
||||
mLocationRequest = new LocationRequest();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
|
||||
public class LocationUpdatesService extends Service {
|
||||
|
||||
private static final String TAG = LocationUpdatesService.class.getSimpleName();
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
|
||||
private FusedLocationProviderClient mFusedLocationClient;
|
||||
|
||||
private LocationCallback mLocationCallback;
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
public LocationUpdatesService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
mLocationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
onNewLocation(locationResult.getLastLocation());
|
||||
}
|
||||
};
|
||||
|
||||
mLocationRequest = new LocationRequest();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.SERVICE_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() {
|
||||
long requestInterval = LocationUtils.getLocationUpdateIntervals(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);
|
||||
|
||||
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_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.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(
|
||||
this,
|
||||
location,
|
||||
LocationUtils.SERVICE_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);
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
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;
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.ListenableWorker;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocationUpdatesWorker extends ListenableWorker {
|
||||
|
||||
private Context currentContext;
|
||||
private LocationCallback callback;
|
||||
private FusedLocationProviderClient fusedLocationClient;
|
||||
|
||||
public LocationUpdatesWorker(Context context, WorkerParameters params) {
|
||||
super(context, params);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
fusedLocationClient.removeLocationUpdates(callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableFuture<Result> startWork() {
|
||||
return CallbackToFutureAdapter.getFuture(completer -> {
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(currentContext);
|
||||
|
||||
callback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
Location location = locationResult.getLastLocation();
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
|
||||
LocationRequest locationRequest = new LocationRequest();
|
||||
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
||||
locationRequest.setInterval(5000);
|
||||
try {
|
||||
fusedLocationClient.requestLocationUpdates(locationRequest,
|
||||
callback, Looper.myLooper());
|
||||
} catch (SecurityException e) {
|
||||
completer.setException(e);
|
||||
}
|
||||
return callback;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
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.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class LocationUtils {
|
||||
|
||||
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.location-updates-state";
|
||||
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.location-updates-interval";
|
||||
static final String KEY_LOCATION_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_MS = 900000; //15 minutes
|
||||
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
||||
|
||||
static int getLocationUpdatesState(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getInt(KEY_REQUESTING_LOCATION_UPDATES, LOCATION_UPDATES_DISABLED);
|
||||
}
|
||||
|
||||
static long getLocationUpdateIntervals(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
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()
|
||||
.putInt(KEY_REQUESTING_LOCATION_UPDATES, locationUpdatesState)
|
||||
.apply();
|
||||
}
|
||||
|
||||
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) {
|
||||
Intent myService = new Intent(context, LocationUpdatesService.class);
|
||||
context.startService(myService);
|
||||
}
|
||||
|
||||
static void startServiceFromBroadcast(Context context) {
|
||||
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent);
|
||||
} else {
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
static void startWorker(Context context, long interval) {
|
||||
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,61 +1,105 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
import com.google.firebase.iid.InstanceIdResult;
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
||||
|
||||
|
||||
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
|
||||
|
||||
private int locationUpdatesType = LocationUtils.LOCATION_UPDATES_DISABLED;
|
||||
private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_MS;
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
||||
new MethodChannel.MethodCallHandler() {
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
(call, result) -> {
|
||||
Context context = getActivity();
|
||||
if (call.method.equals("getFCMToken")) {
|
||||
if (checkPlayServices()) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
||||
if (task.isSuccessful()) {
|
||||
String token = task.getResult().getToken();
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
|
||||
updateTokenTask.execute(token);
|
||||
result.success(token);
|
||||
} else {
|
||||
result.error("fcm_error", task.getException().getMessage(), null);
|
||||
}
|
||||
switch (call.method) {
|
||||
case "getFCMToken":
|
||||
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 {
|
||||
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 = ((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 {
|
||||
startLocationUpdates();
|
||||
}
|
||||
result.success("");
|
||||
} catch (Exception e) {
|
||||
result.error("location_error", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "stopLocationService":
|
||||
try {
|
||||
stopLocationUpdates();
|
||||
result.success("");
|
||||
} catch (Exception e) {
|
||||
result.error("location_error", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "cancelOldLocationWorker":
|
||||
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
|
||||
result.success("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,9 +107,78 @@ public class MainActivity extends FlutterActivity {
|
||||
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
|
||||
}
|
||||
|
||||
private void startLocationUpdates() {
|
||||
if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_SERVICE) {
|
||||
LocationUtils.startService(this);
|
||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
||||
} else if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_WORKER) {
|
||||
LocationUtils.startWorker(this, locationUpdatesInterval);
|
||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
||||
} else {
|
||||
stopLocationUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopLocationUpdates() {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private boolean isNoLocationPermissions() {
|
||||
return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
ActivityCompat.requestPermissions(MainActivity.this,
|
||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||
REQUEST_PERMISSIONS_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startLocationUpdates();
|
||||
} else {
|
||||
stopLocationUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package com.keyboardcrumbs.hassclient;
|
||||
import java.util.Map;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
@ -14,6 +13,8 @@ import android.content.Intent;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
@ -21,12 +22,15 @@ 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";
|
||||
private static final String TAG = MessagingService.class.getSimpleName();
|
||||
|
||||
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
@ -39,26 +43,31 @@ public class MessagingService extends FirebaseMessagingService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewToken(String token) {
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
|
||||
updateTokenTask.execute(token);
|
||||
public void onNewToken(@NonNull String token) {
|
||||
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||
}
|
||||
|
||||
private void sendNotification(Map<String, String> data) {
|
||||
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
||||
boolean autoCancel;
|
||||
if (!data.containsKey("channelId")) {
|
||||
channelId = "ha_notify";
|
||||
channelDescription = "Default notification channel";
|
||||
} else {
|
||||
channelId = data.get("channelId");
|
||||
channelDescription = channelId;
|
||||
}
|
||||
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";
|
||||
channelDescription = "Default notification channel";
|
||||
} else {
|
||||
channelId = customChannelId;
|
||||
channelDescription = channelId;
|
||||
}
|
||||
if (!data.containsKey("title")) {
|
||||
messageTitle = "HA Client";
|
||||
} else {
|
||||
@ -105,22 +114,26 @@ 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)) {
|
||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
|
||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class).setAction(NOTIFICATION_ACTION_BROADCAST);
|
||||
if (autoCancel) {
|
||||
broadcastIntent.putExtra("tag", nTag);
|
||||
}
|
||||
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
|
||||
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent);
|
||||
notificationBuilder.addAction(R.drawable.blank_icon, data.get("action" + i), actionIntent);
|
||||
}
|
||||
}
|
||||
NotificationManager notificationManager =
|
||||
|
@ -7,19 +7,17 @@ import android.content.Intent;
|
||||
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = "NextAlarmReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
@ -35,12 +33,17 @@ public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data workerData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NEXT_ALARM)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class)
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
10,
|
||||
TimeUnit.SECONDS)
|
||||
.setInputData(workerData)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
|
||||
|
@ -1,73 +1,58 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class NotificationActionReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = "NotificationAction";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String intentAction = intent.getAction();
|
||||
if (intentAction == null || !intentAction.equalsIgnoreCase(MessagingService.NOTIFICATION_ACTION_BROADCAST)) {
|
||||
return;
|
||||
}
|
||||
String rawActionData = intent.getStringExtra("actionData");
|
||||
if (intent.hasExtra("tag")) {
|
||||
String notificationTag = intent.getStringExtra("tag");
|
||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationTag, 0);
|
||||
}
|
||||
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
JSONObject actionData = new JSONObject(rawActionData);
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
JSONObject requestData = new JSONObject();
|
||||
if (actionData.getString("action").equals("call-service")) {
|
||||
dataToSend.put("type", "call_service");
|
||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
||||
if (actionData.has("service_data")) {
|
||||
requestData.put("service_data", actionData.get("service_data"));
|
||||
}
|
||||
} else {
|
||||
dataToSend.put("type", "fire_event");
|
||||
requestData.put("event_type", "ha_client_event");
|
||||
JSONObject eventData = new JSONObject();
|
||||
eventData.put("action", actionData.getString("action"));
|
||||
requestData.put("event_data", eventData);
|
||||
}
|
||||
dataToSend.put("data", requestData);
|
||||
String stringRequest = dataToSend.toString();
|
||||
SendTask sendTask = new SendTask();
|
||||
sendTask.execute(requestUrl, stringRequest);
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error handling notification action", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
}
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
Data workerData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
|
||||
.putString("rawActionData", rawActionData)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
10,
|
||||
TimeUnit.SECONDS)
|
||||
.setInputData(workerData)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
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()) || Intent.ACTION_MY_PACKAGE_REPLACED.equalsIgnoreCase(intent.getAction()))) {
|
||||
LocationUtils.startServiceFromBroadcast(context);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.BatteryManager;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SendDataHomeWorker extends Worker {
|
||||
public static final String DATA_TYPE_KEY = "dataType";
|
||||
|
||||
public static final int DATA_TYPE_LOCATION = 1;
|
||||
public static final int DATA_TYPE_NEXT_ALARM = 2;
|
||||
public static final int DATA_TYPE_NOTIFICATION_ACTION = 3;
|
||||
|
||||
private Context currentContext;
|
||||
private static final String TAG = "SendDataHomeWorker";
|
||||
|
||||
public static final String KEY_LAT_ARG = "Lat";
|
||||
public static final String KEY_LONG_ARG = "Long";
|
||||
public static final String KEY_ACC_ARG = "Acc";
|
||||
|
||||
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
|
||||
|
||||
public SendDataHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Log.d(TAG, "Start sending data home");
|
||||
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
int dataType = getInputData().getInt(DATA_TYPE_KEY, 0);
|
||||
String stringRequest;
|
||||
if (dataType == DATA_TYPE_LOCATION) {
|
||||
Log.d(TAG, "Location data");
|
||||
stringRequest = getLocationDataToSend();
|
||||
} else if (dataType == DATA_TYPE_NEXT_ALARM) {
|
||||
Log.d(TAG, "Next alarm data");
|
||||
stringRequest = getNextAlarmDataToSend();
|
||||
} else if (dataType == DATA_TYPE_NOTIFICATION_ACTION) {
|
||||
Log.d(TAG, "Notification action data");
|
||||
stringRequest = getNotificationActionData();
|
||||
} else {
|
||||
Log.e(TAG, "doWork() unknown data type: " + dataType);
|
||||
return Result.failure();
|
||||
}
|
||||
try {
|
||||
URL url = new URL(requestUrl);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
assert stringRequest != null;
|
||||
byte[] outputBytes = stringRequest.getBytes(StandardCharsets.UTF_8);
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
urlConnection.disconnect();
|
||||
if (responseCode >= 300) {
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
return Result.retry();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
return Result.failure();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error =(", e);
|
||||
return Result.failure();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
return Result.failure();
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private String getLocationDataToSend() {
|
||||
try {
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
dataToSend.put("type", "update_location");
|
||||
JSONObject dataObject = new JSONObject();
|
||||
|
||||
JSONArray gps = new JSONArray();
|
||||
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
|
||||
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
|
||||
|
||||
dataObject.put("gps", gps);
|
||||
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
|
||||
|
||||
BatteryManager bm;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
bm = currentContext.getSystemService(BatteryManager.class);
|
||||
} else {
|
||||
bm = (BatteryManager)currentContext.getSystemService(Context.BATTERY_SERVICE);
|
||||
}
|
||||
int batLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
|
||||
dataObject.put("battery", batLevel);
|
||||
|
||||
dataToSend.put("data", dataObject);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getLocationDataToSend", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getNotificationActionData() {
|
||||
try {
|
||||
String rawActionData = getInputData().getString("rawActionData");
|
||||
if (rawActionData == null || rawActionData.length() == 0) {
|
||||
Log.e(TAG,"getNotificationActionData rawAction data is empty");
|
||||
return null;
|
||||
}
|
||||
JSONObject actionData = new JSONObject(rawActionData);
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
JSONObject requestData = new JSONObject();
|
||||
if (actionData.getString("action").equals("call-service")) {
|
||||
dataToSend.put("type", "call_service");
|
||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
||||
if (actionData.has("service_data")) {
|
||||
requestData.put("service_data", actionData.get("service_data"));
|
||||
}
|
||||
} else {
|
||||
dataToSend.put("type", "fire_event");
|
||||
requestData.put("event_type", "ha_client_event");
|
||||
JSONObject eventData = new JSONObject();
|
||||
eventData.put("action", actionData.getString("action"));
|
||||
requestData.put("event_data", eventData);
|
||||
}
|
||||
dataToSend.put("data", requestData);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getNotificationActionData", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getNextAlarmDataToSend() {
|
||||
try {
|
||||
final AlarmManager alarmManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
alarmManager = currentContext.getSystemService(AlarmManager.class);
|
||||
} else {
|
||||
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
|
||||
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
dataToSend.put("type", "update_sensor_states");
|
||||
JSONArray dataArray = new JSONArray();
|
||||
JSONObject sensorData = new JSONObject();
|
||||
JSONObject sensorAttrs = new JSONObject();
|
||||
sensorData.put("unique_id", "next_alarm");
|
||||
sensorData.put("type", "sensor");
|
||||
final long triggerTimestamp;
|
||||
if (alarmClockInfo != null) {
|
||||
triggerTimestamp = alarmClockInfo.getTriggerTime();
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(triggerTimestamp);
|
||||
Date date = calendar.getTime();
|
||||
sensorData.put("state", DATE_TIME_FORMAT.format(date));
|
||||
sensorAttrs.put("date", DATE_FORMAT.format(date));
|
||||
sensorAttrs.put("time", TIME_FORMAT.format(date));
|
||||
sensorAttrs.put("timestamp", triggerTimestamp);
|
||||
} else {
|
||||
sensorData.put("state", "");
|
||||
sensorAttrs.put("date", "");
|
||||
sensorAttrs.put("time", "");
|
||||
sensorAttrs.put("timestamp", 0);
|
||||
}
|
||||
sensorData.put("icon", "mdi:alarm");
|
||||
sensorData.put("attributes", sensorAttrs);
|
||||
dataArray.put(0, sensorData);
|
||||
dataToSend.put("data", dataArray);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getNextAlarmDataToSend", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.util.Log;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class SendTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private static final String TAG = "SendTask";
|
||||
|
||||
public SendTask(){
|
||||
//set context variables if required
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
String urlString = params[0];
|
||||
String data = params[1];
|
||||
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
byte[] outputBytes = data.getBytes("UTF-8");
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
|
||||
urlConnection.disconnect();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
public class UpdateNextAlarmWorker extends Worker {
|
||||
|
||||
private Context currentContext;
|
||||
private static final String TAG = "NextAlarmWorker";
|
||||
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
|
||||
|
||||
public UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
final AlarmManager alarmManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
alarmManager = currentContext.getSystemService(AlarmManager.class);
|
||||
} else {
|
||||
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
|
||||
|
||||
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
dataToSend.put("type", "update_sensor_states");
|
||||
JSONArray dataArray = new JSONArray();
|
||||
JSONObject sensorData = new JSONObject();
|
||||
JSONObject sensorAttrs = new JSONObject();
|
||||
sensorData.put("unique_id", "next_alarm");
|
||||
sensorData.put("type", "sensor");
|
||||
final long triggerTimestamp;
|
||||
if (alarmClockInfo != null) {
|
||||
triggerTimestamp = alarmClockInfo.getTriggerTime();
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(triggerTimestamp);
|
||||
sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime()));
|
||||
sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime()));
|
||||
sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime()));
|
||||
sensorAttrs.put("timestamp", triggerTimestamp);
|
||||
} else {
|
||||
sensorData.put("state", "");
|
||||
sensorAttrs.put("date", "");
|
||||
sensorAttrs.put("time", "");
|
||||
sensorAttrs.put("timestamp", 0);
|
||||
}
|
||||
sensorData.put("icon", "mdi:alarm");
|
||||
sensorData.put("attributes", sensorAttrs);
|
||||
dataArray.put(0, sensorData);
|
||||
dataToSend.put("data", dataArray);
|
||||
|
||||
String stringRequest = dataToSend.toString();
|
||||
try {
|
||||
URL url = new URL(requestUrl);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
byte[] outputBytes = stringRequest.getBytes("UTF-8");
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
urlConnection.disconnect();
|
||||
if (responseCode >= 300) {
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
return Result.retry();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
return Result.failure();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error setting next alarm", e);
|
||||
return Result.failure();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
return Result.failure();
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.util.Log;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.Context;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
|
||||
public class UpdateTokenTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private static final String TAG = "UpdateTokenTask";
|
||||
|
||||
private WeakReference<Context> contextRef;
|
||||
|
||||
public UpdateTokenTask(Context context){
|
||||
contextRef = new WeakReference<>(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
Log.d(TAG, "Updating push token");
|
||||
Context context = contextRef.get();
|
||||
if (context != null) {
|
||||
String token = params[0];
|
||||
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString("flutter.npush-token", token);
|
||||
editor.commit();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable/mini_icon_location.png
Normal file
BIN
android/app/src/main/res/drawable/mini_icon_location.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 571 B |
Binary file not shown.
BIN
fonts/materialdesignicons-webfont-5.3.45.ttf
Normal file
BIN
fonts/materialdesignicons-webfont-5.3.45.ttf
Normal file
Binary file not shown.
@ -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<dynamic> 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
|
||||
|
88
lib/cards/map_card.dart
Normal file
88
lib/cards/map_card.dart
Normal file
@ -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<MapCard> {
|
||||
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
CardHeader(name: widget.card.title),
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
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)
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
73
lib/cards/widgets/entities_map.dart
Normal file
73
lib/cards/widgets/entities_map.dart
Normal file
@ -0,0 +1,73 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
|
||||
class EntitiesMap extends StatelessWidget {
|
||||
|
||||
final List<EntityWrapper> 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<Marker> markers = [];
|
||||
List<LatLng> 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;
|
||||
}
|
||||
}
|
@ -278,6 +278,7 @@ class HomeAssistant {
|
||||
_rawPanels = data;
|
||||
List<Panel> 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(
|
||||
|
@ -22,20 +22,21 @@ 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';
|
||||
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';
|
||||
@ -48,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';
|
||||
@ -61,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';
|
||||
@ -117,7 +120,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';
|
||||
@ -158,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<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
// Print the exception to the console.
|
||||
@ -230,10 +232,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,9 +33,6 @@ class AppSettings {
|
||||
bool nextAlarmSensorCreated = false;
|
||||
DisplayMode displayMode;
|
||||
AppTheme appTheme;
|
||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||
Duration locationUpdateInterval;
|
||||
bool locationTrackingEnabled = false;
|
||||
|
||||
bool get isAuthenticated => longLivedToken != null;
|
||||
bool get isTempAuthenticated => tempToken != null;
|
||||
@ -49,6 +48,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');
|
||||
@ -59,10 +59,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
|
||||
@ -71,6 +67,38 @@ 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');
|
||||
int oldLocationTrackingInterval = prefs.getInt("location-interval") ?? 0;
|
||||
if (oldLocationTrackingInterval < 15) {
|
||||
oldLocationTrackingInterval = 15;
|
||||
}
|
||||
try {
|
||||
await platform.invokeMethod('startLocationService', <String, dynamic>{
|
||||
'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.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();
|
||||
});
|
||||
@ -409,7 +408,7 @@ class _MainPageState extends State<MainPage> 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;
|
||||
}
|
||||
|
@ -11,9 +11,18 @@ class IntegrationSettingsPage extends StatefulWidget {
|
||||
|
||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
|
||||
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||
/*static final locationAccuracy = {
|
||||
100: "High",
|
||||
102: "Balanced"
|
||||
};*/
|
||||
|
||||
Duration _locationInterval;
|
||||
bool _locationTrackingEnabled = false;
|
||||
bool _wait = false;
|
||||
bool _showNotification = true;
|
||||
//int _accuracy = 100;
|
||||
bool _useForegroundService = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -27,103 +36,235 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
await prefs.reload();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
_locationInterval = prefs.getInt("location-interval") ??
|
||||
AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
if (_locationInterval % 5 != 0) {
|
||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||
}
|
||||
//_accuracy = prefs.getInt("location-updates-priority") ?? 100;
|
||||
_locationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0;
|
||||
_showNotification = prefs.getBool("location-updates-show-notification") ?? true;
|
||||
_useForegroundService = prefs.getBool("foreground-location-tracking") ?? false;
|
||||
_locationInterval = Duration(milliseconds: prefs.getInt("location-updates-interval") ??
|
||||
900000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _incLocationInterval() {
|
||||
if (_locationInterval < 720) {
|
||||
if (_locationInterval.inSeconds < 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + 5;
|
||||
_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 > 5) {
|
||||
if ((_useForegroundService && _locationInterval.inSeconds > 5) || (!_useForegroundService && _locationInterval.inMinutes > 15)) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - 5;
|
||||
if (_locationInterval.inSeconds <= 60) {
|
||||
_locationInterval = _locationInterval - Duration(seconds: 5);
|
||||
} else if (_locationInterval.inMinutes <= 15) {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 1);
|
||||
} else if (_locationInterval.inMinutes <= 60) {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 5);
|
||||
} else if (_locationInterval.inHours <= 4) {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 10);
|
||||
} else if (_locationInterval.inHours > 4) {
|
||||
_locationInterval = _locationInterval - Duration(hours: 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_switchLocationTrackingState(bool state) async {
|
||||
if (state) {
|
||||
await LocationManager().updateDeviceLocation();
|
||||
try {
|
||||
await platform.invokeMethod('startLocationService', <String, dynamic>{
|
||||
'location-updates-interval': _locationInterval.inMilliseconds,
|
||||
//'location-updates-priority': _accuracy,
|
||||
'foreground-location-tracking': _useForegroundService,
|
||||
'location-updates-show-notification': _showNotification
|
||||
});
|
||||
} catch (e) {
|
||||
_locationTrackingEnabled = false;
|
||||
}
|
||||
} else {
|
||||
await platform.invokeMethod('stopLocationService');
|
||||
}
|
||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
setState(() {
|
||||
_wait = false;
|
||||
});
|
||||
}
|
||||
|
||||
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 (_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);
|
||||
}
|
||||
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("Location tracking", style: Theme.of(context).textTheme.title),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
InkWell(
|
||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
||||
child: Text(
|
||||
"Please read documentation!",
|
||||
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
)
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable device location tracking"),
|
||||
Switch(
|
||||
value: _locationTrackingEnabled,
|
||||
onChanged: _wait ? null : (value) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = value;
|
||||
_wait = true;
|
||||
});
|
||||
_switchLocationTrackingState(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("Location update interval in minutes:"),
|
||||
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", style: Theme.of(context).textTheme.title),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("+", style: Theme.of(context).textTheme.title),
|
||||
onPressed: () => _incLocationInterval(),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
);
|
||||
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable"),
|
||||
Switch(
|
||||
value: _locationTrackingEnabled,
|
||||
onChanged: _wait ? null : (value) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = value;
|
||||
_wait = true;
|
||||
});
|
||||
_switchLocationTrackingState(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
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<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: _locationTrackingEnabled ? null : (val) {
|
||||
setState(() {
|
||||
_accuracy = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(height: Sizes.rowPadding),*/
|
||||
Text("Update interval"),
|
||||
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.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),
|
||||
),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
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 || _useForegroundService) ? null : (value) {
|
||||
setState(() {
|
||||
_showNotification = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
_getNotes()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
28
lib/utils/RandomColorGenerator.dart
Normal file
28
lib/utils/RandomColorGenerator.dart
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -74,7 +74,7 @@ class HAView {
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return ViewWidget(
|
||||
view: this,
|
||||
view: this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
12
pubspec.yaml
12
pubspec.yaml
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 1.2.0+1200
|
||||
version: 1.3.0+1309
|
||||
|
||||
|
||||
environment:
|
||||
@ -26,12 +26,10 @@ 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
|
||||
syncfusion_flutter_core: ^18.2.44
|
||||
syncfusion_flutter_gauges: ^18.2.44
|
||||
flutter_map: ^0.10.1+1
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
@ -50,4 +48,4 @@ flutter:
|
||||
fonts:
|
||||
- family: "Material Design Icons"
|
||||
fonts:
|
||||
- asset: fonts/materialdesignicons-webfont-4.5.95.ttf
|
||||
- asset: fonts/materialdesignicons-webfont-5.3.45.ttf
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user