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

#android #android-widget #locationmanager #android-gps #android-doze

#Android #android-виджет #locationmanager #android-gps #android-доза

Вопрос:

TL: DR;

Короткая история

Виджет приложения на главном экране не может получить местоположение GPS от IntentService того, который использует LocationManager::getLastKnownLocation , потому что через некоторое время, когда приложение работает в фоновом режиме или теряет фокус, Location возвращаемое значение равно нулю, например, последняя позиция неизвестна.
Я пытался использовать Service , WorkerManager , AlarmManager и запрашивать a WakeLock без успеха.


Ситуация

Я разрабатываю приложение для 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. Я думаю, что есть два возможных обходных пути:

  1. Вернитесь к реализации службы переднего плана и установите тип службы на location . Как указано здесь, служба переднего плана, запущенная из appwidget, не подпадает под ограничения «во время использования».
  2. Получите доступ к фоновому местоположению, как описано здесь . Начиная с Android 11, чтобы получить этот доступ, вам нужно направить пользователей в настройки приложения, и они должны предоставить его вручную. Обратите внимание, что Google Play недавно представил новую политику конфиденциальности, поэтому, если вы собираетесь опубликовать свое приложение в Google Play, вам нужно будет доказать, что это абсолютно необходимо для вашего приложения, и получить одобрение

Комментарии:

1. пункт 1 чрезвычайно интересен, в нормальном состоянии должен сработать, теперь проблема переключается в режим экономии времени на устройстве, который останавливает каждое подключение к Интернету и GPS. Есть идеи? используя пункт 2, это недопустимый вариант, мне не нужно получать фоновую службу, любое уведомление на переднем плане для меня нормально, дело в том, что даже с помощью службы переднего плана некоторые функции отключены.