WIP #571 Native foreground servise with active location tracking
This commit is contained in:
parent
f84728b948
commit
7ebf5f7c18
@ -56,6 +56,10 @@
|
|||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".LocationUpdatesService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true" />
|
||||||
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
@ -0,0 +1,358 @@
|
|||||||
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import com.google.android.gms.tasks.OnCompleteListener;
|
||||||
|
import com.google.android.gms.tasks.Task;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bound and started service that is promoted to a foreground service when location updates have
|
||||||
|
* been requested and all clients unbind.
|
||||||
|
*
|
||||||
|
* For apps running in the background on "O" devices, location is computed only once every 10
|
||||||
|
* minutes and delivered batched every 30 minutes. This restriction applies even to apps
|
||||||
|
* targeting "N" or lower which are run on "O" devices.
|
||||||
|
*
|
||||||
|
* This sample show how to use a long-running service for location updates. When an activity is
|
||||||
|
* bound to this service, frequent location updates are permitted. When the activity is removed
|
||||||
|
* from the foreground, the service promotes itself to a foreground service, and location updates
|
||||||
|
* continue. When the activity comes back to the foreground, the foreground service stops, and the
|
||||||
|
* notification assocaited with that service is removed.
|
||||||
|
*/
|
||||||
|
public class LocationUpdatesService extends Service {
|
||||||
|
|
||||||
|
private static final String PACKAGE_NAME =
|
||||||
|
"com.google.android.gms.location.sample.locationupdatesforegroundservice";
|
||||||
|
|
||||||
|
private static final String TAG = LocationUpdatesService.class.getSimpleName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the channel for notifications.
|
||||||
|
*/
|
||||||
|
private static final String CHANNEL_ID = "channel_01";
|
||||||
|
|
||||||
|
static final String ACTION_BROADCAST = PACKAGE_NAME + ".broadcast";
|
||||||
|
|
||||||
|
static final String EXTRA_LOCATION = PACKAGE_NAME + ".location";
|
||||||
|
private static final String EXTRA_STARTED_FROM_NOTIFICATION = PACKAGE_NAME +
|
||||||
|
".started_from_notification";
|
||||||
|
|
||||||
|
private final IBinder mBinder = new LocalBinder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The desired interval for location updates. Inexact. Updates may be more or less frequent.
|
||||||
|
*/
|
||||||
|
private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fastest rate for active location updates. Updates will never be more frequent
|
||||||
|
* than this value.
|
||||||
|
*/
|
||||||
|
private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS =
|
||||||
|
UPDATE_INTERVAL_IN_MILLISECONDS / 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier for the notification displayed for the foreground service.
|
||||||
|
*/
|
||||||
|
private static final int NOTIFICATION_ID = 954311;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to check whether the bound activity has really gone away and not unbound as part of an
|
||||||
|
* orientation change. We create a foreground service notification only if the former takes
|
||||||
|
* place.
|
||||||
|
*/
|
||||||
|
private boolean mChangingConfiguration = false;
|
||||||
|
|
||||||
|
private NotificationManager mNotificationManager;
|
||||||
|
|
||||||
|
private LocationRequest mLocationRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to the Fused Location Provider API.
|
||||||
|
*/
|
||||||
|
private FusedLocationProviderClient mFusedLocationClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for changes in location.
|
||||||
|
*/
|
||||||
|
private LocationCallback mLocationCallback;
|
||||||
|
|
||||||
|
private Handler mServiceHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current location.
|
||||||
|
*/
|
||||||
|
private Location mLocation;
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createLocationRequest();
|
||||||
|
getLastLocation();
|
||||||
|
|
||||||
|
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||||
|
handlerThread.start();
|
||||||
|
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||||
|
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
// Android O requires a Notification Channel.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
CharSequence name = "Location service";
|
||||||
|
// Create the channel for the notification
|
||||||
|
NotificationChannel mChannel =
|
||||||
|
new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
|
||||||
|
// Set the Notification Channel for the Notification Manager.
|
||||||
|
mNotificationManager.createNotificationChannel(mChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Log.i(TAG, "Service started");
|
||||||
|
boolean startedFromNotification = intent.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// We got here because the user decided to remove location updates from the notification.
|
||||||
|
if (startedFromNotification) {
|
||||||
|
removeLocationUpdates();
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
// Tells the system to not try to recreate the service after it has been killed.
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
mChangingConfiguration = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
// Called when a client (MainActivity in case of this sample) comes to the foreground
|
||||||
|
// and binds with this service. The service should cease to be a foreground service
|
||||||
|
// when that happens.
|
||||||
|
Log.i(TAG, "in onBind()");
|
||||||
|
stopForeground(true);
|
||||||
|
mChangingConfiguration = false;
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRebind(Intent intent) {
|
||||||
|
// Called when a client (MainActivity in case of this sample) returns to the foreground
|
||||||
|
// and binds once again with this service. The service should cease to be a foreground
|
||||||
|
// service when that happens.
|
||||||
|
Log.i(TAG, "in onRebind()");
|
||||||
|
stopForeground(true);
|
||||||
|
mChangingConfiguration = false;
|
||||||
|
super.onRebind(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
Log.i(TAG, "Last client unbound from service");
|
||||||
|
|
||||||
|
// Called when the last client (MainActivity in case of this sample) unbinds from this
|
||||||
|
// service. If this method is called due to a configuration change in MainActivity, we
|
||||||
|
// do nothing. Otherwise, we make this service a foreground service.
|
||||||
|
if (!mChangingConfiguration && Utils.requestingLocationUpdates(this)) {
|
||||||
|
Log.i(TAG, "Starting foreground service");
|
||||||
|
/*
|
||||||
|
// TODO(developer). If targeting O, use the following code.
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {
|
||||||
|
mNotificationManager.startServiceInForeground(new Intent(this,
|
||||||
|
LocationUpdatesService.class), NOTIFICATION_ID, getNotification());
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, getNotification());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
startForeground(NOTIFICATION_ID, getNotification());
|
||||||
|
}
|
||||||
|
return true; // Ensures onRebind() is called when a client re-binds.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
mServiceHandler.removeCallbacksAndMessages(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request for location updates. Note that in this sample we merely log the
|
||||||
|
* {@link SecurityException}.
|
||||||
|
*/
|
||||||
|
public void requestLocationUpdates() {
|
||||||
|
Log.i(TAG, "Requesting location updates");
|
||||||
|
Utils.setRequestingLocationUpdates(this, true);
|
||||||
|
startService(new Intent(getApplicationContext(), LocationUpdatesService.class));
|
||||||
|
try {
|
||||||
|
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
||||||
|
mLocationCallback, Looper.myLooper());
|
||||||
|
} catch (SecurityException unlikely) {
|
||||||
|
Utils.setRequestingLocationUpdates(this, false);
|
||||||
|
Log.e(TAG, "Lost location permission. Could not request updates. " + unlikely);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes location updates. Note that in this sample we merely log the
|
||||||
|
* {@link SecurityException}.
|
||||||
|
*/
|
||||||
|
public void removeLocationUpdates() {
|
||||||
|
Log.i(TAG, "Removing location updates");
|
||||||
|
try {
|
||||||
|
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
||||||
|
Utils.setRequestingLocationUpdates(this, false);
|
||||||
|
stopSelf();
|
||||||
|
} catch (SecurityException unlikely) {
|
||||||
|
Utils.setRequestingLocationUpdates(this, true);
|
||||||
|
Log.e(TAG, "Lost location permission. Could not remove updates. " + unlikely);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link NotificationCompat} used as part of the foreground service.
|
||||||
|
*/
|
||||||
|
private Notification getNotification() {
|
||||||
|
Intent intent = new Intent(this, LocationUpdatesService.class);
|
||||||
|
|
||||||
|
CharSequence text = Utils.getLocationText(mLocation);
|
||||||
|
|
||||||
|
// Extra to help us figure out if we arrived in onStartCommand via the notification or not.
|
||||||
|
intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true);
|
||||||
|
|
||||||
|
// The PendingIntent that leads to a call to onStartCommand() in this service.
|
||||||
|
PendingIntent servicePendingIntent = PendingIntent.getService(this, 0, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// The PendingIntent to launch activity.
|
||||||
|
PendingIntent activityPendingIntent = PendingIntent.getActivity(this, 0,
|
||||||
|
new Intent(this, MainActivity.class), 0);
|
||||||
|
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.addAction(R.drawable.blank_icon, "Open HA Client",
|
||||||
|
activityPendingIntent)
|
||||||
|
.addAction(R.drawable.blank_icon, "Stop",
|
||||||
|
servicePendingIntent)
|
||||||
|
.setContentText(text)
|
||||||
|
.setContentTitle(Utils.getLocationTitle(this))
|
||||||
|
.setOngoing(true)
|
||||||
|
.setPriority(Notification.PRIORITY_HIGH)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setTicker(text)
|
||||||
|
.setWhen(System.currentTimeMillis());
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getLastLocation() {
|
||||||
|
try {
|
||||||
|
mFusedLocationClient.getLastLocation()
|
||||||
|
.addOnCompleteListener(new OnCompleteListener<Location>() {
|
||||||
|
@Override
|
||||||
|
public void onComplete(@NonNull Task<Location> task) {
|
||||||
|
if (task.isSuccessful() && task.getResult() != null) {
|
||||||
|
mLocation = task.getResult();
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to get location.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (SecurityException unlikely) {
|
||||||
|
Log.e(TAG, "Lost location permission." + unlikely);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNewLocation(Location location) {
|
||||||
|
Log.i(TAG, "New location: " + location);
|
||||||
|
|
||||||
|
mLocation = location;
|
||||||
|
|
||||||
|
// Notify anyone listening for broadcasts about the new location.
|
||||||
|
Intent intent = new Intent(ACTION_BROADCAST);
|
||||||
|
intent.putExtra(EXTRA_LOCATION, location);
|
||||||
|
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
|
||||||
|
|
||||||
|
// Update notification content if running as a foreground service.
|
||||||
|
if (serviceIsRunningInForeground(this)) {
|
||||||
|
mNotificationManager.notify(NOTIFICATION_ID, getNotification());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the location request parameters.
|
||||||
|
*/
|
||||||
|
private void createLocationRequest() {
|
||||||
|
mLocationRequest = new LocationRequest();
|
||||||
|
mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
|
||||||
|
mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
|
||||||
|
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used for the client Binder. Since this service runs in the same process as its
|
||||||
|
* clients, we don't need to deal with IPC.
|
||||||
|
*/
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
LocationUpdatesService getService() {
|
||||||
|
return LocationUpdatesService.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this is a foreground service.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context}.
|
||||||
|
*/
|
||||||
|
public boolean serviceIsRunningInForeground(Context context) {
|
||||||
|
ActivityManager manager = (ActivityManager) context.getSystemService(
|
||||||
|
Context.ACTIVITY_SERVICE);
|
||||||
|
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(
|
||||||
|
Integer.MAX_VALUE)) {
|
||||||
|
if (getClass().getName().equals(service.service.getClassName())) {
|
||||||
|
if (service.foreground) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,30 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.location.Location;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
@ -24,8 +39,39 @@ import com.google.firebase.messaging.FirebaseMessaging;
|
|||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterActivity {
|
||||||
|
|
||||||
|
private static final String TAG = MainActivity.class.getSimpleName();
|
||||||
|
|
||||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
||||||
|
|
||||||
|
// Used in checking for runtime permissions.
|
||||||
|
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
|
||||||
|
|
||||||
|
// The BroadcastReceiver used to listen from broadcasts from the service.
|
||||||
|
private MyReceiver myReceiver;
|
||||||
|
|
||||||
|
// A reference to the service used to get location updates.
|
||||||
|
private LocationUpdatesService mService = null;
|
||||||
|
|
||||||
|
// Tracks the bound state of the service.
|
||||||
|
private boolean mBound = false;
|
||||||
|
|
||||||
|
// Monitors the state of the connection to the service.
|
||||||
|
private final ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
LocationUpdatesService.LocalBinder binder = (LocationUpdatesService.LocalBinder) service;
|
||||||
|
mService = binder.getService();
|
||||||
|
mBound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
mService = null;
|
||||||
|
mBound = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||||
@ -53,6 +99,16 @@ public class MainActivity extends FlutterActivity {
|
|||||||
} else {
|
} else {
|
||||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
||||||
}
|
}
|
||||||
|
} else if (call.method.equals("startLocationService")) {
|
||||||
|
if (!checkPermissions()) {
|
||||||
|
requestPermissions();
|
||||||
|
} else {
|
||||||
|
mService.requestLocationUpdates();
|
||||||
|
}
|
||||||
|
result.success("");
|
||||||
|
} else if (call.method.equals("stopLocationService")) {
|
||||||
|
mService.removeLocationUpdates();
|
||||||
|
result.success("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,6 +122,148 @@ public class MainActivity extends FlutterActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
myReceiver = new MyReceiver();
|
||||||
|
// Check that the user hasn't revoked permissions by going to Settings.
|
||||||
|
if (Utils.requestingLocationUpdates(this)) {
|
||||||
|
if (!checkPermissions()) {
|
||||||
|
requestPermissions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
// Bind to the service. If the service is in foreground mode, this signals to the service
|
||||||
|
// that since this activity is in the foreground, the service can exit foreground mode.
|
||||||
|
bindService(new Intent(this, LocationUpdatesService.class), mServiceConnection,
|
||||||
|
Context.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
LocalBroadcastManager.getInstance(this).registerReceiver(myReceiver,
|
||||||
|
new IntentFilter(LocationUpdatesService.ACTION_BROADCAST));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(myReceiver);
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
if (mBound) {
|
||||||
|
// Unbind from the service. This signals to the service that this activity is no longer
|
||||||
|
// in the foreground, and the service can respond by promoting itself to a foreground
|
||||||
|
// service.
|
||||||
|
unbindService(mServiceConnection);
|
||||||
|
mBound = false;
|
||||||
|
}
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current state of the permissions needed.
|
||||||
|
*/
|
||||||
|
private boolean checkPermissions() {
|
||||||
|
return PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(this,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestPermissions() {
|
||||||
|
boolean shouldProvideRationale =
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||||
|
|
||||||
|
// Provide an additional rationale to the user. This would happen if the user denied the
|
||||||
|
// request previously, but didn't check the "Don't ask again" checkbox.
|
||||||
|
if (shouldProvideRationale) {
|
||||||
|
Log.i(TAG, "Displaying permission rationale to provide additional context.");
|
||||||
|
/*
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(R.id.activity_main),
|
||||||
|
R.string.permission_rationale,
|
||||||
|
Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setAction(R.string.ok, new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
// Request permission
|
||||||
|
ActivityCompat.requestPermissions(MainActivity.this,
|
||||||
|
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||||
|
REQUEST_PERMISSIONS_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
*/
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Requesting permission");
|
||||||
|
// Request permission. It's possible this can be auto answered if device policy
|
||||||
|
// sets the permission in a given state or the user denied the permission
|
||||||
|
// previously and checked "Never ask again".
|
||||||
|
ActivityCompat.requestPermissions(MainActivity.this,
|
||||||
|
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||||
|
REQUEST_PERMISSIONS_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback received when a permissions request has been completed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
|
@NonNull int[] grantResults) {
|
||||||
|
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
|
||||||
|
if (grantResults.length <= 0) {
|
||||||
|
// If user interaction was interrupted, the permission request is cancelled and you
|
||||||
|
// receive empty arrays.
|
||||||
|
Log.i(TAG, "User interaction was cancelled.");
|
||||||
|
} else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// Permission was granted.
|
||||||
|
mService.requestLocationUpdates();
|
||||||
|
} else {
|
||||||
|
// Permission denied.
|
||||||
|
/*
|
||||||
|
setButtonsState(false);
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(R.id.activity_main),
|
||||||
|
R.string.permission_denied_explanation,
|
||||||
|
Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setAction(R.string.settings, new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
// Build intent that displays the App settings screen.
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||||
|
Uri uri = Uri.fromParts("package",
|
||||||
|
BuildConfig.APPLICATION_ID, null);
|
||||||
|
intent.setData(uri);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receiver for broadcasts sent by {@link LocationUpdatesService}.
|
||||||
|
*/
|
||||||
|
private class MyReceiver extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
Location location = intent.getParcelableExtra(LocationUpdatesService.EXTRA_LOCATION);
|
||||||
|
if (location != null) {
|
||||||
|
Toast.makeText(MainActivity.this, Utils.getLocationText(location),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
|
||||||
|
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.foreground-location-service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if requesting location updates, otherwise returns false.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context}.
|
||||||
|
*/
|
||||||
|
static boolean requestingLocationUpdates(Context context) {
|
||||||
|
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_REQUESTING_LOCATION_UPDATES, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the location updates state in SharedPreferences.
|
||||||
|
* @param requestingLocationUpdates The location updates state.
|
||||||
|
*/
|
||||||
|
static void setRequestingLocationUpdates(Context context, boolean requestingLocationUpdates) {
|
||||||
|
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(KEY_REQUESTING_LOCATION_UPDATES, requestingLocationUpdates)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@code location} object as a human readable string.
|
||||||
|
* @param location The {@link Location}.
|
||||||
|
*/
|
||||||
|
static String getLocationText(Location location) {
|
||||||
|
return location == null ? "Unknown location" :
|
||||||
|
"(" + location.getLatitude() + ", " + location.getLongitude() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getLocationTitle(Context context) {
|
||||||
|
return "Location updated: " + DateFormat.getDateTimeInstance().format(new Date());
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,11 @@ class IntegrationSettingsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||||
|
|
||||||
|
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||||
|
|
||||||
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||||
bool _locationTrackingEnabled = false;
|
bool _locationTrackingEnabled = false;
|
||||||
|
bool _foregroundLocationTrackingEnabled = false;
|
||||||
bool _wait = false;
|
bool _wait = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,6 +31,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||||
|
_foregroundLocationTrackingEnabled = prefs.getBool("foreground-location-service") ?? false;
|
||||||
_locationInterval = prefs.getInt("location-interval") ??
|
_locationInterval = prefs.getInt("location-interval") ??
|
||||||
AppSettings().defaultLocationUpdateIntervalMinutes;
|
AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||||
if (_locationInterval % 5 != 0) {
|
if (_locationInterval % 5 != 0) {
|
||||||
@ -63,6 +67,17 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_switchForegroundLocationTrackingState(bool state) async {
|
||||||
|
if (state) {
|
||||||
|
await platform.invokeMethod('startLocationService');
|
||||||
|
} else {
|
||||||
|
await platform.invokeMethod('stopLocationService');
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_wait = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
@ -84,7 +99,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Enable device location tracking"),
|
Text("Enable location tracking"),
|
||||||
Switch(
|
Switch(
|
||||||
value: _locationTrackingEnabled,
|
value: _locationTrackingEnabled,
|
||||||
onChanged: _wait ? null : (value) {
|
onChanged: _wait ? null : (value) {
|
||||||
@ -97,7 +112,23 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Foreground tracking"),
|
||||||
|
Switch(
|
||||||
|
value: _foregroundLocationTrackingEnabled,
|
||||||
|
onChanged: _wait ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_foregroundLocationTrackingEnabled = value;
|
||||||
|
_wait = true;
|
||||||
|
});
|
||||||
|
_switchForegroundLocationTrackingState(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding),
|
||||||
Text("Location update interval in minutes:"),
|
Text("Location update interval in minutes:"),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
Reference in New Issue
Block a user