Android 在应用程序小部件中获取位置 - locationManager 似乎在一段时间后停止工作

Android get Location in app widget - locationManager seems to stop working after a while

TL:DR;

短篇小说

An App Widget, in the home screen, fails to get a GPS position from an IntentService that uses LocationManager::getLastKnownLocation because after a while that the App is in the background or loses focus, the Location returned is null, such as there was no last position known.
I tried using Service, WorkerManager, AlarmManager and requesting a WakeLock with no success.


情况

我正在开发一个 Android 应用程序,它读取 public 数据,并在经过一些计算后,以用户友好的方式向用户显示它们。
该服务仅 .json public 可用,其中包含有关我所在地区天气状况的数据。大多数情况下,它是一个包含一些(不超过 20 个)非常简单记录的数组。这些记录每 5 分钟更新一次
我在应用程序中添加了一个 App Widget。小部件所做的是向用户显示单个(计算的)值。它从 Android 系统(由 android:updatePeriodMillis="1800000" 指定)获取一次更新,并且 监听用户交互 (点击)发送更新请求。
用户可以在几种小部件类型之间进行选择,每种都显示不同的值,但都具有相同的点击更新行为。

环境

.gradle 配置文件:

defaultConfig {
    applicationId "it.myApp"
    versionCode code
    versionName "0.4.0"
    minSdkVersion 26
    targetSdkVersion 30
}

目标

我想添加的是一个 App Widget 类型,它允许用户获取 位置感知数据
理想的结果是,一旦添加到主屏幕上,App Widget 将监听用户交互(点击),点击时将询问所需的数据。
这可以通过接收准备显示的计算值或接收位置和要比较的地理定位数据列表来完成,然后创建要显示的值。

执行过程及错误

按顺序,这是我尝试过的方法和遇到的问题。

LocationManager.requestSingleUpdate 想法

我尝试的第一件事是直接在小部件的 clickListener 中调用 clickListener =27=]。我无法得到各种错误的任何有效结果,所以, 在 Holy Whosebug 上冲浪我明白这样做并不是 App Widget 的本意。
所以我切换到基于 Intent 的流程。


IntentService:

我实施了一个 IntentService,其中包含所有 startForegroundService 相关问题。
经过多次努力,我 运行 应用程序和小部件正在调用该服务。但是我的位置没有发回,自定义的 GPS_POSITION_AVAILABLE 操作也没有发回,我不明白为什么,直到我脑海中闪过一个东西,调用回调时服务正在死亡或死亡。
所以我明白 IntentService 不是我应该使用的。然后我切换到基于标准 Service 的流程。


Service尝试:
更不用说获取服务的无限问题运行ning,我来到这个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();
    }
}

其中我不得不使用一些变通方法,例如 LocService.this. 来访问某种参数或调用最终我的 Message 参数以在 Lambda 中使用。

一切似乎都很好,我得到了一个位置,我能够用一个 Intent 将它发送回小部件,有一点我不喜欢,但我完全可以接受。我说的是在 phone 中短暂显示的通知,告诉用户该服务是 运行ning,这没什么大不了的,如果它是 运行ning 是为了用户输入,不太好看,但可行。

然后我遇到了一个奇怪的问题,我点击了小部件,一个开始 Toast 告诉我服务确实已经启动但是通知并没有消失。我等待,然后用 phone.
的“全部关闭”关闭应用程序 我再次尝试,小部件似乎可以正常工作。直到,服务又卡住了。所以我打开我的应用程序,看看数据是否被处理,“哒哒”我立即得到服务的下一个吐司“解冻”。
我得出的结论是我的 Service 正在运行,但在某些时候,当应用程序失去焦点一段时间(显然是在使用小部件时)时,服务冻结了。也许是 Android 的 Doze 或 App Standby,我不确定。我阅读了更多内容,发现 WorkerWorkerManager 可以绕过 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).[=62 保存=]

嗯,好吧,也许我在应用程序失焦时无法获取实时位置,很奇怪,但我可以接受。我可以使用:

LocationManager.getLastKnownLocation阶段:

所以我切换回原来的 IntentService,现在是 运行ning 同步,所以处理回调没有问题,而且我能够使用 Intent 模式,我喜欢。 事实是,一旦实现了接收端,我发现即使 LocationManager.getLastKnownLocation 在应用程序失去焦点一段时间后也停止工作。我认为这是不可能的,因为我没有要求实时位置,所以如果几秒钟前我的 phone 能够 return 一个 lastKnownLocation 它现在应该可以这样做了。应该只关心我的位置有多“旧”,而不是 if 我得到一个位置。


编辑: 我刚刚尝试 AlarmManager 我读到的某处可以与 Doze 和 App Standby 交互。不幸的是,这都没有奏效。这是我使用的一段代码:

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 尝试了不同的服务位置,但和往常一样,没有任何改变。服务 return 在一小段时间内保持正确的位置,然后冻结。
这个代码:

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: 显然我等不及更新 Whosebug,所以我绕了个新方向,尝试一些不同的东西。新的尝试是关于 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));

解决方案

好吧,我卡住了,我认为目前我无法完成这一段,任何帮助都是有用的。

您似乎遇到了 android 10 and android 11 中添加的访问后台位置的限制。我认为有两种可能的解决方法:

  1. 返回前台服务实现并设置service type to location. As stated here从 appwidget 启动的前台服务不受“使用中”限制。
  2. 按照说明获取后台位置访问权限here. Starting from android 11 to get this access you need to direct users to app settings and they should grant it manually. Please note Google Play introduced a new privacy policy recently, so if you are going to publish your app on Google Play, you'll have to prove it's absolutely necessary for your app and get an approval