Android получает местоположение в виджете приложения - кажется, что locationManager перестает работать через некоторое время

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));

Решение

Ну, я застрял, я не думаю, что в данный момент я могу выполнить этот пункт, любая помощь может быть полезной.


person Gianmarco    schedule 18.11.2020    source источник


Ответы (1)


Похоже, вы нарушили ограничения на доступ к фоновому местоположению, добавленные в Android 10 и Android 11. Я думаю, что есть два возможных обходных пути:

  1. Вернитесь к реализации службы переднего плана и установите для тип службы значение location. Как указано здесь, служба переднего плана запускалась из appwidget не подлежит ограничениям во время использования.
  2. Получите доступ к фоновому местоположению, как описано здесь. Начиная с Android 11, чтобы получить этот доступ, вам нужно направить пользователей в настройки приложения, и они должны предоставить его вручную. Обратите внимание, что Google Play недавно ввела новую политику конфиденциальности, поэтому, если вы собираетесь опубликовать свое приложение в Google Play, вам придется доказать, что это абсолютно необходимо для вашего приложения и получить одобрение
person esentsov    schedule 22.11.2020
comment
Пункт 1 чрезвычайно интересен, в нормальном состоянии должен помочь, теперь проблема перешла в режим экономии заряда батареи на устройстве, который прекращает любое подключение к Интернету и GPS. Любые идеи? используя пункт 2, это недопустимый вариант, мне не нужно получать фоновую службу, любое уведомление переднего плана для меня нормально, дело в том, что даже при службе переднего плана некоторые функции отключены. - person Gianmarco; 23.11.2020