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

Gia*_*rco 4 android android-widget locationmanager android-gps android-doze

TL:博士;

短篇故事

IntentService主屏幕中的应用程序小部件无法从使用的应用程序获取 GPS 位置LocationManager::getLastKnownLocation,因为一段时间后该应用程序处于后台或失去焦点,Location返回的是 null,例如没有已知的最后位置。
我尝试使用ServiceWorkerManagerAlarmManager并请求 aWakeLock但没有成功。


情况

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

环境

  • 安卓工作室4.1.1
  • 使用JAVA
  • 在物理设备Samsung Galaxy S10 SM-G973FAPI 级别 30上进行测试
  • 没有可用的模拟器(我无法启动它)

.gradle 配置文件:

defaultConfig {
    applicationId "it.myApp"
    versionCode code
    versionName "0.4.0"
    minSdkVersion 26
    targetSdkVersion 30
}
Run Code Online (Sandbox Code Playgroud)

目标

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

实施过程及错误

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

这个LocationManager.requestSingleUpdate想法

我尝试的第一件事是直接调用小部件clickListenerLocationManager.requestSingleUpdate. 由于各种错误,我无法获得任何有效的结果,因此,在神圣的 StackOverflow 上冲浪时,我了解到这样做并不是应用程序小部件的目的。
所以我切换到Intent基于 - 的流程。


IntentService

我实现了一个IntentService,并解决了所有startForegroundService相关问题。
经过多次努力,我运行了应用程序,小部件正在调用服务。但我的位置没有发回,自定义GPS_POSITION_AVAILABLE操作也没有发回,我无法理解为什么,直到我脑海中闪现出一些东西,当回调被调用时,服务正在死亡或死亡。
所以我明白 anIntentService不是我应该使用的。然后我切换到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();
    }
}
Run Code Online (Sandbox Code Playgroud)

其中我必须使用一些解决方法,例如LocService.this.访问某种参数或调用最终我的Message参数以在 Lambda 内部使用。

一切看起来都很好,我得到了一个位置,我能够将其发送回具有意图的小部件,有一点我不喜欢,但我很可能接受它。我说的是手机中短暂显示的通知,告诉用户服务正在运行,这没什么大不了的,如果它正在运行是为了用户输入,虽然不太好看但可行。

然后我遇到了一个奇怪的问题,我点击了小部件,启动Toast告诉我服务确实已启动,但通知并没有消失。我等待着,然后用“关闭所有”手机关闭了应用程序。
我再次尝试,该小部件似乎可以正常工作。直到,服务再次陷入困境。所以我打开我的应用程序,看看数据是否已处理,然后“tah dah”我立即得到了服务的下一个Toast“unfreezing”。
我得出的结论是我Service正在工作,但在某些时候,当应用程序失去焦点一段时间(显然是在使用小部件时),服务冻结了。也许是为了 Android 的打瞌睡或应用程序待机,我不确定。我阅读了更多内容,发现也许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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

其中我使用了一个线程来使用否则LocationManager我会遇到“调用死线程”错误。
不用说,这是有效的(更多矿石更少,我不再实施接收方),没有显示任何通知,但我遇到了与以前相同的问题,唯一的是我明白问题不在于(WorkerService) 本身但与locationManager. 一段时间后,应用程序没有聚焦(因为我正在观看主屏幕等待点击我的小部件)locationManager停止工作,挂起我的 Worker,这仅由我的countDownLatch.await(5, SECONDS).

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

阶段LocationManager.getLastKnownLocation

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


编辑:我刚刚尝试AlarmManager在某处读到它可以与打瞌睡和应用程序待机交互。不幸的是,这都没有达到目的。这是我使用的一段代码:

defaultConfig {
    applicationId "it.myApp"
    versionCode code
    versionName "0.4.0"
    minSdkVersion 26
    targetSdkVersion 30
}
Run Code Online (Sandbox Code Playgroud)

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);
});
Run Code Online (Sandbox Code Playgroud)

EDIT3: 显然我迫不及待地更新StackOverflow,所以我绕了一个新的方向,尝试一些不同的东西。新的尝试是关于PowerManager获得一个WakeLock. 在我看来,这可能是避免LocationManager停止工作的解决方案。仍然没有成功,不过。

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案

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

ese*_*sov 5

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

  1. 回到前台服务实现,将服务类型设置为location如此处所述,从 appwidget 启动的前台服务不受“使用时”限制。
  2. 按照此处所述获取后台位置访问权限。从 Android 11 开始,要获得此访问权限,您需要将用户引导至应用程序设置,并且他们应该手动授予该权限。请注意,Google Play 最近推出了新的隐私政策,因此,如果您要在 Google Play 上发布您的应用,您必须证明它对您的应用绝对必要并获得批准