#android #android-widget #locationmanager #android-gps #android-doze
#Android #android-виджет #locationmanager #android-gps #android-доза
Вопрос:
TL: DR;
Короткая история
Виджет приложения на главном экране не может получить местоположение GPS от
IntentService
того, который используетLocationManager::getLastKnownLocation
, потому что через некоторое время, когда приложение работает в фоновом режиме или теряет фокус,Location
возвращаемое значение равно нулю, например, последняя позиция неизвестна.
Я пытался использоватьService
,WorkerManager
,AlarmManager
и запрашивать aWakeLock
без успеха.
Ситуация
Я разрабатываю приложение для Android, которое считывает общедоступные данные и после нескольких вычислений показывает их пользователю в удобном для пользователя формате.
Служба является .json
общедоступной и содержит данные о состоянии погоды в моем регионе. В основном это массив с некоторыми (не более 20) очень простыми записями. Эти записи обновляются каждые 5 минут.
В комплекте с приложением я добавил виджет приложения. Виджет показывает пользователю одно (вычисленное) значение. Это время от времени обновляет систему Android (как указано android:updatePeriodMillis="1800000"
), а также прослушивает взаимодействия с пользователем (нажмите), чтобы отправить запрос на обновление.
Пользователь может выбирать между несколькими типами виджетов, каждый из которых показывает разное значение, но все с одинаковым поведением при нажатии на обновление.
Окружающая среда
- Android Studio 4.1.1
- Использование JAVA
- Тестирование на
Samsung Galaxy S10 SM-G973F
уровне API физического устройства 30 - Эмулятор недоступен (я не могу его запустить)
.файл конфигурации gradle:
defaultConfig {
applicationId "it.myApp"
versionCode code
versionName "0.4.0"
minSdkVersion 26
targetSdkVersion 30
}
Цель
Я хочу добавить тип виджета приложения, который позволяет пользователю получать данные с учетом местоположения.
Идеальным результатом было бы то, что после добавления на главный экран виджет приложения будет прослушивать взаимодействие с пользователем (нажатие) и при нажатии запрашивать необходимые данные.
Это можно либо выполнить, получив готовое к показу вычисленное значение, либо получить местоположение и список геолокационных данных для сравнения, а затем создать значение для отображения.
Процесс реализации и ошибки
Это, по порядку, то, что я пробовал, и проблемы, с которыми я столкнулся.
The LocationManager.requestSingleUpdate
idea
The 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 amp;amp; 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 amp;amp; 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:
The LocationManager.getLastKnownLocation
phase:
Поэтому я вернулся к своему оригиналу IntentService
, который теперь работал синхронно, поэтому проблем с обработкой обратных вызовов не было, и я смог использовать Intent
шаблон, который мне нравится. Дело в том, что после реализации стороны получателя я понял, что даже LocationManager.getLastKnownLocation
перестал работать через некоторое время, приложение было не в фокусе. Я думал, что это невозможно, потому что я не запрашивал живое местоположение, поэтому, если несколько секунд назад мой телефон смог вернуть a lastKnownLocation
, он должен быть в состоянии сделать это сейчас. Проблема должна заключаться только в том, насколько «старым» является мое местоположение, а не в том, получаю ли я местоположение.
РЕДАКТИРОВАТЬ: я только что попробовал с AlarmManager
этим, где-то я читал, что он может взаимодействовать с режимом ожидания и ожидания приложения. К сожалению, ни то, ни другое не помогло. Это фрагмент кода, который я использовал:
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getService(context, 1, intent, PendingIntent.FLAG_NO_CREATE);
if (pendingIntent != null amp;amp; alarmManager != null) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() 500, pendingIntent);
}
EDIT2: я попробовал другое местоположение службы, используя GoogleAPI, но, как обычно, ничего не изменилось. Служба возвращает правильную позицию в течение небольшого периода времени, затем она зависает.
Это код:
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);
});
ПРАВКА3:
Очевидно, я не могу дождаться обновления StackOverflow, поэтому я пошел в новом направлении, чтобы попробовать что-то другое. Новая попытка заключается в PowerManager
приобретении WakeLock
. На мой взгляд, это могло бы быть решением, позволяющим избежать LocationManager
остановки работы. Все еще безуспешно.
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));
Решение
Ну, я застрял, я не думаю, что на данный момент я могу выполнить этот пункт, любая помощь может быть полезной.
Ответ №1:
Похоже, вы нарушаете ограничения на доступ к фоновому местоположению, добавленные в Android 10 и Android 11. Я думаю, что есть два возможных обходных пути:
- Вернитесь к реализации службы переднего плана и установите тип службы на
location
. Как указано здесь, служба переднего плана, запущенная из appwidget, не подпадает под ограничения «во время использования». - Получите доступ к фоновому местоположению, как описано здесь . Начиная с Android 11, чтобы получить этот доступ, вам нужно направить пользователей в настройки приложения, и они должны предоставить его вручную. Обратите внимание, что Google Play недавно представил новую политику конфиденциальности, поэтому, если вы собираетесь опубликовать свое приложение в Google Play, вам нужно будет доказать, что это абсолютно необходимо для вашего приложения, и получить одобрение
Комментарии:
1. пункт 1 чрезвычайно интересен, в нормальном состоянии должен сработать, теперь проблема переключается в режим экономии времени на устройстве, который останавливает каждое подключение к Интернету и GPS. Есть идеи? используя пункт 2, это недопустимый вариант, мне не нужно получать фоновую службу, любое уведомление на переднем плане для меня нормально, дело в том, что даже с помощью службы переднего плана некоторые функции отключены.