问题背景
最近有需求,在APP启动后,退到后台,还要能实现周期获取位置信息上报服务器,研究了一下实现方案。
问题分析
一、APP退到后台后网络请求实现
APP退到后台后,实现周期循环发送网络请求。目前尝试了两种方案是OK,如下:
(1)AlarmManager + 前台服务 +广播的方案,可以正常实现,大体思路是,启动一个前台服务,使用AlarmManager发起一个定时广播,然后广播接收器接收到广播后,循环去执行service的操作。
(2)使用jetpeck库提供的worker实现,基于PeriodicWorkRequest实现一个周期执行的任务,比如周期设置为15分钟,可以在后台稳定执行。
二、APP退到后台后获取地理位置实现
APP申请位置时,用户选择了列表中的始终允许后,APP在后台是可以正常获取到位置信息的。不过这里有个坑,因为安卓11版本后退位置信息管控策略进行了更新,如果APP的target sdk版本大于29时,需要分别申请前台位置权限和后台位置权限,本APPtargetsdk是小于等于29的,可以同时申请前台位置权限和后台位置权限。(compileSdkVersion小于29时,会没有这个后台位置权限,需要最好升级到29)
问题解决
下面展示大概的代码,可以参考实现。
(1)引入依赖
api('androidx.work:work-runtime:2.0.1')
(2)manifest文件中增加申请权限
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 这个权限用于访问GPS定位 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
(3)权限申请(同时申请位置权限和后台位置权限)
RxPermissionHelper helper = new RxPermissionHelper(this);
helper.requestEach(new RxPermissionHelper.PermissionCallback() {
@Override
public void granted(String permissionName) {
LogUtil.writerLog("ACCESS_FINE_LOCATION granted");
}
@Override
public void denied(String permissionName, boolean forever) {
}
@Override
public void result(boolean allGranted) {
}
}, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION);
(4)work类:去执行前台服务
/**
* work类执行定时任务
*/
public class SimpleWorker extends Worker {
private CurPosUtil curPosUtil;
public SimpleWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
Log.d("baorant", "执行调度任务");
LogUtil.writerLog("执行调度任务");
startService();
return Result.success();
}
private void startService() {
// curPosUtil = new CurPosUtil(getApplicationContext());
Intent intent = new Intent(getApplicationContext(), RecordService.class);
getApplicationContext().startService(intent);
}
}
(5)RecordService前台服务类(需要在manifest文件中配置)
/**
* 一个定时任务
*
* 方案:使用前台服务去执行网络请求,定时发送广播,然后在广播接收器中重新启动服务,实现循环后台服务。
*/
public class RecordService extends Service {
private CurPosUtil curPosUtil;
/**
* 每30秒更新一次数据
*/
private static final int ONE_Miniute= 30 * 1000;
private static final int PENDING_REQUEST=0;
int count = 0;
public RecordService() {
}
@Override
public void onCreate() {
super.onCreate();
curPosUtil = new CurPosUtil(getApplicationContext());
LogUtil.writerLog("RecordService onCreate");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String NOTIFICATION_CHANNEL_ID = "package_name";
String channelName = "My Background Service";
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,channelName, NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
Notification notification = new Notification.Builder(this,NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_dial_icon) // the status icon
.setWhen(System.currentTimeMillis()) // the time stamp
.setContentText("定时服务正在运行") // the contents of the entry
.build();
startForeground(2, notification);
}
}
/**
* 调用Service都会执行到该方法
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtil.writerLog("RecordService: onStartCommand");
// 这里模拟后台操作
initPos();
return super.onStartCommand(intent, flags, startId);
}
private void initPos() {
curPosUtil = new CurPosUtil(this);
curPosUtil.getCurPos(new CurPosUtil.CurPosCallback() {
@Override
public void getCurPos(double s, double s1, String s2) {
LogUtil.writerLog(DateUtil.timeToDate(String.valueOf(System.currentTimeMillis())));
LogUtil.writerLog("getCurPos: " + s + " " + s1 + " " + s2);
commonLogin(s + " " + s1 + " " + s2);
}
});
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
public void commonLogin(String position) {
RetrofitHelper.getInstance().login(position, "", "", "",
"", "", "", "")
.subscribe(new Observer<Boolean>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(Boolean aBoolean) {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
}
}
(6)activity中启动周期任务,周期15分钟循环执行
PeriodicWorkRequest.Builder request =
new PeriodicWorkRequest.Builder(SimpleWorker.class, 15
, TimeUnit.MINUTES).addTag("simpleTask");
LogUtil.writerLog(DateUtil.timeToDate(String.valueOf(System.currentTimeMillis())));
LogUtil.writerLog("点击执行task");
WorkManager.getInstance().enqueue(request.build() );
(7)LogUtil工具类,输出日志到文件,方便定位
/**
* 日志工具,输出日志到文件
*/
public class LogUtil {
/**
* 路径 "/data/data/com包名/files/backLogTest"
*
* @param msg 需要打印的内容
*/
public static void writerLog(String msg) {
Log.d("baorant", msg);
// 保存到的文件路径
final String filePath = App.getContext().getFilesDir().getPath();
FileWriter fileWriter;
BufferedWriter bufferedWriter = null;
try {
// 创建文件夹
File dir = new File(filePath, "backLogTest");
if (!dir.exists()) {
dir.mkdir();
}
// 创建文件
File file = new File(dir, "lowTemperature.txt");
if (!file.exists()) {
file.createNewFile();
}
// 写入日志文件
fileWriter = new FileWriter(file, true);
bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write( msg + "=======时间 :"+ getCurrentTime()+ "\n");
bufferedWriter.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static String getCurrentTime() {
Calendar calendar = Calendar.getInstance();
@SuppressLint("SimpleDateFormat")
SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(calendar.getTime());
}
}
问题总结
运行结果如下:
如结果所示,基于该方案可以实现APP在后台,周期循环获取位置信息并进行上报,有兴趣的同学可以进一步深入研究。