Short Story
An App Widget, in the home screen, fails to get a GPS position from an
IntentService
that usesLocationManager::getLastKnownLocation
because after a while that the App is in the background or loses focus, theLocation
returned is null, such as there was no last position known.
I tried usingService
,WorkerManager
,AlarmManager
and requesting aWakeLock
with no success.
I am developing an Android application that reads public data and, after a few computations, shows them, formatted in a user-friendly way, to the user.
The service is a .json
publicly available that contains data about weather status in my local area. Mostly it is an array with some (no more than 20) very simple records. Those records are updated every 5 minutes.
Included with the App I added an App Widget. What the widget does is to show a single (computed) value to the user. That gets a once-in-a-while update from the Android System (as specified by android:updatePeriodMillis="1800000"
), and also listen to user interactions (tap) to send an update request.
The user can choose between a few widget types, each showing a different value, but all with the same tap-to-update behaviour.
Samsung Galaxy S10 SM-G973F
API level 30.gradle config file:
defaultConfig {
applicationId "it.myApp"
versionCode code
versionName "0.4.0"
minSdkVersion 26
targetSdkVersion 30
}
What I want to add is an App Widget type that allows the user to get Location-aware data.
The ideal result would be that, once added on the home screen, the App Widget will listen to user interaction (tap) and when tapped will ask the required data.
This can be either done receiving the ready-to-show computed value or receiving the Location and a list of Geolocated Data to be compared, then create the value to show.
This, in order, is what I tried and the problems I got.
LocationManager.requestSingleUpdate
ideaThe first thing I tried, knowing that I won't need a continuous update of the position because of the infrequent update of the raw data, was to call in the clickListener
of the widget directly the LocationManager.requestSingleUpdate
. I was unable to have any valid result with various errors so,
surfing the Holy StackOverflow I understood that doing like so was not what an App Widget was intended.
So I switched to an Intent
-based process.
The IntentService
:
I implemented an IntentService
, with all the startForegroundService
related problems.
After many struggles, I run the Application and the widget was calling the service. But my location was not sent back, nor was the custom GPS_POSITION_AVAILABLE
action and I could not understand why until a something flashed in my mind, the service was dying or dead when the callback was called.
So I understood that an IntentService
was not what I should have used. I then switched to a standard Service
based process.
The Service
attempt:
Not to mention the infinite problems in getting the service running, I came to this class:
public class LocService extends Service {
public static final String ACTION_GET_POSITION = "GET_POSITION";
public static final String ACTION_POSITION_AVAILABLE = "GPS_POSITION_AVAILABLE";
public static final String ACTUAL_POSITION = "ACTUAL_POSITION";
public static final String WIDGET_ID = "WIDGET_ID";
private Looper serviceLooper;
private static ServiceHandler serviceHandler;
public static void startActionGetPosition(Context context,
int widgetId) {
Intent intent = new Intent(context, LocService.class);
intent.setAction(ACTION_GET_POSITION);
intent.putExtra(WIDGET_ID, widgetId);
context.startForegroundService(intent);
}
// Handler that receives messages from the thread
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (LocService.this.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED && LocService.this.checkSelfPermission(
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(LocService.this, R.string.cannot_get_gps, Toast.LENGTH_SHORT)
.show();
} else {
LocationManager locationManager = (LocationManager) LocService.this.getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
final int widgetId = msg.arg2;
final int startId = msg.arg1;
locationManager.requestSingleUpdate(criteria, location -> {
Toast.makeText(LocService.this, "location", Toast.LENGTH_SHORT)
.show();
Intent broadcastIntent = new Intent(LocService.this, TideWidget.class);
broadcastIntent.setAction(ACTION_POSITION_AVAILABLE);
broadcastIntent.putExtra(ACTUAL_POSITION, location);
broadcastIntent.putExtra(WIDGET_ID, widgetId);
LocService.this.sendBroadcast(broadcastIntent);
stopSelf(startId);
}, null);
}
}
}
@Override
public void onCreate() {
HandlerThread thread = new HandlerThread("ServiceStartArguments");
thread.start();
if (Build.VERSION.SDK_INT >= 26) {
String CHANNEL_ID = "my_channel_01";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel human readable title",
NotificationManager.IMPORTANCE_NONE);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("")
.setContentText("")
.build();
startForeground(1, notification);
}
// Get the HandlerThread's Looper and use it for our Handler
serviceLooper = thread.getLooper();
serviceHandler = new ServiceHandler(serviceLooper);
}
@Override
public int onStartCommand(Intent intent,
int flags,
int startId) {
int appWidgetId = intent.getIntExtra(WIDGET_ID, -1);
Toast.makeText(this, "Waiting GPS", Toast.LENGTH_SHORT)
.show();
Message msg = serviceHandler.obtainMessage();
msg.arg1 = startId;
msg.arg2 = appWidgetId;
serviceHandler.sendMessage(msg);
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
Toast.makeText(this, "DONE", Toast.LENGTH_SHORT)
.show();
}
}
In which I had to use some workarounds like LocService.this.
to access some kind of params or calling final my Message
params to be used inside the Lambda.
Everything seems fine, I was getting a Location, I was able to send it back to the widget with an Intent, there was a little thing that I didn't like but I could very well have lived with that. I am talking about the notification that showed briefly in the phone telling the user that the service was running, not a big deal, if it was running was for a user input, not quite fancy to see but viable.
Then I came to a weird problem, I tapped the widget, a start Toast
told me that the service was indeed started but then the notification didn't go away. I waited, then closed the app with "close all" of my phone.
I tried again and the widget seemed to be working. Until, the service got stuck again. So I opened my application to see if the data was processed and "tah dah" I immediately got the next Toast of the service "unfreezing".
I came to the conclusion that my Service
was working, but ad some point, while the app was out of focus for a while (obviously when using the widget), the service froze. Maybe for Android's Doze or for an App Standby, I was not sure. I read some more and I found out that maybe a Worker
and WorkerManager
could bypass Android background services limitations.
The Worker
way:
So I went for another change and implemented a Worker
and this is what I got:
public class LocationWorker extends Worker {
String LOG_TAG = "LocationWorker";
public static final String ACTION_GET_POSITION = "GET_POSITION";
public static final String ACTION_POSITION_AVAILABLE = "GPS_POSITION_AVAILABLE";
public static final String ACTUAL_POSITION = "ACTUAL_POSITION";
public static final String WIDGET_ID = "WIDGET_ID";
private Context context;
private MyHandlerThread mHandlerThread;
public LocationWorker(@NonNull Context context,
@NonNull WorkerParameters workerParams) {
super(context, workerParams);
this.context = context;
}
@NonNull
@Override
public Result doWork() {
Log.e(LOG_TAG, "doWork");
CountDownLatch countDownLatch = new CountDownLatch(2);
mHandlerThread = new MyHandlerThread("MY_THREAD");
mHandlerThread.start();
Runnable runnable = new Runnable() {
@Override
public void run() {
if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED && context.checkSelfPermission(
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
Log.e("WORKER", "NO_GPS");
} else {
countDownLatch.countDown();
LocationManager locationManager = (LocationManager) context.getSystemService(
Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
locationManager.requestSingleUpdate(criteria, new LocationListener() {
@Override
public void onLocationChanged(@NonNull Location location) {
Log.e("WORKER", location.toString());
Intent broadcastIntent = new Intent(context, TideWidget.class);
broadcastIntent.setAction(ACTION_POSITION_AVAILABLE);
broadcastIntent.putExtra(ACTUAL_POSITION, location);
broadcastIntent.putExtra(WIDGET_ID, 1);
context.sendBroadcast(broadcastIntent);
}
}, mHandlerThread.getLooper());
}
}
};
mHandlerThread.post(runnable);
try {
if (countDownLatch.await(5, TimeUnit.SECONDS)) {
return Result.success();
} else {
Log.e("FAIL", "" + countDownLatch.getCount());
return Result.failure();
}
} catch (InterruptedException e) {
e.printStackTrace();
return Result.failure();
}
}
class MyHandlerThread extends HandlerThread {
Handler mHandler;
MyHandlerThread(String name) {
super(name);
}
@Override
protected void onLooperPrepared() {
Looper looper = getLooper();
if (looper != null) mHandler = new Handler(looper);
}
void post(Runnable runnable) {
if (mHandler != null) mHandler.post(runnable);
}
}
class MyLocationListener implements LocationListener {
@Override
public void onLocationChanged(final Location loc) {
Log.d(LOG_TAG, "Location changed: " + loc.getLatitude() + "," + loc.getLongitude());
}
@Override
public void onStatusChanged(String provider,
int status,
Bundle extras) {
Log.d(LOG_TAG, "onStatusChanged");
}
@Override
public void onProviderDisabled(String provider) {
Log.d(LOG_TAG, "onProviderDisabled");
}
@Override
public void onProviderEnabled(String provider) {
Log.d(LOG_TAG, "onProviderEnabled");
}
}
}
In which I used a thread to be able to use the LocationManager
otherwise I was having a "called on dead thread" error.
Needless to say that this was working (more ore less, I was not implementing the receiving side anymore), no notification was shown, but I was having the same problem as before, the only thing was that I understood that the problem was not in the Worker
(or Service
) itself but with the locationManager
. After a while that the app was not focused (as I was watching the home screen waiting to tap my widget) locationManager
stopped working, hanging my Worker, that was saved only by my countDownLatch.await(5, SECONDS)
.
Well, ok, maybe I could not get a live location while the app was out of focus, strange, but I can accept it. I could use:
LocationManager.getLastKnownLocation
phase:So I switched back to my original IntentService
that now was running synchrously so there was no problem in handling callbacks, and I was able to use the Intent
pattern, that I like.
The fact is that, once implemented the receiver side, I figured out that even the LocationManager.getLastKnownLocation
stopped working after a while the app was out of focus. I thought it was impossible because I was not asking for a live location, so if few seconds ago my phone was able to return a lastKnownLocation
it should be able to do so now. The concern should only be in how "old" my location is, not if I am getting a location.
EDIT: I just tried with AlarmManager
that somewhere I read it can interact with the Doze and App Standby. Unfortunately neither that did the trick. This is a piece of code of what I used:
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getService(context, 1, intent, PendingIntent.FLAG_NO_CREATE);
if (pendingIntent != null && alarmManager != null) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 500, pendingIntent);
}
EDIT2: I tried a different service location using googleApi, but, as usual, nothing changed. The service returns the correct position for a small period of time, then it freezes.
This the code:
final int startId = msg.arg1;
FusedLocationProviderClient mFusedLocationClient = LocationServices.getFusedLocationProviderClient(LocService.this);
mFusedLocationClient.getLastLocation().addOnSuccessListener(location -> {
if (location != null) {
Toast.makeText(LocService.this, location.toString(), Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(LocService.this, "NULL", Toast.LENGTH_SHORT)
.show();
}
stopSelf(startId);
}).addOnCompleteListener(task -> {
Toast.makeText(LocService.this, "COMPLETE", Toast.LENGTH_SHORT)
.show();
stopSelf(startId);
});
EDIT3:
Obviously I can't wait updating StackOverflow, so I took a detour in a new direction, to try something different. The new try is about PowerManager
, acquiring a WakeLock
. In my mind that could have been a solution to avoid LocationManager
stop working. No success still, tho.
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
PowerManager.WakeLock mWakeLock = null;
if (powerManager != null)
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TRY:");
if (mWakeLock != null)
mWakeLock.acquire(TimeUnit.HOURS.toMillis(500));
Well, I am stuck, I don't think at the moment I can fulfil this paragraph, any help can be of use.
It looks like you are hitting the restrictions on accessing background location added in android 10 and android 11. I think there are two possible workarounds:
location
. As stated here a foreground service started from appwidget is not a subject to "while-in-use" restrictions.