Compare commits
60 Commits
1.1.0-beta
...
1.3.0-beta
Author | SHA1 | Date | |
---|---|---|---|
07dafa7039 | |||
8a1f539668 | |||
19325b5b7f | |||
b68d6539b6 | |||
aa8c402f97 | |||
343d39d157 | |||
b056de5230 | |||
f7e139b963 | |||
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 | |||
85693ac5ce | |||
da8efcd43f | |||
7ebf5f7c18 | |||
f84728b948 | |||
26a62d341e | |||
772bddeb9a | |||
5b55940ccf | |||
7683d18e81 | |||
d09afc37b5 | |||
1c686402d0 | |||
5f4a3fbdfc | |||
312ed99e9f | |||
25e6d51c17 | |||
b501574bab | |||
53b31d8e90 | |||
6d80420a9b | |||
e977054139 | |||
6367d38524 | |||
f9b2d7d84c | |||
44c28ad106 |
@ -13,6 +13,6 @@ Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant commun
|
|||||||
#### Last release build status
|
#### Last release build status
|
||||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||||
|
|
||||||
#### Special thanks to
|
#### Projects used
|
||||||
- [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify)
|
- [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski)
|
||||||
- [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/)
|
- [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE)
|
||||||
|
@ -80,7 +80,9 @@ flutter {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||||
implementation 'com.google.firebase:firebase-messaging:20.2.0'
|
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.work:work-runtime:2.3.4'
|
||||||
|
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
|
@ -56,13 +56,32 @@
|
|||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</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">
|
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
|
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver android:name=".NextAlarmBroadcastReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<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
|
<service
|
||||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
@ -74,7 +93,7 @@
|
|||||||
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||||
android:enabled="false">
|
android:enabled="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
@ -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;
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.location.Location;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import io.flutter.plugin.common.MethodCall;
|
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
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.GoogleApiAvailability;
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
import com.google.android.gms.common.ConnectionResult;
|
||||||
import com.google.firebase.iid.FirebaseInstanceId;
|
import com.google.firebase.iid.FirebaseInstanceId;
|
||||||
import com.google.firebase.iid.InstanceIdResult;
|
|
||||||
import com.google.firebase.messaging.FirebaseMessaging;
|
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterActivity {
|
||||||
|
|
||||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
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
|
@Override
|
||||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
||||||
new MethodChannel.MethodCallHandler() {
|
(call, result) -> {
|
||||||
@Override
|
|
||||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
|
||||||
Context context = getActivity();
|
Context context = getActivity();
|
||||||
if (call.method.equals("getFCMToken")) {
|
switch (call.method) {
|
||||||
if (checkPlayServices()) {
|
case "getFCMToken":
|
||||||
FirebaseInstanceId.getInstance().getInstanceId()
|
try {
|
||||||
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
if (checkPlayServices()) {
|
||||||
@Override
|
FirebaseInstanceId.getInstance().getInstanceId()
|
||||||
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
.addOnCompleteListener(task -> {
|
||||||
if (task.isSuccessful()) {
|
if (task.isSuccessful()) {
|
||||||
String token = task.getResult().getToken();
|
String token = task.getResult().getToken();
|
||||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
|
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||||
updateTokenTask.execute(token);
|
result.success(token);
|
||||||
result.success(token);
|
} else {
|
||||||
} else {
|
Exception ex = task.getException();
|
||||||
result.error("fcm_error", task.getException().getMessage(), null);
|
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);
|
||||||
}
|
}
|
||||||
});
|
} catch (Exception e) {
|
||||||
} else {
|
result.error("get_token_exception", e.getMessage(), e);
|
||||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
}
|
||||||
}
|
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);
|
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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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.util.Map;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
@ -14,6 +13,8 @@ import android.content.Intent;
|
|||||||
import android.media.RingtoneManager;
|
import android.media.RingtoneManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||||
@ -21,12 +22,15 @@ import com.google.firebase.messaging.RemoteMessage;
|
|||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.util.Log;
|
||||||
import android.webkit.URLUtil;
|
import android.webkit.URLUtil;
|
||||||
|
|
||||||
|
|
||||||
public class MessagingService extends FirebaseMessagingService {
|
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
|
@Override
|
||||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||||
@ -39,26 +43,31 @@ public class MessagingService extends FirebaseMessagingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewToken(String token) {
|
public void onNewToken(@NonNull String token) {
|
||||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
|
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||||
updateTokenTask.execute(token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendNotification(Map<String, String> data) {
|
private void sendNotification(Map<String, String> data) {
|
||||||
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
||||||
boolean autoCancel;
|
boolean autoCancel;
|
||||||
if (!data.containsKey("channelId")) {
|
|
||||||
channelId = "ha_notify";
|
|
||||||
channelDescription = "Default notification channel";
|
|
||||||
} else {
|
|
||||||
channelId = data.get("channelId");
|
|
||||||
channelDescription = channelId;
|
|
||||||
}
|
|
||||||
if (!data.containsKey("body")) {
|
if (!data.containsKey("body")) {
|
||||||
messageBody = "";
|
messageBody = "";
|
||||||
} else {
|
} else {
|
||||||
messageBody = data.get("body");
|
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")) {
|
if (!data.containsKey("title")) {
|
||||||
messageTitle = "HA Client";
|
messageTitle = "HA Client";
|
||||||
} else {
|
} else {
|
||||||
@ -105,22 +114,26 @@ public class MessagingService extends FirebaseMessagingService {
|
|||||||
.setAutoCancel(autoCancel)
|
.setAutoCancel(autoCancel)
|
||||||
.setSound(defaultSoundUri)
|
.setSound(defaultSoundUri)
|
||||||
.setContentIntent(pendingIntent);
|
.setContentIntent(pendingIntent);
|
||||||
|
Bitmap image = null;
|
||||||
if (URLUtil.isValidUrl(imageUrl)) {
|
if (URLUtil.isValidUrl(imageUrl)) {
|
||||||
Bitmap image = getBitmapFromURL(imageUrl);
|
image = getBitmapFromURL(imageUrl);
|
||||||
if (image != null) {
|
}
|
||||||
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
|
if (image != null) {
|
||||||
notificationBuilder.setLargeIcon(image);
|
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++) {
|
for (int i = 1; i <= 3; i++) {
|
||||||
if (data.containsKey("action" + 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) {
|
if (autoCancel) {
|
||||||
broadcastIntent.putExtra("tag", nTag);
|
broadcastIntent.putExtra("tag", nTag);
|
||||||
}
|
}
|
||||||
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
|
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
|
||||||
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
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 =
|
NotificationManager notificationManager =
|
||||||
@ -145,7 +158,7 @@ public class MessagingService extends FirebaseMessagingService {
|
|||||||
connection.connect();
|
connection.connect();
|
||||||
InputStream input = connection.getInputStream();
|
InputStream input = connection.getInputStream();
|
||||||
return BitmapFactory.decodeStream(input);
|
return BitmapFactory.decodeStream(input);
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
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 java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
||||||
|
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (intent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction());
|
||||||
|
final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction());
|
||||||
|
if (!isBootIntent && !isNextAlarmIntent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Constraints constraints = new Constraints.Builder()
|
||||||
|
.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(SendDataHomeWorker.class)
|
||||||
|
.setBackoffCriteria(
|
||||||
|
BackoffPolicy.EXPONENTIAL,
|
||||||
|
10,
|
||||||
|
TimeUnit.SECONDS)
|
||||||
|
.setInputData(workerData)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorkManager
|
||||||
|
.getInstance(context)
|
||||||
|
.enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||||
|
}
|
||||||
|
}
|
@ -1,69 +1,58 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import android.app.NotificationManager;
|
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 java.util.concurrent.TimeUnit;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
public class NotificationActionReceiver extends BroadcastReceiver {
|
public class NotificationActionReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
private static final String TAG = "NotificationActionReceiver";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
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");
|
String rawActionData = intent.getStringExtra("actionData");
|
||||||
if (intent.hasExtra("tag")) {
|
if (intent.hasExtra("tag")) {
|
||||||
String notificationTag = intent.getStringExtra("tag");
|
String notificationTag = intent.getStringExtra("tag");
|
||||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationManager.cancel(notificationTag, 0);
|
notificationManager.cancel(notificationTag, 0);
|
||||||
}
|
}
|
||||||
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
Constraints constraints = new Constraints.Builder()
|
||||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
if (webhookId != null) {
|
.build();
|
||||||
try {
|
Data workerData = new Data.Builder()
|
||||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
|
||||||
"://" +
|
.putString("rawActionData", rawActionData)
|
||||||
prefs.getString("flutter.hassio-domain", "") +
|
.build();
|
||||||
":" +
|
|
||||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
OneTimeWorkRequest uploadWorkRequest =
|
||||||
JSONObject actionData = new JSONObject(rawActionData);
|
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||||
if (URLUtil.isValidUrl(requestUrl)) {
|
.setBackoffCriteria(
|
||||||
JSONObject dataToSend = new JSONObject();
|
BackoffPolicy.EXPONENTIAL,
|
||||||
JSONObject requestData = new JSONObject();
|
10,
|
||||||
if (actionData.getString("action").equals("call-service")) {
|
TimeUnit.SECONDS)
|
||||||
dataToSend.put("type", "call_service");
|
.setInputData(workerData)
|
||||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
.setConstraints(constraints)
|
||||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
.build();
|
||||||
if (actionData.has("service_data")) {
|
|
||||||
requestData.put("service_data", actionData.get("service_data"));
|
WorkManager
|
||||||
}
|
.getInstance(context)
|
||||||
} else {
|
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,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 |
@ -1,6 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx2g
|
org.gradle.jvmargs=-Xmx512m
|
||||||
org.gradle.daemon=true
|
|
||||||
org.gradle.caching=true
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.enableR8=true
|
android.enableR8=true
|
||||||
|
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']) {
|
switch (rawData['type']) {
|
||||||
case CardType.ENTITIES:
|
case CardType.ENTITIES:
|
||||||
case CardType.HISTORY_GRAPH:
|
case CardType.HISTORY_GRAPH:
|
||||||
case CardType.MAP:
|
|
||||||
case CardType.PICTURE_GLANCE:
|
case CardType.PICTURE_GLANCE:
|
||||||
case CardType.SENSOR:
|
case CardType.SENSOR:
|
||||||
case CardType.ENTITY:
|
case CardType.ENTITY:
|
||||||
@ -47,6 +46,9 @@ class CardData {
|
|||||||
return CardData(null);
|
return CardData(null);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case CardType.MAP:
|
||||||
|
return MapCardData(rawData);
|
||||||
|
break;
|
||||||
case CardType.ENTITY_BUTTON:
|
case CardType.ENTITY_BUTTON:
|
||||||
case CardType.BUTTON:
|
case CardType.BUTTON:
|
||||||
case CardType.PICTURE_ENTITY:
|
case CardType.PICTURE_ENTITY:
|
||||||
@ -90,6 +92,12 @@ class CardData {
|
|||||||
return BadgesData(rawData);
|
return BadgesData(rawData);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (rawData.containsKey('entity')) {
|
||||||
|
rawData['entities'] = [rawData['entity']];
|
||||||
|
}
|
||||||
|
if (rawData.containsKey('entities') && rawData['entities'] is List) {
|
||||||
|
return EntitiesCardData(rawData);
|
||||||
|
}
|
||||||
return CardData(null);
|
return CardData(null);
|
||||||
}
|
}
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
@ -103,7 +111,11 @@ class CardData {
|
|||||||
type = rawData['type'];
|
type = rawData['type'];
|
||||||
conditions = rawData['conditions'] ?? [];
|
conditions = rawData['conditions'] ?? [];
|
||||||
showEmpty = rawData['show_empty'] ?? true;
|
showEmpty = rawData['show_empty'] ?? true;
|
||||||
stateFilter = rawData['state_filter'] ?? [];
|
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
|
||||||
|
stateFilter = rawData['state_filter'];
|
||||||
|
} else {
|
||||||
|
stateFilter = [];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
type = CardType.UNKNOWN;
|
type = CardType.UNKNOWN;
|
||||||
conditions = [];
|
conditions = [];
|
||||||
@ -374,7 +386,13 @@ class LightCardData extends CardData {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildCardWidget() {
|
Widget buildCardWidget() {
|
||||||
return LightCard(card: this);
|
if (this.entity != null && this.entity.entity is LightEntity) {
|
||||||
|
return LightCard(card: this);
|
||||||
|
}
|
||||||
|
return ErrorCard(
|
||||||
|
errorText: 'Specify an entity from within the light domain.',
|
||||||
|
showReportButton: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LightCardData(rawData) : super(rawData) {
|
LightCardData(rawData) : super(rawData) {
|
||||||
@ -628,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 {
|
class MediaControlCardData extends CardData {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -2,12 +2,21 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class ErrorCard extends StatelessWidget {
|
class ErrorCard extends StatelessWidget {
|
||||||
final ErrorCardData card;
|
final ErrorCardData card;
|
||||||
|
final String errorText;
|
||||||
|
final bool showReportButton;
|
||||||
|
|
||||||
const ErrorCard({Key key, this.card}) : super(key: key);
|
const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
String error;
|
||||||
|
if (errorText == null) {
|
||||||
|
error = 'There was an error showing ${card?.type}';
|
||||||
|
} else {
|
||||||
|
error = errorText;
|
||||||
|
}
|
||||||
return CardWrapper(
|
return CardWrapper(
|
||||||
|
color: Theme.of(context).errorColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -15,21 +24,25 @@ class ErrorCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
|
error,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
card != null ?
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||||
},
|
},
|
||||||
child: Text('Copy card config'),
|
child: Text('Copy card config'),
|
||||||
),
|
) :
|
||||||
|
Container(width: 0, height: 0),
|
||||||
|
showReportButton ?
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||||
},
|
},
|
||||||
child: Text('Report issue'),
|
child: Text('Report issue'),
|
||||||
)
|
) :
|
||||||
|
Container(width: 0, height: 0)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,6 @@ class UnsupportedCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container();
|
return Container(height: 20);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,12 +4,14 @@ class CardWrapper extends StatelessWidget {
|
|||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
|
color: color,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: child
|
child: child
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -40,8 +40,8 @@ class CoverEntity extends Entity {
|
|||||||
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||||
|
|
||||||
|
|
||||||
double get currentPosition => _getDoubleAttributeValue('current_position');
|
double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0;
|
||||||
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
|
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0;
|
||||||
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
|
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
|
||||||
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
|
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
|
||||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||||
|
@ -8,8 +8,8 @@ class TimerEntity extends Entity {
|
|||||||
@override
|
@override
|
||||||
void update(Map rawData, String webHost) {
|
void update(Map rawData, String webHost) {
|
||||||
super.update(rawData, webHost);
|
super.update(rawData, webHost);
|
||||||
String durationSource = "${attributes["duration"]}";
|
if (attributes.containsKey('duration')) {
|
||||||
if (durationSource != null && durationSource.isNotEmpty) {
|
String durationSource = "${attributes["duration"]}";
|
||||||
try {
|
try {
|
||||||
List<String> durationList = durationSource.split(":");
|
List<String> durationList = durationSource.split(":");
|
||||||
if (durationList.length == 1) {
|
if (durationList.length == 1) {
|
||||||
|
@ -149,7 +149,7 @@ class EntityCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isExist(String entityId) {
|
bool isExist(String entityId) {
|
||||||
return _allEntities[entityId] != null;
|
return _allEntities.containsKey(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
||||||
|
@ -221,7 +221,7 @@ class HomeAssistant {
|
|||||||
var data = json.decode(prefs.getString('cached_services'));
|
var data = json.decode(prefs.getString('cached_services'));
|
||||||
_parseServices(data ?? {});
|
_parseServices(data ?? {});
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e(e, stacktrace: stacktrace);
|
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
||||||
@ -261,7 +261,7 @@ class HomeAssistant {
|
|||||||
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||||
_parsePanels(data ?? {});
|
_parsePanels(data ?? {});
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e(e, stacktrace: stacktrace);
|
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
|
||||||
panels.clear();
|
panels.clear();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -278,6 +278,7 @@ class HomeAssistant {
|
|||||||
_rawPanels = data;
|
_rawPanels = data;
|
||||||
List<Panel> dashboards = [];
|
List<Panel> dashboards = [];
|
||||||
data.forEach((k,v) {
|
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)}";
|
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') {
|
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
|
||||||
dashboards.add(
|
dashboards.add(
|
||||||
|
@ -22,20 +22,21 @@ import 'package:device_info/device_info.dart';
|
|||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'plugins/dynamic_multi_column_layout.dart';
|
import 'plugins/dynamic_multi_column_layout.dart';
|
||||||
import 'plugins/spoiler_card.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:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import 'package:syncfusion_flutter_core/core.dart';
|
import 'package:syncfusion_flutter_core/core.dart';
|
||||||
import 'package:syncfusion_flutter_gauges/gauges.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 'utils/logger.dart';
|
||||||
import '.secrets.dart';
|
import '.secrets.dart';
|
||||||
|
|
||||||
part 'const.dart';
|
part 'const.dart';
|
||||||
part 'utils/launcher.dart';
|
part 'utils/launcher.dart';
|
||||||
|
part 'utils/RandomColorGenerator.dart';
|
||||||
part 'entities/entity.class.dart';
|
part 'entities/entity.class.dart';
|
||||||
part 'entities/entity_wrapper.class.dart';
|
part 'entities/entity_wrapper.class.dart';
|
||||||
part 'entities/timer/timer_entity.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/light/light_entity.class.dart';
|
||||||
part 'entities/select/select_entity.class.dart';
|
part 'entities/select/select_entity.class.dart';
|
||||||
part 'entities/sun/sun_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/sensor/sensor_entity.class.dart';
|
||||||
part 'entities/slider/slider_entity.dart';
|
part 'entities/slider/slider_entity.dart';
|
||||||
part 'entities/media_player/media_player_entity.class.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/default_entity_container.widget.dart';
|
||||||
part 'entities/missed_entity.widget.dart';
|
part 'entities/missed_entity.widget.dart';
|
||||||
part 'cards/entity_button_card.dart';
|
part 'cards/entity_button_card.dart';
|
||||||
|
part 'cards/map_card.dart';
|
||||||
part 'pages/widgets/entity_attributes_list.dart';
|
part 'pages/widgets/entity_attributes_list.dart';
|
||||||
part 'entities/entity_icon.widget.dart';
|
part 'entities/entity_icon.widget.dart';
|
||||||
part 'entities/entity_name.widget.dart';
|
part 'entities/entity_name.widget.dart';
|
||||||
@ -117,7 +120,6 @@ part 'pages/entity.page.dart';
|
|||||||
part 'utils/mdi.class.dart';
|
part 'utils/mdi.class.dart';
|
||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'managers/auth_manager.class.dart';
|
part 'managers/auth_manager.class.dart';
|
||||||
part 'managers/location_manager.class.dart';
|
|
||||||
part 'managers/mobile_app_integration_manager.class.dart';
|
part 'managers/mobile_app_integration_manager.class.dart';
|
||||||
part 'managers/connection_manager.class.dart';
|
part 'managers/connection_manager.class.dart';
|
||||||
part 'managers/device_info_manager.class.dart';
|
part 'managers/device_info_manager.class.dart';
|
||||||
@ -156,11 +158,9 @@ part 'cards/badges.dart';
|
|||||||
part 'managers/app_settings.dart';
|
part 'managers/app_settings.dart';
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
|
||||||
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
|
||||||
const String appName = 'HA Client';
|
const String appName = 'HA Client';
|
||||||
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
|
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
|
||||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md';
|
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.3.0.md';
|
||||||
|
|
||||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||||
// Print the exception to the console.
|
// Print the exception to the console.
|
||||||
@ -232,10 +232,6 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
_currentTheme = event.theme;
|
_currentTheme = event.theme;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
workManager.Workmanager.initialize(
|
|
||||||
updateDeviceLocationIsolate,
|
|
||||||
isInDebugMode: false
|
|
||||||
);
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,6 +245,7 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
positiveText: "Ok"
|
positiveText: "Ok"
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
InAppPurchaseConnection.instance.completePurchase(purchase[0]);
|
||||||
} else {
|
} else {
|
||||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ class AppSettings {
|
|||||||
|
|
||||||
static const AUTH_TOKEN_KEY = 'llt';
|
static const AUTH_TOKEN_KEY = 'llt';
|
||||||
|
|
||||||
|
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||||
|
|
||||||
static final AppSettings _instance = AppSettings._internal();
|
static final AppSettings _instance = AppSettings._internal();
|
||||||
|
|
||||||
factory AppSettings() {
|
factory AppSettings() {
|
||||||
@ -28,11 +30,9 @@ class AppSettings {
|
|||||||
String webhookId;
|
String webhookId;
|
||||||
double haVersion;
|
double haVersion;
|
||||||
bool scrollBadges;
|
bool scrollBadges;
|
||||||
|
bool nextAlarmSensorCreated = false;
|
||||||
DisplayMode displayMode;
|
DisplayMode displayMode;
|
||||||
AppTheme appTheme;
|
AppTheme appTheme;
|
||||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
|
||||||
Duration locationUpdateInterval;
|
|
||||||
bool locationTrackingEnabled = false;
|
|
||||||
|
|
||||||
bool get isAuthenticated => longLivedToken != null;
|
bool get isAuthenticated => longLivedToken != null;
|
||||||
bool get isTempAuthenticated => tempToken != null;
|
bool get isTempAuthenticated => tempToken != null;
|
||||||
@ -48,6 +48,7 @@ class AppSettings {
|
|||||||
await Hive.openBox(DEFAULT_HIVE_BOX);
|
await Hive.openBox(DEFAULT_HIVE_BOX);
|
||||||
Logger.d('Loading settings...');
|
Logger.d('Loading settings...');
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await migrate(prefs);
|
||||||
_domain = prefs.getString('hassio-domain');
|
_domain = prefs.getString('hassio-domain');
|
||||||
_port = prefs.getString('hassio-port');
|
_port = prefs.getString('hassio-port');
|
||||||
webhookId = prefs.getString('app-webhook-id');
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
@ -58,9 +59,6 @@ class AppSettings {
|
|||||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
httpWebHost =
|
httpWebHost =
|
||||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||||
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
|
||||||
defaultLocationUpdateIntervalMinutes);
|
|
||||||
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
|
||||||
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
|
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
'https://ha-client.app')}&redirect_uri=${Uri
|
'https://ha-client.app')}&redirect_uri=${Uri
|
||||||
@ -69,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': true
|
||||||
|
});
|
||||||
|
} 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 {
|
Future<dynamic> loadSingle(String key) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.get('$key');
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
@ -5,9 +5,9 @@ class MobileAppIntegrationManager {
|
|||||||
static final _appRegistrationData = {
|
static final _appRegistrationData = {
|
||||||
"device_name": "",
|
"device_name": "",
|
||||||
"app_version": "$appVersion",
|
"app_version": "$appVersion",
|
||||||
"manufacturer": DeviceInfoManager().manufacturer,
|
"manufacturer": DeviceInfoManager().manufacturer ?? "unknown",
|
||||||
"model": DeviceInfoManager().model,
|
"model": DeviceInfoManager().model ?? "unknown",
|
||||||
"os_version": DeviceInfoManager().osVersion,
|
"os_version": DeviceInfoManager().osVersion ?? "0",
|
||||||
"app_data": {
|
"app_data": {
|
||||||
"push_token": "",
|
"push_token": "",
|
||||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
|
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
|
||||||
@ -62,12 +62,13 @@ class MobileAppIntegrationManager {
|
|||||||
includeAuthHeader: true,
|
includeAuthHeader: true,
|
||||||
data: json.encode(registrationData)
|
data: json.encode(registrationData)
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Processing registration responce...");
|
Logger.d("Processing registration response...");
|
||||||
var responseObject = json.decode(response);
|
var responseObject = json.decode(response);
|
||||||
AppSettings().webhookId = responseObject["webhook_id"];
|
AppSettings().webhookId = responseObject["webhook_id"];
|
||||||
AppSettings().save({
|
AppSettings().save({
|
||||||
'app-webhook-id': responseObject["webhook_id"]
|
'app-webhook-id': responseObject["webhook_id"]
|
||||||
}).then((prefs) {
|
}).then((_) {
|
||||||
|
_createNextAlarmSensor(true);
|
||||||
completer.complete();
|
completer.complete();
|
||||||
eventBus.fire(ShowPopupEvent(
|
eventBus.fire(ShowPopupEvent(
|
||||||
popup: Popup(
|
popup: Popup(
|
||||||
@ -112,6 +113,7 @@ class MobileAppIntegrationManager {
|
|||||||
_askToRegisterApp();
|
_askToRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
Logger.d('App registration works fine');
|
Logger.d('App registration works fine');
|
||||||
|
_createNextAlarmSensor(false);
|
||||||
}
|
}
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
@ -131,6 +133,42 @@ class MobileAppIntegrationManager {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static _createNextAlarmSensor(bool force) {
|
||||||
|
if (AppSettings().nextAlarmSensorCreated && !force) {
|
||||||
|
Logger.d("Next alarm sensor was previously created");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logger.d("Creating next alarm sensor...");
|
||||||
|
ConnectionManager().sendHTTPPost(
|
||||||
|
endPoint: "/api/webhook/${AppSettings().webhookId}",
|
||||||
|
includeAuthHeader: false,
|
||||||
|
data: json.encode(
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"device_class": "timestamp",
|
||||||
|
"icon": "mdi:alarm",
|
||||||
|
"name": "Next Alarm",
|
||||||
|
"state": "",
|
||||||
|
"type": "sensor",
|
||||||
|
"unique_id": "next_alarm"
|
||||||
|
},
|
||||||
|
"type": "register_sensor"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).then((_){
|
||||||
|
AppSettings().nextAlarmSensorCreated = true;
|
||||||
|
AppSettings().save({
|
||||||
|
'next-alarm-sensor-created': true
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
if (e is http.Response) {
|
||||||
|
Logger.e("Error creating next alarm sensor: ${e.statusCode}: ${e.body}");
|
||||||
|
} else {
|
||||||
|
Logger.e("Error creating next alarm sensor: ${e?.toString()}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static void _showError() {
|
static void _showError() {
|
||||||
eventBus.fire(ShowPopupEvent(
|
eventBus.fire(ShowPopupEvent(
|
||||||
popup: Popup(
|
popup: Popup(
|
||||||
|
@ -56,7 +56,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||||
_fetchData(useCache: true);
|
_fetchData(useCache: true);
|
||||||
LocationManager();
|
|
||||||
StartupUserMessagesManager().checkMessagesToShow();
|
StartupUserMessagesManager().checkMessagesToShow();
|
||||||
MobileAppIntegrationManager.checkAppRegistration();
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
});
|
});
|
||||||
@ -409,7 +408,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||||
if (_previousViewCount != currentViewCount) {
|
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);
|
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||||
_previousViewCount = currentViewCount;
|
_previousViewCount = currentViewCount;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,18 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildProducts() {
|
List<Widget> _buildProducts() {
|
||||||
List<Widget> productWidgets = [];
|
List<Widget> productWidgets = [
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: Text(
|
||||||
|
'This will not unlock any additional functionality. This is only a donation to the HA Client open source project.',
|
||||||
|
style: Theme.of(context).textTheme.headline5,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
for (ProductDetails product in _products) {
|
for (ProductDetails product in _products) {
|
||||||
productWidgets.add(
|
productWidgets.add(
|
||||||
ProductPurchase(
|
ProductPurchase(
|
||||||
|
@ -11,9 +11,18 @@ class IntegrationSettingsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
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 _locationTrackingEnabled = false;
|
||||||
bool _wait = false;
|
bool _wait = false;
|
||||||
|
bool _showNotification = true;
|
||||||
|
//int _accuracy = 100;
|
||||||
|
bool _useForegroundService = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -27,103 +36,235 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
await prefs.reload();
|
await prefs.reload();
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
//_accuracy = prefs.getInt("location-updates-priority") ?? 100;
|
||||||
_locationInterval = prefs.getInt("location-interval") ??
|
_locationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0;
|
||||||
AppSettings().defaultLocationUpdateIntervalMinutes;
|
_showNotification = prefs.getBool("location-updates-show-notification") ?? true;
|
||||||
if (_locationInterval % 5 != 0) {
|
_useForegroundService = prefs.getBool("foreground-location-tracking") ?? false;
|
||||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
_locationInterval = Duration(milliseconds: prefs.getInt("location-updates-interval") ??
|
||||||
}
|
900000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _incLocationInterval() {
|
void _incLocationInterval() {
|
||||||
if (_locationInterval < 720) {
|
if (_locationInterval.inSeconds < 60) {
|
||||||
setState(() {
|
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() {
|
void _decLocationInterval() {
|
||||||
if (_locationInterval > 5) {
|
if ((_useForegroundService && _locationInterval.inSeconds > 5) || (!_useForegroundService && _locationInterval.inMinutes > 15)) {
|
||||||
setState(() {
|
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 {
|
_switchLocationTrackingState(bool state) async {
|
||||||
if (state) {
|
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(() {
|
setState(() {
|
||||||
_wait = false;
|
_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 more accurate and stable tracking', 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding),
|
||||||
InkWell(
|
Row(
|
||||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
children: <Widget>[
|
||||||
child: Text(
|
Text("Enable"),
|
||||||
"Please read documentation!",
|
Switch(
|
||||||
style: Theme.of(context).textTheme.subhead.copyWith(
|
value: _locationTrackingEnabled,
|
||||||
color: Colors.blue,
|
onChanged: _wait ? null : (value) {
|
||||||
decoration: TextDecoration.underline
|
setState(() {
|
||||||
)
|
_locationTrackingEnabled = value;
|
||||||
),
|
_wait = true;
|
||||||
),
|
});
|
||||||
Container(height: Sizes.rowPadding,),
|
_switchLocationTrackingState(value);
|
||||||
Row(
|
},
|
||||||
children: <Widget>[
|
),
|
||||||
Text("Enable device location tracking"),
|
],
|
||||||
Switch(
|
),
|
||||||
value: _locationTrackingEnabled,
|
Container(height: Sizes.rowPadding),
|
||||||
onChanged: _wait ? null : (value) {
|
Row(
|
||||||
setState(() {
|
children: <Widget>[
|
||||||
_locationTrackingEnabled = value;
|
Text("Use foreground service"),
|
||||||
_wait = true;
|
Switch(
|
||||||
});
|
value: _useForegroundService,
|
||||||
_switchLocationTrackingState(value);
|
onChanged: _locationTrackingEnabled ? null : (value) {
|
||||||
},
|
setState(() {
|
||||||
),
|
_useForegroundService = value;
|
||||||
],
|
if (!_useForegroundService && _locationInterval.inMinutes < 15) {
|
||||||
),
|
_locationInterval = Duration(minutes: 15);
|
||||||
Container(height: Sizes.rowPadding,),
|
} else if (_useForegroundService) {
|
||||||
Text("Location update interval in minutes:"),
|
_showNotification = true;
|
||||||
Row(
|
}
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
});
|
||||||
mainAxisSize: MainAxisSize.max,
|
},
|
||||||
children: <Widget>[
|
),
|
||||||
//Expanded(child: Container(),),
|
],
|
||||||
FlatButton(
|
),
|
||||||
padding: EdgeInsets.all(0.0),
|
Container(height: Sizes.rowPadding),
|
||||||
child: Text("-", style: Theme.of(context).textTheme.title),
|
/*Text("Accuracy:", style: Theme.of(context).textTheme.body2),
|
||||||
onPressed: () => _decLocationInterval(),
|
Container(height: Sizes.rowPadding),
|
||||||
),
|
DropdownButton<int>(
|
||||||
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
|
value: _accuracy,
|
||||||
FlatButton(
|
iconSize: 30.0,
|
||||||
padding: EdgeInsets.all(0.0),
|
isExpanded: true,
|
||||||
child: Text("+", style: Theme.of(context).textTheme.title),
|
disabledHint: Text(locationAccuracy[_accuracy]),
|
||||||
onPressed: () => _incLocationInterval(),
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
String buttonText = '';
|
String buttonText = '';
|
||||||
String buttonTextInactive = '';
|
String buttonTextInactive = '';
|
||||||
if (product.id.contains("year")) {
|
if (product.id.contains("year")) {
|
||||||
period += "/ year";
|
period += "once a year";
|
||||||
buttonText = "Subscribe";
|
buttonText = "Subscribe";
|
||||||
buttonTextInactive = "Already";
|
buttonTextInactive = "Already";
|
||||||
priceColor = Colors.amber;
|
priceColor = Colors.amber;
|
||||||
|
@ -39,7 +39,7 @@ class Panel {
|
|||||||
eventBus.fire(ReloadUIEvent());
|
eventBus.fire(ReloadUIEvent());
|
||||||
});
|
});
|
||||||
} else {
|
} 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) {
|
Widget build(BuildContext context) {
|
||||||
return ViewWidget(
|
return ViewWidget(
|
||||||
view: this,
|
view: this
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
pubspec.yaml
16
pubspec.yaml
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 1.1.0+1156
|
version: 1.3.0+1310
|
||||||
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -19,19 +19,17 @@ dependencies:
|
|||||||
date_format: ^1.0.8
|
date_format: ^1.0.8
|
||||||
charts_flutter: ^0.8.1
|
charts_flutter: ^0.8.1
|
||||||
flutter_markdown: ^0.3.3
|
flutter_markdown: ^0.3.3
|
||||||
in_app_purchase: ^0.3.0+3
|
in_app_purchase: ^0.3.4
|
||||||
flutter_custom_tabs: ^0.6.0
|
flutter_custom_tabs: ^0.6.0
|
||||||
flutter_webview_plugin: ^0.3.10+1
|
flutter_webview_plugin: ^0.3.10+1
|
||||||
webview_flutter: ^0.3.19+7
|
webview_flutter: ^0.3.19+7
|
||||||
hive: ^1.4.1+1
|
hive: ^1.4.1+1
|
||||||
hive_flutter: ^0.3.0+2
|
hive_flutter: ^0.3.0+2
|
||||||
device_info: ^0.4.1+4
|
device_info: ^0.4.2+4
|
||||||
geolocator: ^5.3.1
|
|
||||||
workmanager: ^0.2.2
|
|
||||||
battery: ^1.0.0
|
|
||||||
firebase_crashlytics: ^0.1.3+3
|
firebase_crashlytics: ^0.1.3+3
|
||||||
syncfusion_flutter_core: ^18.1.52
|
syncfusion_flutter_core: ^18.2.54
|
||||||
syncfusion_flutter_gauges: ^18.1.52
|
syncfusion_flutter_gauges: ^18.2.54
|
||||||
|
flutter_map: ^0.10.1+1
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
@ -50,4 +48,4 @@ flutter:
|
|||||||
fonts:
|
fonts:
|
||||||
- family: "Material Design Icons"
|
- family: "Material Design Icons"
|
||||||
fonts:
|
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