【问题标题】:Android get Location in app widget - locationManager seems to stop working after a whileAndroid 在应用小部件中获取位置 - locationManager 似乎在一段时间后停止工作
【发布时间】:2021-03-01 20:36:18
【问题描述】:

TL:DR;

短篇小说

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


情况

我正在开发一个 Android 应用程序,它可以读取公共数据,并在经过几次计算后,以用户友好的方式将它们显示给用户。
该服务是公开的.json,其中包含有关我所在地区天气状况的数据。大多数情况下,它是一个包含一些(不超过 20 个)非常简单记录的数组。这些记录每 5 分钟更新一次
包含在应用程序中,我添加了一个 App Widget。小部件的作用是向用户显示单个(计算的)值。这会从 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
}

目标

我要添加的是允许用户获取位置感知数据的 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,我不确定。我阅读了更多内容,发现 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) 保存。

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

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


    【解决方案1】:

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

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

    【讨论】:

    • 第 1 点非常有趣,在正常情况下应该可以解决问题,现在问题转移到设备上的省电模式,即停止所有互联网和 GPS 连接。有任何想法吗?使用第 2 点这不是一个有效的选项,我不需要获得后台服务,任何前台通知对我来说都可以,问题是即使使用前台服务,某些功能也会被禁用。
    猜你喜欢
    • 1970-01-01
    • 2014-11-15
    • 2022-11-21
    • 2010-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多