九、服务、启动器和组件
长期运行的服务
在 Android 中,有几种方法可以在当前活动结束后一次性或周期性地运行“作业”。这里讨论的技术将在以下两种类型之间变化:
-
将强绑定到当前活动,比如
AsyncTask
,意思是如果活动结束,那么任务被垃圾回收。 -
没有将强绑定到当前活动,例如
JobScheduler
,其中任务甚至在父活动本身被垃圾收集后仍继续。
扳机
触发器是长期运行的服务如何分配任务和运行的机制,它们是服务的初始入口点。大多数触发器将为服务提供某种形式的持久性。这些触发可以在表 9-1 中看到。
表 9-1
长期运行的服务触发器
|
引发
|
描述
|
| --- | --- |
| 目的 | 从代码中直接触发,通常来自用户交互。 |
| 手机闹钟服务 | 在未来的特定时间触发,一次性或重复发生。 |
| 广播接收器 | 收到特定广播消息时触发。例如,BootComplete
、PowerConnected
或自定义接收器。 |
| 传感器回调 | 收到特定传感器值时触发。 |
| 工作管理器 | 根据 API 等级使用JobScheduler
或AlarmManager
和BootComplete
。 |
| 作业调度程序 | 从 API level 21 (Android L)开始,AlarmManager
的更智能实现允许根据网络、空闲和充电状态运行。也瞌睡顺从。 |
服务
有几种类型的“服务”可以运行,为应用提供长期运行的后台工作。这些可以在表 9-2 中看到。
表 9-2
长期运行的服务类型
|
服务
|
描述
|
| --- | --- |
| 工作管理器 | 封装启动器和服务元素(考虑到底层实现被抽象时的向后兼容性)。并发工作之间的最小间隔为 15 分钟,每个工人最多只能运行 10 分钟。WorkManagers
重启后也会自动保持(如果许可的话,使用BootComplete
广播接收器)。 |
| 作业调度程序 | 封装了启动器和服务元素。这些都是高度可定制的,可以根据网络、空闲和电池状态等环境因素运行工作。除此之外,它们还可以被定义为以特定的时间间隔或特定的时间段运行,并在重启后持续运行(如果许可可用,使用BootComplete
广播接收器)。工作之间的最小间隔是 15 分钟。 |
| 服务 | 有许多类型的服务;然而,最常见的一种是意向服务,其中工作请求按顺序运行。后续请求(对服务的意图)会一直等到第一个操作完成。 |
| 线 | 主要用于不想在 UI 线程上工作的作业(例如网络),但是,只要它们的父线程没有被杀死,就可以用于长期运行的后台工作。线程被绑定到父应用。 |
| 异步任务 | 绑定到父Activity
的生命,意味着如果活动结束或者被杀死,那么AsyncTask
也是。这样做的好处是更容易将工作从AsyncTask
推回到 UI 线程。 |
| 前台服务 | 从 API 26 开始,后台服务(如意向服务)被限制为仅在应用处于前台时运行。取而代之的是前台服务,在前台服务运行时,它们必须向用户显示一个持续的通知(例如,一个音乐应用在播放时显示一个音乐播放器)。 |
IntentService、AlarmManager 和 BootComplete
如前所述,IntentService
1 是一种不能直接与 UI 交互的服务。IntentService
中的工作请求按顺序运行,请求将一直等待,直到当前操作完成。在IntentService
上运行的操作不能被中断。
一个AlarmManager
2 是 Android 中的一种机制,允许代码在后台线程中延迟和继续运行。一个AlarmManager
可以被配置为在未来的特定时间以预先配置的时间间隔运行。AlarmManager
还有一个setAlarmClock
选项,允许它在设备处于低功耗空闲或打盹模式时触发。
可以设置一个BroadcastReceiver
来监听引导完成意图 3 ,如第 4 章所述。该意图在设备重启后启动时发送。反过来,在接收到BootComplete
意图后启动AlarmManager
将意味着设备重启后后台服务继续运行。
从 Android Oreo 8(API 26 级)开始,Android 服务不再能从后台进程启动。 4 这意味着在 Android 8+中,要使用JobSchedulers
或前台服务。请记住,虽然该功能仅适用于针对 Android 8+的应用,但它可以由用户在设置 5 页面中启用,这也对服务如何在后台运行提出了许多额外的限制。
下面的函数将设置一个 报警管理器 按照 waitBeforeRepeatInMinutes 参数的定义每 x 分钟重复一次:
public void startPeriodicWork(long waitBeforeRepeatInMinutes){
// Construct an intent that will execute the AlarmReceiver
Intent intent = new Intent(context, AlarmReceiver.class);
// Create a PendingIntent to be triggered when the alarm goes off
final PendingIntent pIntent = PendingIntent.getBroadcast(context, AlarmReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup periodic alarm every every half hour from this point onwards
long firstMillis = System.currentTimeMillis(); // alarm is set right away
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// First parameter is the type: ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC_WAKEUP
// Interval can be INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_DAY
if (alarm != null) {
alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis,
waitBeforeRepeatInMinutes * 60 * 1000, pIntent);
}
}
接下来,创建 AlarmManager BroadcastReceiver 类。在 Android manifest 中设置 process 属性,这样如果应用关闭了 6 ,它将继续保持活动状态。作为其中的一部分,将 BroadcastReceiver 添加到 AndroidManifest.xml 文件中。
<receiver android:name=".receivers.AlarmReceiver"
android:process=":remote" />
在 警报接收者 中增加以下内容。java:
public class AlarmReceiver extends BroadcastReceiver {
public static final int REQUEST_CODE = 12345;
// Triggered by the Alarm periodically (starts the service to run task)
@Override
public void onReceive(Context context, Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Alarm Receiver with tid "+ tid);
TaskManager taskManager = new TaskManager(context);
taskManager.oneOffTask();
}
}
接下来,为 BootComplete 添加 BroadcastReceiver。将以下内容添加到 AndroidManifest.xml 文件中:
<receiver android:name=".receivers.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
然后创建BootReceiver.java
类:
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Boot Complete Receiver with tid "+ tid);
TaskManager taskManager = new TaskManager(context);
taskManager.startPeriodicWork(5);
}
}
最后创造出IntentService
;为此,创建一个名为 ServiceManager.java: 的文件
public class ServiceManager extends IntentService {
public ServiceManager() {
super("ServiceTest"); //Used to name the worker thread, important only for debugging.
}
@Override
protected void onHandleIntent(Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Service with tid "+ tid);
String val = intent.getStringExtra("foo");
//todo Add the work to be performed here.
}
}
将此服务添加到 AndroidManifest.xml
文件:
<service android:name=".managers.ServiceManager"
android:exported="false"/>
前台服务
Android 中有一系列不同类型的服务 7 ,从启动的服务(运行在 UI 线程中)到IntentService
(运行在自己的线程中)再到绑定的服务(只要有一个活动绑定到它就运行)。
截至 Android 8 Oreo (API 26),Android 应用运行后台服务有限制,除非应用本身在前台。在这种情况下,应该使用startForegroundService()
方法,而不是使用context.startService()
方法。在此之后,服务有 5 秒钟的时间向用户显示通知并调用startForeground(1, notification)
方法,该方法在服务期间一直存在,直到stopForeground(true)
和stopSelf()
方法被调用。前台服务像任何其他服务一样扩展了Service
类,并且除了遵循前面的规则和限制之外,以相同的方式进行操作。
应该使用如下代码来标识应该使用后台服务还是前台服务:
Intent intent = new Intent(context, ServiceManager.class); //replace with an appropriate intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
}else{
context.startService(intent);
}
由于通知是前台服务启动的一部分,这意味着它的副产品是建立一个通知通道 8 (针对 Android 8.0 - API 级别 26 及以上)。添加通知通道是为最终用户提供细粒度访问的一种方式,允许他们更改通知设置并决定应用中的哪些通知通道应该可见。
以下显示了设置通知通道的示例:
public static void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance);
channel.setDescription(CHANNEL_DESC);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
发送通知:
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, 0);
Notification notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle("Notification Title")
.setContentText("Notification Text")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build();
startForeground(1, notification);
}
从 Android 9 (API level 28)开始,除了如下将您的服务添加到 Android 清单中,您还需要添加 FOREGROUND_SERVICE
权限:
<service android:name=".managers.ForegroundServiceManager"
android:exported="false"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
作业调度程序
从 Android 5 L (API 21)开始,当设备有更多可用资源时,作业调度器作为一种任务批处理作业的方式被引入。作为一个整体,多项工作可以由JobSchedulers
,
来完成,他们会将这些任务分成几批。这意味着所分配的工作可能不会按预期执行;然而,它将在该时间前后发生(例如,被指示每 15 分钟执行一次的任务可能在一次运行的 14 分钟后执行,而在另一次运行的 16 分钟后执行)。
JobSchedulers 最强大的功能之一是,如果设置或未设置特定的标准,例如,没有网络连接、电池正在充电或设备处于空闲状态,它们允许工作延期。还有一个选项是用setPeriodic
和setPersisted
来运行周期性的工作,并在重启后继续运行(如果应用拥有 RECEIVE_BOOT_COMPLETED 权限)。setOverrideDeadline
选项还允许在运行一次性工作的最大时间内,允许在强制运行工作之前等待一段时间。
添加以下内容。如果不想要周期工作器,则删除 setPeriodic 和 setPersisted 标记:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startJobScheduler(){
ComponentName serviceComponent = new ComponentName(context, JobSchedulerManager.class);
JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent);
//builder.setMinimumLatency(1 * 1000); // wait at least /Can't call setMinimumLatency() on a periodic job/
//builder.setOverrideDeadline(3 * 1000); // maximum delay //Can't call setOverrideDeadline() on a periodic job.
builder.setPeriodic(1000); //runs over time
builder.setPersisted(true); // persists over reboot
//builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); // require unmetered network
//builder.setRequiresDeviceIdle(true); // device should be idle
//builder.setRequiresCharging(false); // we don't care if the device is charging or not
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler != null) {
jobScheduler.schedule(builder.build());
}
}
然后做一个名为 JobSchedulerManager.java 的类:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerManager extends JobService {
@Override
public boolean onStartJob(JobParameters jobParameters) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Job Scheduler with tid "+ tid);
//todo perform work here
// returning false means the work has been done, return true if the job is being run asynchronously
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
将下面的类添加到 AndroidManifest.xml 文件中:
<service android:name=".managers.JobSchedulerManager"
android:permission="android.permission.BIND_JOB_SERVICE"/>
一个JobScheduler
服务必须用先前的权限来保护,这样只有具有该权限的应用才能给该服务分配任务。如果在清单中声明了作业服务,但未使用此权限进行保护,则该服务将被系统忽略。
工作经理
如 Android 文档所述, 9 WorkManagers
从 API 14 开始向后兼容。API 14-22 上使用了BroadcastReceiver
和AlarmManager
的组合,API 23+上使用了JobScheduler
。使用WorkManager
而不是AlarmManager
的一个主要缺点是对它们的运行时间有限制(这是从在幕后使用JobScheduler
继承而来的);这包括WorkManager
运行时间不得超过 10 分钟,并且在当前工作开始至少 15 分钟后才能执行另一项连续工作。这样做的原因是为了遵守瞌睡限制。
使用工作管理器时,将以下依赖项添加到 gradle.build
文件中:
def work_version = "2.3.3"
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
下面的代码执行任务并启动一个工作管理器:
PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(
com.example.taskscheduler.managers.WorkManager.class, 15, TimeUnit.MINUTES)
.build(); //update path to match your created WorkManager.java class
WorkManager.getInstance().cancelAllWork();
WorkManager.getInstance().enqueue(work);
最后创建一个名为 WorkManager.java 的类:
public class WorkManager extends Worker {
Context context;
public WorkManager(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
this.context = context;
}
@Override
public Result doWork() {
int tid = Process.myTid();
Log.v("TaskScheduler", "Worker started with tid "+ tid);
// Todo run your work here.
return Result.success();
}
}
穿线
在main
应用线程(由系统为每个应用创建)之外,一个应用可以有多个额外的执行线程。然而,线程的设置相当简单,因为AsyncTasks
被绑定到它们的父线程(通常是一个Activity
)的生命中。在这种情况下,如果线程的父线程被破坏(即,被用户从任务堆栈中移除),则该线程受到垃圾收集(其中移除未使用的资源以为其他组件回收内存)。
启动线程:
public void startThread(){
Thread thread = new ThreadManager();
thread.start();
}
制作一个名为 ThreadManager.java 的 java 类:
public class ThreadManager extends Thread{
public ThreadManager() {
super();
}
@Override
public void run() {
long tid = getId();
// Todo do work here.
Log.v("TaskScheduler", "Starting a new thread "+ tid);
while (true){
Log.v("TaskScheduler", "In a thread: " + tid);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
异步任务
在 Android R–API level 30 中不推荐使用(建议使用标准的java.util.concurrent
或 Kotlin 并发实用程序)–
AsyncTasks 允许在单独的线程上短期运行(一次仅运行几秒钟)代码,同时还可以访问 UI 线程。当构造一个AsyncTask
对象时,需要提供三种类型;这些是:
-
传递到 AsyncTask 执行中的参数的类型
-
后台计算期间使用的进度单位的类型
-
后台方法的结果的类型
必须在主线程上加载、创建和执行——从 API 级开始,这是自动完成的。
调用异步任务:
AsyncTask<String, Void, Void> task = new myAsyncTask(getApplicationContext()).execute("example string");
取消异步任务:
task.cancel(true);
创建一个 AsyncTask 类(作为活动类的私有或包私有子类):
class myAsyncTask extends AsyncTask<String, Void, Void> {
private Context mContext;
public myAsyncTask(Context context) {
this.mContext = context;
}
@Override
protected Void doInBackground(final String... strings) {
final Context context = this.mContext;
runOnUiThread(new Runnable() {
@Override
public void run() {
for (String text:strings) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
}
}
});
return null;
}
}
电池和安全对长期运行服务的影响
长期运行的服务可以为用户提供无数有用的功能;然而,由于电池和安全问题,有无数种方法可以提前终止长期运行的服务。这随服务类型的不同而不同;但是,这通常是由电源管理限制引起的。 10 这些包括以下运行时的限制:
瞌睡
Doze 是在 Android 6 (API level 23)中引入的,当设备空闲时(意味着设备最近一段时间没有接收用户交互),Doze 通过将应用运行时划分到维护窗口中来充当电池节省工具。这些应用可以在其中运行后台任务的窗口开始时很频繁,但是,随着时间的推移,设备空闲的时间越长,这些窗口就会变得越来越不同。这意味着虽然一个AlarmManager
可能被分配每 5 分钟运行一次的任务,但是瞌睡限制会阻止它定期运行。
报警管理器的限制:
-
setExact()
和setWindow()
报警管理器报警被推迟到下一个维护窗口。 -
setAndAllowWhileIdle()
和setExactAndAllowWhileIdle()
将在打盹维护窗口期间正常启动。setAlarmClock()
也将在维护窗口期间启动,系统在警报触发前不久退出休眠。
作业计划程序的限制:
- 作业调度程序或工作管理器被挂起。
其他限制:
- 网络访问、唤醒锁、Wi-Fi 扫描和同步适配器会被忽略和暂停。
您可以使用下面的命令测试一个应用如何在 Doze 模式下运行(在 API 级别 23 以上:
adb shell dumpsys deviceidle force-idle
通过运行以下命令可以退出空闲模式:
adb shell dumpsys deviceidle unforce
应用备用桶
Android 9 (API 级别 28)增加了另一个省电功能。此功能将所有应用分配到四个存储桶之一。每个制造商可以为如何将应用放入每个桶中设置自己的标准(Android 文档强调“机器学习”技术可以用于支持这一决策过程)。反过来,为什么应用被分配特定的存储桶的确切原理是未知的。
这些桶是
-
活动 -应用当前正在使用或最近使用过,包括应用是否已启动活动或正在运行前台服务,或者用户已点击应用的通知:
-
工作 -无限制
-
警报 -无限制
-
-
工作集-app 正常使用:
-
作业 -最多延迟 2 小时
-
警报 -最多延迟 6 分钟
-
-
频繁 -经常使用该应用,但不是每天都使用:
-
作业 -最多延迟 8 小时
-
警报 -最多延迟 30 分钟
-
-
稀有 -不常用的应用:
-
作业 -最多延迟 24 小时
-
警报 -最多延迟 2 小时
-
联网 -最多延迟 24 小时
-
-
Never -应用已安装,但从未运行:
- 如果应用从未运行过,组件将被禁用。
修改组件的状态
应用组件是用前面提到的AndroidManifest.xml file. A
编写的,有四种主要类型的组件,它们是
-
活动
-
服务
-
广播接收机
-
内容供应器
静态修改组件状态
可以通过在 Android 清单中编辑组件条目的android:enabled="false"
标签来静态修改组件。以下活动SecondaryActivity
已经通过其在 Android 清单中的条目被默认禁用。
设置组件的启用属性:
<activity android:name=".SecondaryActivity"
android:enabled="false"
/>
动态修改组件
组件可以通过编程设置为三种主要状态,它们是
-
组件 _ 启用 _ 状态 _ 默认
- 将组件设置为清单中定义的默认状态。
-
组件已启用状态已启用
- 显式启用组件。
-
组件 _ 启用 _ 状态 _ 禁用
- 显式禁用组件。禁用的组件不能使用或启动。
有两种其他的组成状态;然而,这些不能用setComponentEnabledSetting
方法设置。这些是
-
组件 _ 启用 _ 状态 _ 禁用 _ 用户
- 显式禁用该组件,并且可以由用户在适当的系统用户界面中重新启用。
-
组件 _ 启用 _ 状态 _ 禁用 _ 直到 _ 使用
- 这种状态意味着组件应该被识别为禁用的(即,在启动器中不显示活动),直到用户明确地试图在它应该被设置为启用的地方使用它。
启用组件 :
PackageManager packageManager = getApplicationContext().getPackageManager();
ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,PackageManager.DONT_KILL_APP);
返回组件的状态:
PackageManager packageManager = getApplicationContext().getPackageManager();
ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class);
int componentState = packageManager.getComponentEnabledSetting(componentName);
禁用一个组件 :
PackageManager packageManager = getApplicationContext().getPackageManager();
ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,PackageManager.DONT_KILL_APP);
创建 Android 启动器
Android Launcher 在 API 级别 1 中实现,是 Android 的一个组件,它允许 Android 应用作为 Android 设备主屏幕上的基本活动(如图 9-2 所示)。这些主屏幕可以由各个原始设备制造商设置;然而,其他著名的发射器包括 Facebook Home。必须在 Android 设备的设置菜单中设置一个启动器,如图 9-1 所示。
图 9-2
示例启动器
图 9-1
启动器设置
创建启动器应用
首先将以下属性添加到 AndroidManifest.xml 文件中的 activities 活动标记:
android:launchMode="singleTask"
然后给同一个活动标签的意图过滤器添加两个类别:
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.HOME" />
在这个阶段,该应用将作为一个启动器,并且可以从 Android 设备的设置中选择作为主屏幕。下面详细介绍了几个在创建启动器时有用的附加技术。
附加功能
检索应用列表:
private List<ResolveInfo> getListOfApplications(Context context){
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> pkgAppsList = context.getPackageManager().queryIntentActivities( mainIntent, 0);
return pkgAppsList;
}
检索应用的图标:
public static Drawable getActivityIcon(Context context, String packageName, String activityName) {
PackageManager pm = context.getPackageManager();
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName, activityName));
ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
return resolveInfo.loadIcon(pm);
}
设置图像视图。将一个 ImageView 对象添加到您的活动中,名称为 imageView:
<ImageView
android:id="@+id/imageView"
android:layout_width="129dp"
android:layout_height="129dp"
android:foregroundGravity="center_vertical"
app:srcCompat="@android:drawable/ic_dialog_alert"
android:layout_gravity="center"
/>
为 ImageView : 创建一个点击监听器
ImageView chromeIcon = (ImageView) findViewById(R.id.imageView);
chromeIcon.setImageDrawable(getActivityIcon(getApplicationContext(),"com.android.chrome", "com.google.android.apps.chrome.Main"));
ImageView img = findViewById(R.id.imageView);
img.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent launchIntent = getPackageManager().getLaunchIntentForPackage("com.android.chrome");
startActivity(launchIntent);
}
});
设置壁纸。将下面的代码添加到 styles.xml 的名称为 AppTheme 的样式标记中:
<item name="android:windowShowWallpaper">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
隐藏系统界面 :
private void hideSystemUI() {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN);
版权属于:月萌API www.moonapi.com,转载请注明出处