Android获取应用程序中的位置小工具-位置管理器似乎在一段时间后停止工作



TL:DR

短篇小说

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


情况

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

环境

  • Android Studio 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
}

目标

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

实施过程和错误

按顺序来说,这就是我所尝试的,也是我遇到的问题。

LocationManager.requestSingleUpdate理念

我知道由于原始数据的更新不频繁,我不需要连续更新位置,所以我尝试的第一件事是直接调用小部件的clickListenerLocationManager.requestSingleUpdate。由于各种错误,我无法得到任何有效的结果,因此,浏览Holy StackOverflow我明白这样做不是App Widget的本意
所以我切换到基于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.来访问某种参数,或者调用final我的Message参数以在Lambda中使用。

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

然后我遇到了一个奇怪的问题,我点击了小部件,一个启动Toast告诉我服务确实启动了,但随后通知并没有消失。我等着,然后用";关闭所有";我的手机
我再试了一次,小部件似乎在工作。直到,服务再次陷入困境。所以我打开了我的应用程序,看看数据是否被处理过;tah-dah";我立刻得到了下一份"祝酒词";解冻">
我得出的结论是,我的Service正在工作,但在某个时候,当应用程序失去焦点一段时间时(显然是在使用小部件时),服务冻结了。也许是安卓的Doze,或者是应用程序待机,我不确定。我读了更多,发现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");
}
}
}

其中我使用了能够使用CCD_ 30的线程;被称为死线";错误
不用说,这是有效的(或多或少,我不再实现接收端),没有显示任何通知,但我遇到了与以前相同的问题,唯一的问题是我知道问题不在Worker(或Service)本身,而是在locationManager中。过了一段时间,应用程序没有聚焦(因为我正在观看主屏幕,等待点击我的小部件)locationManager停止工作,挂起了我的Worker,这只由我的countDownLatch.await(5, SECONDS)保存。

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

LocationManager.getLastKnownLocation阶段:

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


编辑:我刚刚尝试使用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尝试了不同的服务位置,但和往常一样,没有任何变化。服务在一小段时间内返回正确的位置,然后冻结
这是代码:

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停止工作的解决方案。仍然没有成功,tho。

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和android 11中添加的访问后台位置的限制。我认为有两种可能的解决方案:

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

相关内容

最新更新