【发布时间】:2021-03-01 20:36:18
【问题描述】:
TL:DR;
短篇小说
主屏幕中的 App Widget 无法从使用
LocationManager::getLastKnownLocation的IntentService获取 GPS 位置,因为一段时间后,该 App 处于后台或失去焦点,返回的Location为空,例如没有已知的最后位置。
我尝试使用Service、WorkerManager、AlarmManager并请求WakeLock,但没有成功。
情况
我正在开发一个 Android 应用程序,它可以读取公共数据,并在经过几次计算后,以用户友好的方式将它们显示给用户。
该服务是公开的.json,其中包含有关我所在地区天气状况的数据。大多数情况下,它是一个包含一些(不超过 20 个)非常简单记录的数组。这些记录每 5 分钟更新一次。
包含在应用程序中,我添加了一个 App Widget。小部件的作用是向用户显示单个(计算的)值。这会从 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
}
目标
我要添加的是允许用户获取位置感知数据的 App Widget 类型。
理想的结果是,一旦添加到主屏幕上,App Widget 将监听用户交互(点击)并在点击时询问所需的数据。
这可以通过接收准备显示的计算值或接收位置和要比较的地理定位数据列表来完成,然后创建要显示的值。
实施过程及错误
按顺序,这是我尝试过的以及遇到的问题。
LocationManager.requestSingleUpdate 理念
我知道由于原始数据不经常更新而无需连续更新位置,我尝试的第一件事是直接在小部件的clickListener 中调用LocationManager.requestSingleUpdate。由于各种错误,我无法获得任何有效结果,所以,
浏览神圣的 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 中使用。
一切似乎都很好,我得到了一个位置,我能够用一个 Intent 将它发送回小部件,有一些我不喜欢的小东西,但我完全可以忍受。我说的是在电话中短暂显示的通知,告诉用户服务正在运行,没什么大不了的,如果它正在运行是为了用户输入,不是很花哨但可行。
然后我遇到了一个奇怪的问题,我点击了小部件,启动Toast告诉我服务确实启动了,但通知并没有消失。我等了一会儿,然后用我的手机“全部关闭”关闭了应用程序。
我再次尝试,小部件似乎正在工作。直到,服务再次卡住。所以我打开了我的应用程序,看看数据是否被处理,“tah dah”我立即得到了服务“解冻”的下一个 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));
解决方案
好吧,我被卡住了,我认为目前我无法完成这一段,任何帮助都可以使用。
【问题讨论】:
标签: android android-widget locationmanager android-gps android-doze