最近做了一个项目,需要一直运行在前台,系统虽然是定制的,但厂商不配合,不去修改系统,使app一直运行在前台
所以保活手段只能 app 端实现
某些保活手段酌情使用,因为这个项目运行在某台长期运行的设备上,而不是手机,所以不考虑耗电等问题
随着Android 版本更新,很多黑科技保活手段都不再有效
同时为了兼容性,防止在某些设备上无法使用,所以目前只使用常规手段保活。
线程轮询
使用线程轮询运行状态,如果app退出,立刻启动app到前台
监听 Activity 的启动
自定义 Application 并注册 activity 启动监听
Application需要实现 Application.ActivityLifecycleCallbacks
定义 private int appCount = 0;
在下面两个方法中添加如下
@Override
public void onActivityStarted(Activity activity) {
appCount++;
}
@Override
public void onActivityStopped(Activity activity) {
appCount--;
}
Application 添加方法
public boolean isForground() {
return appCount > 0;
}
注册 activity 生命周期回调
registerActivityLifecycleCallbacks(this);
启动线程,然后编写 死循环
注意死循环需要适当的暂停,例如 一秒执行一次,或者五秒执行一次
在线程中,如果检测到 app 没有运行在前台,则立刻启动 MainActivity
if (!((MApplication) mContext.getApplicationContext()).isForground()) {
Intent newIntent = new Intent(MApplication.getContent(), MainActivity.class);
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MApplication.getContent().startActivity(newIntent);
}
桌面启动器
由于app需要一直运行在前台,所以可以将app 直接作为桌面启动器
修改 maniface 文件
直接添加下面三项即可
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MONKEY" />
修改后:
<activity
android:name=".activity.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MONKEY" />
</intent-filter>
</activity>
加上以后,在用户按下 home 键时,要求选择 桌面启动器,点选 本 app 并勾选
始终
注意:需要留出接口,跳转到系统设置,否则无法取消桌面启动器,则无法再打开其它应用
系统服务保活
系统服务包括 辅助服务,通知栏监听服务
服务需要引导用户手动开启
定义服务,继承自 通知栏监听服务
public class NotifyServer extends NotificationListenerService {
@Override
public void onCreate() {
super.onCreate();
}
}
定义服务,继承自 辅助服务
public class HelpServer extends AccessibilityService {
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
@Override
public void onInterrupt() {
}
}
在 Manifest 的 application
中,添加这两个服务
<service
android:name=".NotifyServer"
android:exported="true"
android:label="@string/service_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".HelpServer"
android:label="@string/service_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
其中, accessibility_service_config
文件内容如下:
accessibility_service_config
文件存放在 res/xml
目录下
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/help_info"
android:notificationTimeout="100" />
help_info 文本是辅助服务的提示信息
自此,两个服务已经创建好了,
如果用户授予 app 通知栏监听权限,或者辅助服务权限,对应的服务就会被启动
在里面编写保活代码即可,也可以和前面代码一样,启动线程,每隔数秒,监听 app 是否运行于前台
跳转到授权服务的代码:
// 跳转到 通知栏监听 权限授权界面
private void startNotifySetting(Context context) {
try {
Intent intent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
} else {
intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
Toast.makeText(this, getString(R.string.permission_tip), Toast.LENGTH_LONG).show();
} catch (ActivityNotFoundException e) {//普通情况下找不到的时候需要再特殊处理找一次
try {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.Settings$NotificationAccessSettingsActivity");
intent.setComponent(cn);
intent.putExtra(":settings:show_fragment", "NotificationAccessSettings");
context.startActivity(intent);
Toast.makeText(this, getString(R.string.permission_tip), Toast.LENGTH_LONG).show();
} catch (Exception ignore) {
Toast.makeText(this, getString(R.string.open_activity_faild), Toast.LENGTH_LONG).show();
}
e.printStackTrace();
}
}
// 跳转到 辅助服务 授权界面
private void startHelpSetting() {
try {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Toast.makeText(this, getString(R.string.permission_tip), Toast.LENGTH_LONG).show();
} catch (ActivityNotFoundException ignore) {
Toast.makeText(this, getString(R.string.open_activity_faild), Toast.LENGTH_LONG).show();
}
}
这两个方法在合适的提示后,调用即可
双 app 广播保活
使用两个app,每隔数秒发送广播,相互保活
这两个app 也可以使用上述手段(线程轮询、辅助服务)保活自身
每隔一段时间发送广播
try {
Intent intent = new Intent(Constant.HEART_BEAT_BROAD_CAST);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
ComponentName componentName = new ComponentName(pkg, cls);
intent.setComponent(componentName);
sendBroadcast(intent);
} catch (Exception e) {
e.printStackTrace();
}
在Android 8.0 上,需要使用
ComponentName
指定广播包名,否则无法接收到广播
监听 home 键退出 app,随后重启
在 Activity 中动态注册广播
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
registerReceiver(forgroundReceive, intentFilter);
该广播需要动态注册
Intent.ACTION_CLOSE_SYSTEM_DIALOGS
广播在按下 home
键和 菜单键 时,都能接收到
在广播接收者中
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
Intent newIntent = new Intent(context, MainActivity.class);
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(newIntent);
}
}
这样用户按下 home
和 menu
都会重启应用
注意,如果用户在: 设置->应用管理-> <app> -> 强制停止
会导致应用无法接收到任何广播,这是系统默认拦截行为
开机广播
manifest 注册广播
<receiver
android:name=".StartReceive"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<!-- 开机广播 -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
启动
public class StartReceive extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
LogUtil.i("StartReceive", "receive boot start broadcast ,intent:" + LogUtil.objToString(intent));
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())
|| Intent.ACTION_REBOOT.equals(intent.getAction())) {
Intent newIntent = new Intent(context, MainActivity.class);
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //注意,必须添加这个标记,否则启动会失败
//开机广播监听,启动本应用
context.startActivity(newIntent);
}
}
}
建议
在app所有回到前台的地方,建议都加上判断,如果在某些情况下,需要暂时退出 app,(例如升级app),则允许暂时退出。否则app无法退出的情况下,无法进入系统做任何操作
例如加上 MApplication.isCheckForground()
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction()) && MApplication.isCheckForground()) {
Intent newIntent = new Intent(context, MainActivity.class);
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(newIntent);
}
}
如果app需要手动点击升级,则 isCheckForground
返回值为 false
,允许暂时退出app