TL:DR;
Короткий рассказ
Виджет приложения на главном экране не может получить положение GPS от
IntentService
, использующегоLocationManager::getLastKnownLocation
, потому что через некоторое время, когда приложение находится в фоновом режиме или теряет фокус, возвращаемоеLocation
равно нулю, например, последняя позиция не была известна. .
Я безуспешно пытался использоватьService
,WorkerManager
,AlarmManager
и запрашиватьWakeLock
.
Ситуация
Я разрабатываю приложение для Android, которое считывает общедоступные данные и после нескольких вычислений показывает их пользователю в удобном для пользователя формате.
Сервис является .json
общедоступным и содержит данные о погоде в мой район. Чаще всего это массив с несколькими (не более 20) очень простыми записями. Эти записи обновляются каждые 5 минут.
Вместе с приложением я добавил виджет приложения. Что делает виджет, так это показывает пользователю одно (вычисляемое) значение. Это время от времени получает обновление от системы Android (как указано android:updatePeriodMillis="1800000"
), а также прослушивает действия пользователя (коснитесь), чтобы отправить запрос на обновление. .
Пользователь может выбирать между несколькими типами виджетов, каждый из которых показывает свое значение, но все с одинаковым поведением касания для обновления.
Окружающая обстановка
- Android-студия 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
}
Цель
Я хочу добавить тип виджета приложения, который позволяет пользователю получать данные о местоположении.
Идеальным результатом будет то, что после добавления на главный экран виджет приложения будет прислушивайтесь к действиям пользователя (коснитесь), и при нажатии будут запрашиваться необходимые данные.
Это можно сделать либо с получением готового к показу вычисленного значения, либо с получением местоположения и списка геолокационных данных для сравнения, а затем создать значение для отображения.
Процесс внедрения и ошибки
Это, по порядку, то, что я пробовал, и проблемы, которые я получил.
Идея LocationManager.requestSingleUpdate
Первое, что я попробовал, зная, что мне не потребуется постоянное обновление позиции из-за нечастого обновления необработанных данных, это вызвать clickListener
виджета непосредственно LocationManager.requestSingleUpdate
. Я не смог получить какой-либо действительный результат с различными ошибками, поэтому, просматривая Holy StackOverflow, я понял, что это не то, для чего предназначен виджет приложения.
Поэтому я переключился на процесс на основе Intent
.
IntentService
:
Я реализовал IntentService
со всеми проблемами, связанными с startForegroundService
.
После долгих мучений я запускаю приложение, и виджет вызывает службу. Но мое местоположение не было отправлено обратно, как и пользовательское действие GPS_POSITION_AVAILABLE
, и я не мог понять, почему, пока что-то не мелькнуло в моей голове, служба умирала или мертва, когда был вызван обратный вызов.
Так я понял, что IntentService
не то, что я должен был использовать. Затем я переключился на стандартный процесс, основанный на Service
.
Попытка Service
:
Не говоря уже о бесконечных проблемах с запуском службы, я пришел к этому классу:
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();
}
}
В котором мне пришлось использовать некоторые обходные пути, такие как LocService.this.
, чтобы получить доступ к некоторым параметрам или вызвать окончательные мои параметры Message
для использования внутри Lambda.
Кажется, все в порядке, я получал местоположение, я мог отправить его обратно в виджет с намерением, была небольшая вещь, которая мне не нравилась, но я вполне мог бы с этим жить. Я говорю об уведомлении, которое ненадолго отображалось на телефоне и сообщало пользователю, что служба запущена, не имеет большого значения, если она работала, то для пользовательского ввода, не совсем красивого, но жизнеспособного.
Затем я столкнулся со странной проблемой, я нажал на виджет, запуск Toast
сказал мне, что служба действительно запущена, но затем уведомление не исчезло. Я подождал, а затем закрыл приложение, закрыв весь телефон.
Я попробовал еще раз, и виджет вроде бы заработал. Пока служба снова не застряла. Итак, я открыл свое приложение, чтобы увидеть, были ли обработаны данные, и та-да-да, я сразу же получил следующий Тост о разморозке сервиса.
Я пришел к выводу, что мой Service
работает, но в какой-то момент, пока приложение было не в фокусе на некоторое время (очевидно при использовании виджета) сервис завис. Может быть, для Android Doze или для App Standby, я не был уверен. Я прочитал еще немного и обнаружил, что, возможно, Worker
и WorkerManager
могут обойти ограничения фоновых служб Android.
Способ Worker
:
Итак, я пошел на еще одно изменение и реализовал Worker
, и вот что я получил:
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");
}
}
}
В котором я использовал поток, чтобы иметь возможность использовать LocationManager
, иначе у меня была ошибка вызова мертвого потока.
Излишне говорить, что это работало (более или менее, я больше не реализовывал принимающую сторону), уведомление не показывалось, но у меня была та же проблема, что и раньше, единственное, что я понял, что проблема была не в самом Worker
(или Service
), а в locationManager
. Через некоторое время, когда приложение не было сфокусировано (поскольку я смотрел домашний экран, ожидая нажатия на свой виджет), locationManager
перестал работать, завис мой Worker, который спас только мой countDownLatch.await(5, SECONDS)
.
Ну ладно, может быть, я не мог получить живое местоположение, пока приложение было не в фокусе, странно, но я могу принять это. Я мог бы использовать:
Фаза LocationManager.getLastKnownLocation
:
Поэтому я переключился обратно на исходный IntentService
, который теперь работал синхронно, поэтому не было проблем с обработкой обратных вызовов, и я смог использовать шаблон Intent
, который мне нравится. Дело в том, что как только я реализовал приемную сторону, я понял, что даже LocationManager.getLastKnownLocation
перестал работать через некоторое время, когда приложение было не в фокусе. Я думал, что это невозможно, потому что я не запрашивал текущее местоположение, поэтому, если несколько секунд назад мой телефон смог вернуть lastKnownLocation
, он должен сделать это и сейчас. Беспокойство должно заключаться только в том, сколько лет моему местоположению, а не в том, если я его получаю.
РЕДАКТИРОВАТЬ: я только что попробовал AlarmManager
, где-то я читал, что он может взаимодействовать с режимом ожидания и режимом ожидания приложения. К сожалению, ни то, ни другое не помогло. Это фрагмент кода того, что я использовал:
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: я пробовал использовать другое расположение службы с помощью 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);
});
EDIT3: Очевидно, мне не терпится обновить 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));
Решение
Ну, я застрял, я не думаю, что в данный момент я могу выполнить этот пункт, любая помощь может быть полезной.