Compare commits
46 Commits
rc/1.2.0-b
...
re/1.3.1
Author | SHA1 | Date | |
---|---|---|---|
e633449578 | |||
18c48ad067 | |||
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 |
@ -1,4 +1,9 @@
|
||||
# HA Client
|
||||
| # IMPORTANT! |
|
||||
|:-:|
|
||||
| As you may know official Home Assistant android app continue to grow and improve. Today is the day when it has so much more and better functionality that I see no any reason not to move from HA Client to official Home Assistant app. You need to make the same as it has more device sensors, better location tracking, widgets etc.
|
||||
| **I'm stopping an active HA Client development as now the only advantage it has over the official app is native UI that is very far from completeness.** |
|
||||
| I need to say a HUGE THANK YOU all for a grate support you all made for HA Client. I really appreciate each and every issue reported to GitHub, each and every message from each of you and each donation made to support development. Now it is time to cancel subscription if any and migrate your HA setup to an official Home Assistant app. You can find it in [Google Play](https://play.google.com/store/apps/details?id=io.homeassistant.companion.android). |
|
||||
## Native Android client for Home Assistant
|
||||
### With actionable notifications, location tracking and Lovelace UI support
|
||||
|
||||
|
@ -80,7 +80,9 @@ flutter {
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
implementation 'com.google.firebase:firebase-messaging:20.2.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.3.4'
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
|
@ -56,6 +56,14 @@
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".LocationUpdatesService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".LocationRequestService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
@ -68,6 +76,12 @@
|
||||
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".RestartLocationUpdate">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
|
@ -0,0 +1,146 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
|
||||
public class LocationRequestService extends Service {
|
||||
|
||||
private static final String TAG = LocationRequestService.class.getSimpleName();
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
|
||||
private FusedLocationProviderClient mFusedLocationClient;
|
||||
|
||||
private LocationCallback mLocationCallback;
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
public LocationRequestService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
mLocationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
onNewLocation(locationResult.getLastLocation());
|
||||
}
|
||||
};
|
||||
|
||||
mLocationRequest = new LocationRequest();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location requests";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Service started. startId="+startId);
|
||||
|
||||
requestLocationUpdates();
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try {
|
||||
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
||||
} catch (SecurityException unlikely) {
|
||||
//When we lost permission
|
||||
Log.i(TAG, "No location permission");
|
||||
}
|
||||
mServiceHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requestLocationUpdates() {
|
||||
Log.i(TAG, "Requesting location update in 5 seconds.");
|
||||
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
||||
mLocationRequest.setInterval(5000);
|
||||
|
||||
startForeground(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(this, null, LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID));
|
||||
try {
|
||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
||||
mLocationCallback, Looper.myLooper());
|
||||
} catch (SecurityException unlikely) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private void onNewLocation(Location location) {
|
||||
Log.i(TAG, "New location: " + location);
|
||||
|
||||
mNotificationManager.notify(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(
|
||||
this,
|
||||
location,
|
||||
LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID
|
||||
));
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
stopSelf();
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
|
||||
public class LocationUpdatesService extends Service {
|
||||
|
||||
private static final String TAG = LocationUpdatesService.class.getSimpleName();
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
|
||||
private FusedLocationProviderClient mFusedLocationClient;
|
||||
|
||||
private LocationCallback mLocationCallback;
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
public LocationUpdatesService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
mLocationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
onNewLocation(locationResult.getLastLocation());
|
||||
}
|
||||
};
|
||||
|
||||
mLocationRequest = new LocationRequest();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Service started. startId="+startId);
|
||||
|
||||
requestLocationUpdates();
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try {
|
||||
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
||||
} catch (SecurityException unlikely) {
|
||||
//When we lost permission
|
||||
Log.i(TAG, "No location permission");
|
||||
}
|
||||
mServiceHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requestLocationUpdates() {
|
||||
long requestInterval = LocationUtils.getLocationUpdateIntervals(getApplicationContext());
|
||||
int priority;
|
||||
if (requestInterval >= 600000) {
|
||||
mLocationRequest.setFastestInterval(60000);
|
||||
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
|
||||
} else {
|
||||
priority = LocationRequest.PRIORITY_HIGH_ACCURACY;
|
||||
}
|
||||
Log.i(TAG, "Requesting location updates. Every " + requestInterval + "ms with priority of " + priority);
|
||||
mLocationRequest.setPriority(priority);
|
||||
mLocationRequest.setInterval(requestInterval);
|
||||
|
||||
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
|
||||
try {
|
||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
||||
mLocationCallback, Looper.myLooper());
|
||||
} catch (SecurityException unlikely) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private void onNewLocation(Location location) {
|
||||
Log.i(TAG, "New location: " + location);
|
||||
|
||||
mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(
|
||||
this,
|
||||
location,
|
||||
LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID
|
||||
));
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.ListenableWorker;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocationUpdatesWorker extends ListenableWorker {
|
||||
|
||||
private Context currentContext;
|
||||
private LocationCallback callback;
|
||||
private FusedLocationProviderClient fusedLocationClient;
|
||||
|
||||
public LocationUpdatesWorker(Context context, WorkerParameters params) {
|
||||
super(context, params);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
fusedLocationClient.removeLocationUpdates(callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableFuture<Result> startWork() {
|
||||
return CallbackToFutureAdapter.getFuture(completer -> {
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(currentContext);
|
||||
|
||||
callback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
Location location = locationResult.getLastLocation();
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
if (LocationUtils.showNotification(currentContext)) {
|
||||
NotificationManager notificationManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
notificationManager = currentContext.getSystemService(NotificationManager.class);
|
||||
} else {
|
||||
notificationManager = (NotificationManager)currentContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
notificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
notificationManager.notify(LocationUtils.WORKER_NOTIFICATION_ID, LocationUtils.getNotification(currentContext, location, LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID));
|
||||
}
|
||||
finish();
|
||||
completer.set(Result.success());
|
||||
}
|
||||
};
|
||||
|
||||
LocationRequest locationRequest = new LocationRequest();
|
||||
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
||||
locationRequest.setInterval(5000);
|
||||
try {
|
||||
fusedLocationClient.requestLocationUpdates(locationRequest,
|
||||
callback, Looper.myLooper());
|
||||
} catch (SecurityException e) {
|
||||
completer.setException(e);
|
||||
}
|
||||
return callback;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class LocationUtils {
|
||||
|
||||
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.location-updates-state";
|
||||
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.location-updates-interval";
|
||||
static final String KEY_LOCATION_SHOW_NOTIFICATION = "flutter.location-updates-show-notification";
|
||||
|
||||
static final String WORKER_NOTIFICATION_CHANNEL_ID = "location_worker";
|
||||
static final int WORKER_NOTIFICATION_ID = 954322;
|
||||
static final String SERVICE_NOTIFICATION_CHANNEL_ID = "location_service";
|
||||
static final int SERVICE_NOTIFICATION_ID = 954311;
|
||||
static final String ONETIME_NOTIFICATION_CHANNEL_ID = "location_request";
|
||||
static final int ONETIME_NOTIFICATION_ID = 954333;
|
||||
|
||||
static final String REQUEST_LOCATION_NOTIFICATION = "request_location_update";
|
||||
|
||||
static final String LOCATION_WORK_NAME = "HALocationWorker";
|
||||
static final String LOCATION_REQUEST_NAME = "HALocationRequest";
|
||||
|
||||
static final int LOCATION_UPDATES_DISABLED = 0;
|
||||
static final int LOCATION_UPDATES_SERVICE = 1;
|
||||
static final int LOCATION_UPDATES_WORKER = 2;
|
||||
|
||||
static final int DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
||||
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
||||
|
||||
static int getLocationUpdatesState(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getInt(KEY_REQUESTING_LOCATION_UPDATES, LOCATION_UPDATES_DISABLED);
|
||||
}
|
||||
|
||||
static long getLocationUpdateIntervals(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
static boolean showNotification(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_LOCATION_SHOW_NOTIFICATION, true);
|
||||
}
|
||||
|
||||
static void setLocationUpdatesState(Context context, int locationUpdatesState) {
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putInt(KEY_REQUESTING_LOCATION_UPDATES, locationUpdatesState)
|
||||
.apply();
|
||||
}
|
||||
|
||||
static void setLocationUpdatesSettings(Context context, long interval, boolean showNotification) {
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean(KEY_LOCATION_SHOW_NOTIFICATION, showNotification)
|
||||
.putLong(KEY_LOCATION_UPDATE_INTERVAL, interval)
|
||||
.apply();
|
||||
}
|
||||
|
||||
static void startService(Context context) {
|
||||
Intent myService = new Intent(context, LocationUpdatesService.class);
|
||||
context.startService(myService);
|
||||
}
|
||||
|
||||
static void startServiceFromBroadcast(Context context) {
|
||||
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent);
|
||||
} else {
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
static void startWorker(Context context, long interval) {
|
||||
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
|
||||
}
|
||||
|
||||
static void requestLocationOnce(Context context) {
|
||||
Intent myService = new Intent(context, LocationRequestService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(myService);
|
||||
} else {
|
||||
context.startService(myService);
|
||||
}
|
||||
//OneTimeWorkRequest oneTimeWork = new OneTimeWorkRequest.Builder(LocationUpdatesWorker.class)
|
||||
// .build();
|
||||
//WorkManager.getInstance(context).enqueueUniqueWork(LocationUtils.LOCATION_REQUEST_NAME, ExistingWorkPolicy.REPLACE, oneTimeWork);
|
||||
}
|
||||
|
||||
static Notification getNotification(Context context, Location location, String channelId) {
|
||||
CharSequence title = "Location tracking";
|
||||
CharSequence text = location == null ? "Accuracy: unknown" : "Accuracy: " + location.getAccuracy() + " m";
|
||||
CharSequence bigText = location == null ? "Waiting for location..." : "Time: " + DateFormat.getDateTimeInstance().format(new Date(location.getTime())) +
|
||||
System.getProperty("line.separator") + "Accuracy: " + location.getAccuracy() + " m" +
|
||||
System.getProperty("line.separator") + "Location: " + location.getLatitude() + ", " + location.getLongitude();
|
||||
|
||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
||||
new Intent(context, MainActivity.class), 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||
.setContentIntent(activityPendingIntent)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(bigText))
|
||||
.setPriority(-1)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.mini_icon_location)
|
||||
.setWhen(System.currentTimeMillis());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static Notification getRequestNotification(Context context, Location location, String channelId) {
|
||||
CharSequence title = "Updating location...";
|
||||
CharSequence text = location == null ? "Waiting for location..." : "Accuracy: " + location.getAccuracy() + " m";
|
||||
|
||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
||||
new Intent(context, MainActivity.class), 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||
.setContentIntent(activityPendingIntent)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setPriority(-1)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.mini_icon_location)
|
||||
.setWhen(System.currentTimeMillis());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
@ -1,61 +1,105 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
import com.google.firebase.iid.InstanceIdResult;
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
||||
|
||||
|
||||
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
|
||||
|
||||
private int locationUpdatesType = LocationUtils.LOCATION_UPDATES_DISABLED;
|
||||
private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_MS;
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
||||
new MethodChannel.MethodCallHandler() {
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
(call, result) -> {
|
||||
Context context = getActivity();
|
||||
if (call.method.equals("getFCMToken")) {
|
||||
if (checkPlayServices()) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
||||
if (task.isSuccessful()) {
|
||||
String token = task.getResult().getToken();
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
|
||||
updateTokenTask.execute(token);
|
||||
result.success(token);
|
||||
} else {
|
||||
result.error("fcm_error", task.getException().getMessage(), null);
|
||||
}
|
||||
switch (call.method) {
|
||||
case "getFCMToken":
|
||||
try {
|
||||
if (checkPlayServices()) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
String token = task.getResult().getToken();
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||
result.success(token);
|
||||
} else {
|
||||
Exception ex = task.getException();
|
||||
if (ex != null) {
|
||||
result.error("fcm_error", ex.getMessage(), null);
|
||||
} else {
|
||||
result.error("fcm_error", "Unknown", null);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.error("get_token_exception", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "startLocationService":
|
||||
try {
|
||||
locationUpdatesInterval = ((Number)call.argument("location-updates-interval")).longValue();
|
||||
boolean useForegroundService = (boolean)call.argument("foreground-location-tracking");
|
||||
|
||||
if (useForegroundService) {
|
||||
locationUpdatesType = LocationUtils.LOCATION_UPDATES_SERVICE;
|
||||
} else {
|
||||
locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
|
||||
}
|
||||
LocationUtils.setLocationUpdatesSettings(this, locationUpdatesInterval, (boolean)call.argument("location-updates-show-notification"));
|
||||
if (isNoLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
} else {
|
||||
startLocationUpdates();
|
||||
}
|
||||
result.success("");
|
||||
} catch (Exception e) {
|
||||
result.error("location_error", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "stopLocationService":
|
||||
try {
|
||||
stopLocationUpdates();
|
||||
result.success("");
|
||||
} catch (Exception e) {
|
||||
result.error("location_error", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "cancelOldLocationWorker":
|
||||
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
|
||||
result.success("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,9 +107,78 @@ public class MainActivity extends FlutterActivity {
|
||||
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
|
||||
}
|
||||
|
||||
private void startLocationUpdates() {
|
||||
if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_SERVICE) {
|
||||
LocationUtils.startService(this);
|
||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
||||
} else if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_WORKER) {
|
||||
LocationUtils.startWorker(this, locationUpdatesInterval);
|
||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
||||
} else {
|
||||
stopLocationUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopLocationUpdates() {
|
||||
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
|
||||
stopService(myService);
|
||||
WorkManager.getInstance(this).cancelUniqueWork(LocationUtils.LOCATION_WORK_NAME);
|
||||
NotificationManager notificationManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
notificationManager = getSystemService(NotificationManager.class);
|
||||
} else {
|
||||
notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
notificationManager.cancel(LocationUtils.WORKER_NOTIFICATION_ID);
|
||||
LocationUtils.setLocationUpdatesState(this, LocationUtils.LOCATION_UPDATES_DISABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private boolean isNoLocationPermissions() {
|
||||
return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
ActivityCompat.requestPermissions(MainActivity.this,
|
||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||
REQUEST_PERMISSIONS_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startLocationUpdates();
|
||||
} else {
|
||||
stopLocationUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package com.keyboardcrumbs.hassclient;
|
||||
import java.util.Map;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
@ -14,6 +13,8 @@ import android.content.Intent;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
@ -21,12 +22,15 @@ import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
|
||||
public class MessagingService extends FirebaseMessagingService {
|
||||
|
||||
private static final String TAG = "MessagingService";
|
||||
private static final String TAG = MessagingService.class.getSimpleName();
|
||||
|
||||
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
@ -39,26 +43,31 @@ public class MessagingService extends FirebaseMessagingService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewToken(String token) {
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
|
||||
updateTokenTask.execute(token);
|
||||
public void onNewToken(@NonNull String token) {
|
||||
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||
}
|
||||
|
||||
private void sendNotification(Map<String, String> data) {
|
||||
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
||||
boolean autoCancel;
|
||||
if (!data.containsKey("channelId")) {
|
||||
channelId = "ha_notify";
|
||||
channelDescription = "Default notification channel";
|
||||
} else {
|
||||
channelId = data.get("channelId");
|
||||
channelDescription = channelId;
|
||||
}
|
||||
if (!data.containsKey("body")) {
|
||||
messageBody = "";
|
||||
} else {
|
||||
messageBody = data.get("body");
|
||||
}
|
||||
if (messageBody != null && messageBody.equals(LocationUtils.REQUEST_LOCATION_NOTIFICATION)) {
|
||||
Log.d(TAG, "Location update request received");
|
||||
LocationUtils.requestLocationOnce(this);
|
||||
return;
|
||||
}
|
||||
String customChannelId = data.get("channelId");
|
||||
if (customChannelId == null) {
|
||||
channelId = "ha_notify";
|
||||
channelDescription = "Default notification channel";
|
||||
} else {
|
||||
channelId = customChannelId;
|
||||
channelDescription = channelId;
|
||||
}
|
||||
if (!data.containsKey("title")) {
|
||||
messageTitle = "HA Client";
|
||||
} else {
|
||||
@ -105,22 +114,26 @@ public class MessagingService extends FirebaseMessagingService {
|
||||
.setAutoCancel(autoCancel)
|
||||
.setSound(defaultSoundUri)
|
||||
.setContentIntent(pendingIntent);
|
||||
Bitmap image = null;
|
||||
if (URLUtil.isValidUrl(imageUrl)) {
|
||||
Bitmap image = getBitmapFromURL(imageUrl);
|
||||
if (image != null) {
|
||||
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
|
||||
notificationBuilder.setLargeIcon(image);
|
||||
}
|
||||
image = getBitmapFromURL(imageUrl);
|
||||
}
|
||||
if (image != null) {
|
||||
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
|
||||
notificationBuilder.setLargeIcon(image);
|
||||
} else {
|
||||
notificationBuilder.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(messageBody));
|
||||
}
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
if (data.containsKey("action" + i)) {
|
||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
|
||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class).setAction(NOTIFICATION_ACTION_BROADCAST);
|
||||
if (autoCancel) {
|
||||
broadcastIntent.putExtra("tag", nTag);
|
||||
}
|
||||
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
|
||||
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent);
|
||||
notificationBuilder.addAction(R.drawable.blank_icon, data.get("action" + i), actionIntent);
|
||||
}
|
||||
}
|
||||
NotificationManager notificationManager =
|
||||
|
@ -7,19 +7,17 @@ import android.content.Intent;
|
||||
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = "NextAlarmReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
@ -35,12 +33,17 @@ public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data workerData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NEXT_ALARM)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class)
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
10,
|
||||
TimeUnit.SECONDS)
|
||||
.setInputData(workerData)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
|
||||
|
@ -1,73 +1,58 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class NotificationActionReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = "NotificationAction";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String intentAction = intent.getAction();
|
||||
if (intentAction == null || !intentAction.equalsIgnoreCase(MessagingService.NOTIFICATION_ACTION_BROADCAST)) {
|
||||
return;
|
||||
}
|
||||
String rawActionData = intent.getStringExtra("actionData");
|
||||
if (intent.hasExtra("tag")) {
|
||||
String notificationTag = intent.getStringExtra("tag");
|
||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationTag, 0);
|
||||
}
|
||||
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
JSONObject actionData = new JSONObject(rawActionData);
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
JSONObject requestData = new JSONObject();
|
||||
if (actionData.getString("action").equals("call-service")) {
|
||||
dataToSend.put("type", "call_service");
|
||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
||||
if (actionData.has("service_data")) {
|
||||
requestData.put("service_data", actionData.get("service_data"));
|
||||
}
|
||||
} else {
|
||||
dataToSend.put("type", "fire_event");
|
||||
requestData.put("event_type", "ha_client_event");
|
||||
JSONObject eventData = new JSONObject();
|
||||
eventData.put("action", actionData.getString("action"));
|
||||
requestData.put("event_data", eventData);
|
||||
}
|
||||
dataToSend.put("data", requestData);
|
||||
String stringRequest = dataToSend.toString();
|
||||
SendTask sendTask = new SendTask();
|
||||
sendTask.execute(requestUrl, stringRequest);
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error handling notification action", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
}
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
Data workerData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
|
||||
.putString("rawActionData", rawActionData)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
10,
|
||||
TimeUnit.SECONDS)
|
||||
.setInputData(workerData)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class RestartLocationUpdate extends BroadcastReceiver {
|
||||
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE &&
|
||||
(Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()) || Intent.ACTION_MY_PACKAGE_REPLACED.equalsIgnoreCase(intent.getAction()))) {
|
||||
LocationUtils.startServiceFromBroadcast(context);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.BatteryManager;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SendDataHomeWorker extends Worker {
|
||||
public static final String DATA_TYPE_KEY = "dataType";
|
||||
|
||||
public static final int DATA_TYPE_LOCATION = 1;
|
||||
public static final int DATA_TYPE_NEXT_ALARM = 2;
|
||||
public static final int DATA_TYPE_NOTIFICATION_ACTION = 3;
|
||||
|
||||
private Context currentContext;
|
||||
private static final String TAG = "SendDataHomeWorker";
|
||||
|
||||
public static final String KEY_LAT_ARG = "Lat";
|
||||
public static final String KEY_LONG_ARG = "Long";
|
||||
public static final String KEY_ACC_ARG = "Acc";
|
||||
|
||||
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
|
||||
|
||||
public SendDataHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Log.d(TAG, "Start sending data home");
|
||||
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
int dataType = getInputData().getInt(DATA_TYPE_KEY, 0);
|
||||
String stringRequest;
|
||||
if (dataType == DATA_TYPE_LOCATION) {
|
||||
Log.d(TAG, "Location data");
|
||||
stringRequest = getLocationDataToSend();
|
||||
} else if (dataType == DATA_TYPE_NEXT_ALARM) {
|
||||
Log.d(TAG, "Next alarm data");
|
||||
stringRequest = getNextAlarmDataToSend();
|
||||
} else if (dataType == DATA_TYPE_NOTIFICATION_ACTION) {
|
||||
Log.d(TAG, "Notification action data");
|
||||
stringRequest = getNotificationActionData();
|
||||
} else {
|
||||
Log.e(TAG, "doWork() unknown data type: " + dataType);
|
||||
return Result.failure();
|
||||
}
|
||||
try {
|
||||
URL url = new URL(requestUrl);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
assert stringRequest != null;
|
||||
byte[] outputBytes = stringRequest.getBytes(StandardCharsets.UTF_8);
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
urlConnection.disconnect();
|
||||
if (responseCode >= 300) {
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
return Result.retry();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
return Result.failure();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error =(", e);
|
||||
return Result.failure();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
return Result.failure();
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private String getLocationDataToSend() {
|
||||
try {
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
dataToSend.put("type", "update_location");
|
||||
JSONObject dataObject = new JSONObject();
|
||||
|
||||
JSONArray gps = new JSONArray();
|
||||
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
|
||||
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
|
||||
|
||||
dataObject.put("gps", gps);
|
||||
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
|
||||
|
||||
BatteryManager bm;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
bm = currentContext.getSystemService(BatteryManager.class);
|
||||
} else {
|
||||
bm = (BatteryManager)currentContext.getSystemService(Context.BATTERY_SERVICE);
|
||||
}
|
||||
int batLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
|
||||
dataObject.put("battery", batLevel);
|
||||
|
||||
dataToSend.put("data", dataObject);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getLocationDataToSend", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getNotificationActionData() {
|
||||
try {
|
||||
String rawActionData = getInputData().getString("rawActionData");
|
||||
if (rawActionData == null || rawActionData.length() == 0) {
|
||||
Log.e(TAG,"getNotificationActionData rawAction data is empty");
|
||||
return null;
|
||||
}
|
||||
JSONObject actionData = new JSONObject(rawActionData);
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
JSONObject requestData = new JSONObject();
|
||||
if (actionData.getString("action").equals("call-service")) {
|
||||
dataToSend.put("type", "call_service");
|
||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
||||
if (actionData.has("service_data")) {
|
||||
requestData.put("service_data", actionData.get("service_data"));
|
||||
}
|
||||
} else {
|
||||
dataToSend.put("type", "fire_event");
|
||||
requestData.put("event_type", "ha_client_event");
|
||||
JSONObject eventData = new JSONObject();
|
||||
eventData.put("action", actionData.getString("action"));
|
||||
requestData.put("event_data", eventData);
|
||||
}
|
||||
dataToSend.put("data", requestData);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getNotificationActionData", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getNextAlarmDataToSend() {
|
||||
try {
|
||||
final AlarmManager alarmManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
alarmManager = currentContext.getSystemService(AlarmManager.class);
|
||||
} else {
|
||||
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
|
||||
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
dataToSend.put("type", "update_sensor_states");
|
||||
JSONArray dataArray = new JSONArray();
|
||||
JSONObject sensorData = new JSONObject();
|
||||
JSONObject sensorAttrs = new JSONObject();
|
||||
sensorData.put("unique_id", "next_alarm");
|
||||
sensorData.put("type", "sensor");
|
||||
final long triggerTimestamp;
|
||||
if (alarmClockInfo != null) {
|
||||
triggerTimestamp = alarmClockInfo.getTriggerTime();
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(triggerTimestamp);
|
||||
Date date = calendar.getTime();
|
||||
sensorData.put("state", DATE_TIME_FORMAT.format(date));
|
||||
sensorAttrs.put("date", DATE_FORMAT.format(date));
|
||||
sensorAttrs.put("time", TIME_FORMAT.format(date));
|
||||
sensorAttrs.put("timestamp", triggerTimestamp);
|
||||
} else {
|
||||
sensorData.put("state", "");
|
||||
sensorAttrs.put("date", "");
|
||||
sensorAttrs.put("time", "");
|
||||
sensorAttrs.put("timestamp", 0);
|
||||
}
|
||||
sensorData.put("icon", "mdi:alarm");
|
||||
sensorData.put("attributes", sensorAttrs);
|
||||
dataArray.put(0, sensorData);
|
||||
dataToSend.put("data", dataArray);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getNextAlarmDataToSend", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.util.Log;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class SendTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private static final String TAG = "SendTask";
|
||||
|
||||
public SendTask(){
|
||||
//set context variables if required
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
String urlString = params[0];
|
||||
String data = params[1];
|
||||
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
byte[] outputBytes = data.getBytes("UTF-8");
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
|
||||
urlConnection.disconnect();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
public class UpdateNextAlarmWorker extends Worker {
|
||||
|
||||
private Context currentContext;
|
||||
private static final String TAG = "NextAlarmWorker";
|
||||
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
|
||||
|
||||
public UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
final AlarmManager alarmManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
alarmManager = currentContext.getSystemService(AlarmManager.class);
|
||||
} else {
|
||||
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
|
||||
|
||||
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
dataToSend.put("type", "update_sensor_states");
|
||||
JSONArray dataArray = new JSONArray();
|
||||
JSONObject sensorData = new JSONObject();
|
||||
JSONObject sensorAttrs = new JSONObject();
|
||||
sensorData.put("unique_id", "next_alarm");
|
||||
sensorData.put("type", "sensor");
|
||||
final long triggerTimestamp;
|
||||
if (alarmClockInfo != null) {
|
||||
triggerTimestamp = alarmClockInfo.getTriggerTime();
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(triggerTimestamp);
|
||||
sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime()));
|
||||
sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime()));
|
||||
sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime()));
|
||||
sensorAttrs.put("timestamp", triggerTimestamp);
|
||||
} else {
|
||||
sensorData.put("state", "");
|
||||
sensorAttrs.put("date", "");
|
||||
sensorAttrs.put("time", "");
|
||||
sensorAttrs.put("timestamp", 0);
|
||||
}
|
||||
sensorData.put("icon", "mdi:alarm");
|
||||
sensorData.put("attributes", sensorAttrs);
|
||||
dataArray.put(0, sensorData);
|
||||
dataToSend.put("data", dataArray);
|
||||
|
||||
String stringRequest = dataToSend.toString();
|
||||
try {
|
||||
URL url = new URL(requestUrl);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
byte[] outputBytes = stringRequest.getBytes("UTF-8");
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
urlConnection.disconnect();
|
||||
if (responseCode >= 300) {
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
return Result.retry();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
return Result.failure();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error setting next alarm", e);
|
||||
return Result.failure();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
return Result.failure();
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.util.Log;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.Context;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
|
||||
public class UpdateTokenTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private static final String TAG = "UpdateTokenTask";
|
||||
|
||||
private WeakReference<Context> contextRef;
|
||||
|
||||
public UpdateTokenTask(Context context){
|
||||
contextRef = new WeakReference<>(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
Log.d(TAG, "Updating push token");
|
||||
Context context = contextRef.get();
|
||||
if (context != null) {
|
||||
String token = params[0];
|
||||
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString("flutter.npush-token", token);
|
||||
editor.commit();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable/mini_icon_location.png
Normal file
BIN
android/app/src/main/res/drawable/mini_icon_location.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 571 B |
Binary file not shown.
BIN
fonts/materialdesignicons-webfont-5.3.45.ttf
Normal file
BIN
fonts/materialdesignicons-webfont-5.3.45.ttf
Normal file
Binary file not shown.
@ -21,7 +21,6 @@ class CardData {
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
case CardType.HISTORY_GRAPH:
|
||||
case CardType.MAP:
|
||||
case CardType.PICTURE_GLANCE:
|
||||
case CardType.SENSOR:
|
||||
case CardType.ENTITY:
|
||||
@ -47,6 +46,9 @@ class CardData {
|
||||
return CardData(null);
|
||||
}
|
||||
break;
|
||||
case CardType.MAP:
|
||||
return MapCardData(rawData);
|
||||
break;
|
||||
case CardType.ENTITY_BUTTON:
|
||||
case CardType.BUTTON:
|
||||
case CardType.PICTURE_ENTITY:
|
||||
@ -644,6 +646,52 @@ class MarkdownCardData extends CardData {
|
||||
|
||||
}
|
||||
|
||||
class MapCardData extends CardData {
|
||||
|
||||
String title;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return MapCard(card: this);
|
||||
}
|
||||
|
||||
MapCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
List<dynamic> geoLocationSources = rawData['geo_location_sources'] ?? [];
|
||||
if (geoLocationSources.isNotEmpty) {
|
||||
//TODO add entities by source
|
||||
}
|
||||
var rawEntities = rawData["entities"] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: stateColor,
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MediaControlCardData extends CardData {
|
||||
|
||||
@override
|
||||
|
88
lib/cards/map_card.dart
Normal file
88
lib/cards/map_card.dart
Normal file
@ -0,0 +1,88 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MapCard extends StatefulWidget {
|
||||
final MapCardData card;
|
||||
|
||||
const MapCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MapCardState createState() => _MapCardState();
|
||||
}
|
||||
|
||||
class _MapCardState extends State<MapCard> {
|
||||
|
||||
void _openMap(BuildContext context) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (bc) {
|
||||
return Scaffold(
|
||||
primary: false,
|
||||
/*appBar: new AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.fullscreen),
|
||||
onPressed: () {},
|
||||
)
|
||||
],
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text("${widget.card.title ?? ""}"),
|
||||
),*/
|
||||
body: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
EntitiesMap(
|
||||
entities: widget.card.entities,
|
||||
interactive: true
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
})
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardWrapper(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
CardHeader(name: widget.card.title),
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () => _openMap(context),
|
||||
child: EntitiesMap(
|
||||
aspectRatio: 1,
|
||||
interactive: false,
|
||||
entities: widget.card.entities,
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: Text('Tap to open interactive map', style: Theme.of(context).textTheme.caption)
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
73
lib/cards/widgets/entities_map.dart
Normal file
73
lib/cards/widgets/entities_map.dart
Normal file
@ -0,0 +1,73 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
|
||||
class EntitiesMap extends StatelessWidget {
|
||||
|
||||
final List<EntityWrapper> entities;
|
||||
final bool interactive;
|
||||
final double aspectRatio;
|
||||
final LatLng center;
|
||||
final double zoom;
|
||||
|
||||
const EntitiesMap({Key key, this.entities: const [], this.aspectRatio, this.interactive: true, this.center, this.zoom}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Marker> markers = [];
|
||||
List<LatLng> points = [];
|
||||
entities.forEach((entityWrapper) {
|
||||
double lat = entityWrapper.entity._getDoubleAttributeValue("latitude");
|
||||
double long = entityWrapper.entity._getDoubleAttributeValue("longitude");
|
||||
if (lat != null && long != null) {
|
||||
points.add(LatLng(lat, long));
|
||||
markers.add(
|
||||
Marker(
|
||||
width: 36,
|
||||
height: 36,
|
||||
point: LatLng(lat, long),
|
||||
builder: (ctx) => EntityModel(
|
||||
handleTap: true,
|
||||
entityWrapper: entityWrapper,
|
||||
child: EntityIcon(
|
||||
size: 36,
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
MapOptions mapOptions;
|
||||
if (center != null) {
|
||||
mapOptions = MapOptions(
|
||||
interactive: interactive,
|
||||
center: center,
|
||||
zoom: zoom ?? 10,
|
||||
);
|
||||
} else {
|
||||
mapOptions = MapOptions(
|
||||
interactive: interactive,
|
||||
bounds: LatLngBounds.fromPoints(points),
|
||||
boundsOptions: FitBoundsOptions(padding: EdgeInsets.all(40)),
|
||||
);
|
||||
}
|
||||
Widget map = FlutterMap(
|
||||
options: mapOptions,
|
||||
layers: [
|
||||
new TileLayerOptions(
|
||||
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
subdomains: ['a', 'b', 'c']
|
||||
),
|
||||
new MarkerLayerOptions(
|
||||
markers: markers,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (aspectRatio != null) {
|
||||
return AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: map
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
@ -278,6 +278,7 @@ class HomeAssistant {
|
||||
_rawPanels = data;
|
||||
List<Panel> dashboards = [];
|
||||
data.forEach((k,v) {
|
||||
Logger.d('[HA] Panel $k: title=${v['title']}; component=${v['component_name']}');
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
|
||||
dashboards.add(
|
||||
|
@ -19,23 +19,23 @@ import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:battery/battery.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:syncfusion_flutter_core/core.dart';
|
||||
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong/latlong.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
import 'utils/logger.dart';
|
||||
import '.secrets.dart';
|
||||
|
||||
part 'const.dart';
|
||||
part 'utils/launcher.dart';
|
||||
part 'utils/RandomColorGenerator.dart';
|
||||
part 'entities/entity.class.dart';
|
||||
part 'entities/entity_wrapper.class.dart';
|
||||
part 'entities/timer/timer_entity.class.dart';
|
||||
@ -48,6 +48,7 @@ part 'entities/date_time/date_time_entity.class.dart';
|
||||
part 'entities/light/light_entity.class.dart';
|
||||
part 'entities/select/select_entity.class.dart';
|
||||
part 'entities/sun/sun_entity.class.dart';
|
||||
part 'cards/widgets/entities_map.dart';
|
||||
part 'entities/sensor/sensor_entity.class.dart';
|
||||
part 'entities/slider/slider_entity.dart';
|
||||
part 'entities/media_player/media_player_entity.class.dart';
|
||||
@ -61,6 +62,7 @@ part 'entities/entity_model.widget.dart';
|
||||
part 'entities/default_entity_container.widget.dart';
|
||||
part 'entities/missed_entity.widget.dart';
|
||||
part 'cards/entity_button_card.dart';
|
||||
part 'cards/map_card.dart';
|
||||
part 'pages/widgets/entity_attributes_list.dart';
|
||||
part 'entities/entity_icon.widget.dart';
|
||||
part 'entities/entity_name.widget.dart';
|
||||
@ -100,8 +102,6 @@ part 'entities/vacuum/widgets/vacuum_controls.dart';
|
||||
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
||||
part 'entities/error_entity_widget.dart';
|
||||
part 'pages/settings/connection_settings.part.dart';
|
||||
part 'pages/purchase.page.dart';
|
||||
part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/bottom_info_bar.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
@ -117,7 +117,6 @@ part 'pages/entity.page.dart';
|
||||
part 'utils/mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'managers/auth_manager.class.dart';
|
||||
part 'managers/location_manager.class.dart';
|
||||
part 'managers/mobile_app_integration_manager.class.dart';
|
||||
part 'managers/connection_manager.class.dart';
|
||||
part 'managers/device_info_manager.class.dart';
|
||||
@ -158,7 +157,7 @@ part 'managers/app_settings.dart';
|
||||
EventBus eventBus = new EventBus();
|
||||
const String appName = 'HA Client';
|
||||
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.2.0.md';
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.3.0.md';
|
||||
|
||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
// Print the exception to the console.
|
||||
@ -212,50 +211,20 @@ class HAClientApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HAClientAppState extends State<HAClientApp> {
|
||||
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
|
||||
StreamSubscription _themeChangeSubscription;
|
||||
AppTheme _currentTheme = AppTheme.defaultTheme;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
_currentTheme = widget.theme;
|
||||
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
|
||||
setState(() {
|
||||
_currentTheme = event.theme;
|
||||
});
|
||||
});
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
positiveText: "Ok"
|
||||
)
|
||||
));
|
||||
InAppPurchaseConnection.instance.completePurchase(purchase[0]);
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new MaterialApp(
|
||||
@ -269,7 +238,6 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
"/app-settings": (context) => AppSettingsPage(),
|
||||
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
|
||||
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
|
||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||
"/play-media": (context) => PlayMediaPage(
|
||||
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
||||
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
||||
@ -316,7 +284,6 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_purchaseUpdateSubscription.cancel();
|
||||
_themeChangeSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ class AppSettings {
|
||||
|
||||
static const AUTH_TOKEN_KEY = 'llt';
|
||||
|
||||
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||
|
||||
static final AppSettings _instance = AppSettings._internal();
|
||||
|
||||
factory AppSettings() {
|
||||
@ -31,9 +33,6 @@ class AppSettings {
|
||||
bool nextAlarmSensorCreated = false;
|
||||
DisplayMode displayMode;
|
||||
AppTheme appTheme;
|
||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||
Duration locationUpdateInterval;
|
||||
bool locationTrackingEnabled = false;
|
||||
|
||||
bool get isAuthenticated => longLivedToken != null;
|
||||
bool get isTempAuthenticated => tempToken != null;
|
||||
@ -49,6 +48,7 @@ class AppSettings {
|
||||
await Hive.openBox(DEFAULT_HIVE_BOX);
|
||||
Logger.d('Loading settings...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await migrate(prefs);
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
@ -59,10 +59,6 @@ class AppSettings {
|
||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||
httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||
defaultLocationUpdateIntervalMinutes);
|
||||
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false;
|
||||
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
@ -71,6 +67,38 @@ class AppSettings {
|
||||
}
|
||||
}
|
||||
|
||||
Future migrate(SharedPreferences prefs) async {
|
||||
//Migrating to new location tracking. TODO: Remove when no version 1.2.0 (and older) in the wild
|
||||
if (prefs.getBool("location-tracking-migrated") == null) {
|
||||
Logger.d("[MIGRATION] Migrating to new location tracking...");
|
||||
bool oldLocationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
if (oldLocationTrackingEnabled) {
|
||||
await platform.invokeMethod('cancelOldLocationWorker');
|
||||
int oldLocationTrackingInterval = prefs.getInt("location-interval") ?? 0;
|
||||
if (oldLocationTrackingInterval < 15) {
|
||||
oldLocationTrackingInterval = 15;
|
||||
}
|
||||
try {
|
||||
await platform.invokeMethod('startLocationService', <String, dynamic>{
|
||||
'location-updates-interval': oldLocationTrackingInterval * 60 * 1000,
|
||||
//'location-updates-priority': 100,
|
||||
'location-updates-show-notification': true,
|
||||
'foreground-location-tracking': 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 {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.get('$key');
|
||||
|
@ -1,229 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LocationManager {
|
||||
|
||||
static final LocationManager _instance = LocationManager
|
||||
._internal();
|
||||
|
||||
factory LocationManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
LocationManager._internal() {
|
||||
init();
|
||||
}
|
||||
|
||||
final String backgroundTaskId = "haclocationtask0";
|
||||
final String backgroundTaskTag = "haclocation";
|
||||
|
||||
void init() async {
|
||||
if (AppSettings().locationTrackingEnabled) {
|
||||
await _startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
setSettings(bool enabled, int interval) async {
|
||||
if (interval != AppSettings().locationUpdateInterval.inMinutes) {
|
||||
await _stopLocationService();
|
||||
}
|
||||
AppSettings().save({
|
||||
'location-interval': interval,
|
||||
'location-enabled': enabled
|
||||
});
|
||||
AppSettings().locationUpdateInterval = Duration(minutes: interval);
|
||||
AppSettings().locationTrackingEnabled = enabled;
|
||||
if (enabled && !AppSettings().locationTrackingEnabled) {
|
||||
Logger.d("Starting location tracking");
|
||||
await _startLocationService();
|
||||
} else if (!enabled && AppSettings().locationTrackingEnabled) {
|
||||
Logger.d("Stopping location tracking...");
|
||||
await _stopLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
_startLocationService() async {
|
||||
String webhookId = AppSettings().webhookId;
|
||||
String httpWebHost = AppSettings().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Duration interval;
|
||||
int delayFactor;
|
||||
int taskCount;
|
||||
Logger.d("Starting location update for every ${AppSettings().locationUpdateInterval
|
||||
.inMinutes} minutes...");
|
||||
if (AppSettings().locationUpdateInterval.inMinutes == 10) {
|
||||
interval = Duration(minutes: 20);
|
||||
taskCount = 2;
|
||||
delayFactor = 10;
|
||||
} else if (AppSettings().locationUpdateInterval.inMinutes == 5) {
|
||||
interval = Duration(minutes: 15);
|
||||
taskCount = 3;
|
||||
delayFactor = 5;
|
||||
} else {
|
||||
interval = AppSettings().locationUpdateInterval;
|
||||
taskCount = 1;
|
||||
delayFactor = 0;
|
||||
}
|
||||
for (int i = 1; i <= taskCount; i++) {
|
||||
int delay = i*delayFactor;
|
||||
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
|
||||
await workManager.Workmanager.registerPeriodicTask(
|
||||
"$backgroundTaskId$i",
|
||||
"haClientLocationTracking-0$i",
|
||||
tag: backgroundTaskTag,
|
||||
inputData: {
|
||||
"webhookId": webhookId,
|
||||
"httpWebHost": httpWebHost
|
||||
},
|
||||
frequency: interval,
|
||||
initialDelay: Duration(minutes: delay),
|
||||
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
|
||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||
backoffPolicyDelay: interval,
|
||||
constraints: workManager.Constraints(
|
||||
networkType: workManager.NetworkType.connected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelAll();
|
||||
}
|
||||
|
||||
updateDeviceLocation() async {
|
||||
try {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = AppSettings().webhookId;
|
||||
String httpWebHost = AppSettings().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Logger.d("[Foreground location] Getting battery level...");
|
||||
int batteryLevel = await battery.batteryLevel;
|
||||
Logger.d("[Foreground location] Getting device location...");
|
||||
Position position = await geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationPermissionLevel: GeolocationPermission.locationAlways
|
||||
);
|
||||
if (position != null) {
|
||||
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [position.latitude, position.longitude],
|
||||
"gps_accuracy": position.accuracy,
|
||||
"battery": batteryLevel ?? 100
|
||||
}
|
||||
};
|
||||
Logger.d("[Foreground location] Sending data home...");
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(data)
|
||||
);
|
||||
if (response.statusCode >= 300) {
|
||||
Logger.e('Foreground location update error: ${response.body}');
|
||||
}
|
||||
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
|
||||
} else {
|
||||
Logger.d("[Foreground location] No location. Aborting.");
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void updateDeviceLocationIsolate() {
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||
//print("[Background $backgroundTask] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = data["webhookId"];
|
||||
String httpWebHost = data["httpWebHost"];
|
||||
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
|
||||
//print("[Background $backgroundTask] Getting path for log file...");
|
||||
//final logFileDirectory = await getExternalStorageDirectory();
|
||||
//print("[Background $backgroundTask] Opening log file...");
|
||||
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
|
||||
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
//print("[Background $backgroundTask] Getting battery level...");
|
||||
int batteryLevel;
|
||||
try {
|
||||
batteryLevel = await battery.batteryLevel;
|
||||
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||
} catch(e) {
|
||||
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||
batteryLevel = 0;
|
||||
//logData += 'Battery: error, $e';
|
||||
}
|
||||
if (batteryLevel != null) {
|
||||
data["data"]["battery"] = batteryLevel;
|
||||
//logData += 'Battery: success, $batteryLevel';
|
||||
}/* else {
|
||||
logData += 'Battery: error, level is null';
|
||||
}*/
|
||||
Position location;
|
||||
try {
|
||||
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||
if (location != null && location.latitude != null) {
|
||||
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
/*if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
logData += ' || Post: success, ${response.statusCode}';
|
||||
} else {
|
||||
logData += ' || Post: error, ${response.statusCode}';
|
||||
}*/
|
||||
} catch(e) {
|
||||
//logData += ' || Post: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += ' || Location: error, location is null';
|
||||
}*/
|
||||
} catch (e) {
|
||||
//print("[Background $backgroundTask] Location error: $e");
|
||||
//logData += ' || Location: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += 'Not configured';
|
||||
}*/
|
||||
//print("[Background $backgroundTask] Writing log data...");
|
||||
/*try {
|
||||
var fileMode;
|
||||
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
|
||||
fileMode = FileMode.append;
|
||||
} else {
|
||||
fileMode = FileMode.write;
|
||||
}
|
||||
await logFile.writeAsString('$logData\n', mode: fileMode);
|
||||
} catch (e) {
|
||||
print("[Background $backgroundTask] Error writing log: $e");
|
||||
}
|
||||
print("[Background $backgroundTask] Finished.");*/
|
||||
return true;
|
||||
});
|
||||
}
|
@ -11,53 +11,18 @@ class StartupUserMessagesManager {
|
||||
|
||||
StartupUserMessagesManager._internal();
|
||||
|
||||
bool _needToshowDonateMessage;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _donateMsgTimerKey = "user-msg-donate-timer";
|
||||
static final _donateMsgShownKey = "user-msg-donate-shpown";
|
||||
static final _whatsNewMessageKey = "user-msg-whats-new-url";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
var tInt = prefs.getInt(_donateMsgTimerKey);
|
||||
if (tInt == null) {
|
||||
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
|
||||
_needToshowDonateMessage = false;
|
||||
} else {
|
||||
bool wasShown = prefs.getBool(_donateMsgShownKey) ?? false;
|
||||
_needToshowDonateMessage = (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch - tInt).inDays >= 14) && !wasShown;
|
||||
}
|
||||
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else if (_needToshowDonateMessage) {
|
||||
_showSupportAppDevelopmentMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void _showSupportAppDevelopmentMessage() {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Hi!",
|
||||
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
||||
positiveText: "Show options",
|
||||
negativeText: "Later",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_donateMsgShownKey, true);
|
||||
eventBus.fire(ShowPageEvent(path: "/putchase"));
|
||||
});
|
||||
},
|
||||
onNegative: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
|
||||
});
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
void _showWhatsNewMessage() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString(_whatsNewMessageKey, whatsNewUrl);
|
||||
|
@ -56,7 +56,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
_fetchData(useCache: true);
|
||||
LocationManager();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
});
|
||||
@ -305,15 +304,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
|
||||
title: Text("Support app development"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/putchase');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.help),
|
||||
title: Text("Help"),
|
||||
@ -409,7 +399,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
|
||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
//Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||
_previousViewCount = currentViewCount;
|
||||
}
|
||||
|
@ -1,118 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PurchasePage extends StatefulWidget {
|
||||
PurchasePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_PurchasePageState createState() => new _PurchasePageState();
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
List<ProductDetails> _products;
|
||||
List<PurchaseDetails> _purchases;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
_loadProducts() async {
|
||||
final bool available = await InAppPurchaseConnection.instance.isAvailable();
|
||||
if (!available) {
|
||||
setState(() {
|
||||
_error = "Error connecting to store";
|
||||
});
|
||||
} else {
|
||||
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
_loadPreviousPurchases();
|
||||
}
|
||||
}
|
||||
|
||||
_loadPreviousPurchases() async {
|
||||
final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases();
|
||||
if (response.error != null) {
|
||||
setState(() {
|
||||
_error = "Error loading previous purchases";
|
||||
});
|
||||
} else {
|
||||
_purchases = response.pastPurchases;
|
||||
for (PurchaseDetails purchase in _purchases) {
|
||||
Logger.d("Previous purchase: ${purchase.status}");
|
||||
}
|
||||
if (_products.isEmpty) {
|
||||
setState(() {
|
||||
_error = "No data found in store";
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildProducts() {
|
||||
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) {
|
||||
productWidgets.add(
|
||||
ProductPurchase(
|
||||
product: product,
|
||||
onBuy: (product) => _buyProduct(product),
|
||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||
);
|
||||
}
|
||||
return productWidgets;
|
||||
}
|
||||
|
||||
void _buyProduct(ProductDetails product) {
|
||||
Logger.d("Starting purchase of ${product.id}");
|
||||
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
|
||||
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> body;
|
||||
if (!_loaded) {
|
||||
body = [_error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error)];
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
children: body
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -11,9 +11,18 @@ class IntegrationSettingsPage extends StatefulWidget {
|
||||
|
||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
|
||||
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||
/*static final locationAccuracy = {
|
||||
100: "High",
|
||||
102: "Balanced"
|
||||
};*/
|
||||
|
||||
Duration _locationInterval;
|
||||
bool _locationTrackingEnabled = false;
|
||||
bool _wait = false;
|
||||
bool _showNotification = true;
|
||||
//int _accuracy = 100;
|
||||
bool _useForegroundService = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -27,103 +36,235 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
await prefs.reload();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
_locationInterval = prefs.getInt("location-interval") ??
|
||||
AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
if (_locationInterval % 5 != 0) {
|
||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||
}
|
||||
//_accuracy = prefs.getInt("location-updates-priority") ?? 100;
|
||||
_locationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0;
|
||||
_showNotification = prefs.getBool("location-updates-show-notification") ?? true;
|
||||
_useForegroundService = prefs.getBool("foreground-location-tracking") ?? false;
|
||||
_locationInterval = Duration(milliseconds: prefs.getInt("location-updates-interval") ??
|
||||
900000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _incLocationInterval() {
|
||||
if (_locationInterval < 720) {
|
||||
if (_locationInterval.inSeconds < 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + 5;
|
||||
_locationInterval = _locationInterval + Duration(seconds: 5);
|
||||
});
|
||||
} else if (_locationInterval.inMinutes < 15) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(minutes: 1);
|
||||
});
|
||||
} else if (_locationInterval.inMinutes < 60) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(minutes: 5);
|
||||
});
|
||||
} else if (_locationInterval.inHours < 4) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(minutes: 10);
|
||||
});
|
||||
} else if (_locationInterval.inHours < 48) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + Duration(hours: 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _decLocationInterval() {
|
||||
if (_locationInterval > 5) {
|
||||
if ((_useForegroundService && _locationInterval.inSeconds > 5) || (!_useForegroundService && _locationInterval.inMinutes > 15)) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - 5;
|
||||
if (_locationInterval.inSeconds <= 60) {
|
||||
_locationInterval = _locationInterval - Duration(seconds: 5);
|
||||
} else if (_locationInterval.inMinutes <= 15) {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 1);
|
||||
} else if (_locationInterval.inMinutes <= 60) {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 5);
|
||||
} else if (_locationInterval.inHours <= 4) {
|
||||
_locationInterval = _locationInterval - Duration(minutes: 10);
|
||||
} else if (_locationInterval.inHours > 4) {
|
||||
_locationInterval = _locationInterval - Duration(hours: 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_switchLocationTrackingState(bool state) async {
|
||||
if (state) {
|
||||
await LocationManager().updateDeviceLocation();
|
||||
try {
|
||||
await platform.invokeMethod('startLocationService', <String, dynamic>{
|
||||
'location-updates-interval': _locationInterval.inMilliseconds,
|
||||
//'location-updates-priority': _accuracy,
|
||||
'foreground-location-tracking': _useForegroundService,
|
||||
'location-updates-show-notification': _showNotification
|
||||
});
|
||||
} catch (e) {
|
||||
_locationTrackingEnabled = false;
|
||||
}
|
||||
} else {
|
||||
await platform.invokeMethod('stopLocationService');
|
||||
}
|
||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
setState(() {
|
||||
_wait = false;
|
||||
});
|
||||
}
|
||||
|
||||
String _formatInterval() {
|
||||
String result = "";
|
||||
Duration leftToShow = Duration(seconds: _locationInterval?.inSeconds ?? 0);
|
||||
if (leftToShow.inHours > 0) {
|
||||
result += "${leftToShow.inHours} h ";
|
||||
leftToShow -= Duration(hours: leftToShow.inHours);
|
||||
}
|
||||
if (leftToShow.inMinutes > 0) {
|
||||
result += "${leftToShow.inMinutes} m";
|
||||
leftToShow -= Duration(hours: leftToShow.inMinutes);
|
||||
}
|
||||
if (leftToShow.inSeconds > 0) {
|
||||
result += "${leftToShow.inSeconds} s";
|
||||
leftToShow -= Duration(hours: leftToShow.inSeconds);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget _getNoteWidget(String text, bool important) {
|
||||
return Text(
|
||||
text,
|
||||
style: important ? Theme.of(context).textTheme.caption.copyWith(color: Theme.of(context).errorColor) : Theme.of(context).textTheme.caption,
|
||||
softWrap: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getNotes() {
|
||||
List<Widget> notes = [];
|
||||
if (_locationTrackingEnabled) {
|
||||
notes.add(_getNoteWidget('* Stop location tracking to change settings', false));
|
||||
}
|
||||
if (_useForegroundService) {
|
||||
notes.add(_getNoteWidget('* Notification is mandatory for foreground service', false));
|
||||
} else {
|
||||
notes.add(_getNoteWidget('* Use foreground service for 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
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
InkWell(
|
||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
||||
child: Text(
|
||||
"Please read documentation!",
|
||||
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
)
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable device location tracking"),
|
||||
Switch(
|
||||
value: _locationTrackingEnabled,
|
||||
onChanged: _wait ? null : (value) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = value;
|
||||
_wait = true;
|
||||
});
|
||||
_switchLocationTrackingState(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("Location update interval in minutes:"),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
//Expanded(child: Container(),),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("-", style: Theme.of(context).textTheme.title),
|
||||
onPressed: () => _decLocationInterval(),
|
||||
),
|
||||
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("+", style: Theme.of(context).textTheme.title),
|
||||
onPressed: () => _incLocationInterval(),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
);
|
||||
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable"),
|
||||
Switch(
|
||||
value: _locationTrackingEnabled,
|
||||
onChanged: _wait ? null : (value) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = value;
|
||||
_wait = true;
|
||||
});
|
||||
_switchLocationTrackingState(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Use foreground service"),
|
||||
Switch(
|
||||
value: _useForegroundService,
|
||||
onChanged: _locationTrackingEnabled ? null : (value) {
|
||||
setState(() {
|
||||
_useForegroundService = value;
|
||||
if (!_useForegroundService && _locationInterval.inMinutes < 15) {
|
||||
_locationInterval = Duration(minutes: 15);
|
||||
} else if (_useForegroundService) {
|
||||
_showNotification = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
/*Text("Accuracy:", style: Theme.of(context).textTheme.body2),
|
||||
Container(height: Sizes.rowPadding),
|
||||
DropdownButton<int>(
|
||||
value: _accuracy,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
disabledHint: Text(locationAccuracy[_accuracy]),
|
||||
items: locationAccuracy.keys.map((value) {
|
||||
return new DropdownMenuItem<int>(
|
||||
value: value,
|
||||
child: Text('${locationAccuracy[value]}'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: _locationTrackingEnabled ? null : (val) {
|
||||
setState(() {
|
||||
_accuracy = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(height: Sizes.rowPadding),*/
|
||||
Text("Update interval"),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
//Expanded(child: Container(),),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("-", style: Theme.of(context).textTheme.headline4),
|
||||
onPressed: _locationTrackingEnabled ? null : () => _decLocationInterval(),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(_formatInterval(),
|
||||
textAlign: TextAlign.center,
|
||||
style: _locationTrackingEnabled ? Theme.of(context).textTheme.title.copyWith(color: HAClientTheme().getDisabledStateColor(context)) : Theme.of(context).textTheme.title),
|
||||
),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("+", style: Theme.of(context).textTheme.headline4),
|
||||
onPressed: _locationTrackingEnabled ? null : () => _incLocationInterval(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Show notification"),
|
||||
Switch(
|
||||
value: _showNotification,
|
||||
onChanged: (_locationTrackingEnabled || _useForegroundService) ? null : (value) {
|
||||
setState(() {
|
||||
_showNotification = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding),
|
||||
_getNotes()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ProductPurchase extends StatelessWidget {
|
||||
|
||||
final ProductDetails product;
|
||||
final onBuy;
|
||||
final purchased;
|
||||
|
||||
const ProductPurchase({Key key, @required this.product, @required this.onBuy, this.purchased}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String period = "";
|
||||
Color priceColor;
|
||||
String buttonText = '';
|
||||
String buttonTextInactive = '';
|
||||
if (product.id.contains("year")) {
|
||||
period += "once a year";
|
||||
buttonText = "Subscribe";
|
||||
buttonTextInactive = "Already";
|
||||
priceColor = Colors.amber;
|
||||
} else {
|
||||
period += "";
|
||||
buttonText = "Pay";
|
||||
buttonTextInactive = "Paid";
|
||||
priceColor = Colors.deepOrangeAccent;
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${product.title}",
|
||||
style: Theme.of(context).textTheme.body2,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"${product.description}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
softWrap: true,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${product.price} $period", style: Theme.of(context).textTheme.body1.copyWith(
|
||||
color: priceColor
|
||||
)),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: RaisedButton(
|
||||
child: Text(this.purchased ? buttonTextInactive : buttonText, style: Theme.of(context).textTheme.button),
|
||||
color: Colors.blue,
|
||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ class Panel {
|
||||
eventBus.fire(ReloadUIEvent());
|
||||
});
|
||||
} else {
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "${this.title}");
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "Back to app");
|
||||
}
|
||||
}
|
||||
|
||||
|
28
lib/utils/RandomColorGenerator.dart
Normal file
28
lib/utils/RandomColorGenerator.dart
Normal file
@ -0,0 +1,28 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class RandomColorGenerator {
|
||||
static const colorsList = [
|
||||
Colors.green,
|
||||
Colors.purple,
|
||||
Colors.indigo,
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.cyan
|
||||
];
|
||||
|
||||
int _index = 0;
|
||||
|
||||
Color getCurrent() {
|
||||
return colorsList[_index];
|
||||
}
|
||||
|
||||
Color getNext() {
|
||||
if (_index < colorsList.length - 1) {
|
||||
_index += 1;
|
||||
} else {
|
||||
_index = 1;
|
||||
}
|
||||
return getCurrent();
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -74,7 +74,7 @@ class HAView {
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return ViewWidget(
|
||||
view: this,
|
||||
view: this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
13
pubspec.yaml
13
pubspec.yaml
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 1.1.2+1160
|
||||
version: 1.3.1+1311
|
||||
|
||||
|
||||
environment:
|
||||
@ -19,19 +19,16 @@ dependencies:
|
||||
date_format: ^1.0.8
|
||||
charts_flutter: ^0.8.1
|
||||
flutter_markdown: ^0.3.3
|
||||
in_app_purchase: ^0.3.4
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
hive: ^1.4.1+1
|
||||
hive_flutter: ^0.3.0+2
|
||||
device_info: ^0.4.2+4
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
battery: ^1.0.0
|
||||
firebase_crashlytics: ^0.1.3+3
|
||||
syncfusion_flutter_core: ^18.1.52
|
||||
syncfusion_flutter_gauges: ^18.1.52
|
||||
syncfusion_flutter_core: ^18.2.54
|
||||
syncfusion_flutter_gauges: ^18.2.54
|
||||
flutter_map: ^0.10.1+1
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
@ -50,4 +47,4 @@ flutter:
|
||||
fonts:
|
||||
- family: "Material Design Icons"
|
||||
fonts:
|
||||
- asset: fonts/materialdesignicons-webfont-4.5.95.ttf
|
||||
- asset: fonts/materialdesignicons-webfont-5.3.45.ttf
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user