安卓各个平台适配

标题安卓各个平台适配

一、 安卓6.0适配

【转载】Android 6.0 运行时权限处理完全解析

1.targetSdkVersion=Android 6.0(API 级别 23)

新的权限机制更好的保护了用户的隐私,Google将权限分为两类,一类是Normal Permissions,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是Dangerous Permission,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等。

Normal Permissions如下
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS

Dangerous Permissions:
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS

group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL

group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR

group:android.permission-group.CAMERA
permission:android.permission.CAMERA

group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS

group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION

group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE

group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO

group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS

可以通过adb shell pm list permissions -d -g进行查看。

看到上面的dangerous permissions,会发现一个问题,好像危险权限都是一组一组的,恩,没错,的确是这样的,

那么有个问题:分组对我们的权限机制有什么影响吗?

的确是有影响的,如果app运行在Android 6.x的机器上,对于授权机制是这样的。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对READ_CONTACTS已经授权了,当你的app申请WRITE_CONTACTS时,系统会直接授权通过。此外,对于申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(ps:这个dialog是不能进行定制的)。

不过需要注意的是,不要对权限组过多的依赖,尽可能对每个危险权限都进行正常流程的申请,因为在后期的版本中这个权限组可能会产生变化。

2.相关API

在AndroidManifest文件中添加需要的权限。

这个步骤和我们之前的开发并没有什么变化,试图去申请一个没有声明的权限可能会导致程序崩溃。

检查权限

if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {
}else{
    //
}

这里涉及到一个API,ContextCompat.checkSelfPermission,主要用于检测某个权限是否已经被授予,方法返回值为PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。

申请授权

 ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

该方法是异步的,第一个参数是Context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。

处理权限申请回调

@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            // permission was granted, yay! Do the
            // contacts-related task you need to do.

        } else {

            // permission denied, boo! Disable the
            // functionality that depends on this permission.
        }
        return;
    }
}
   }

ok,对于权限的申请结果,首先验证requestCode定位到你的申请,然后验证grantResults对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。如果你同时申请两个权限,那么grantResults的length就为2,分别记录你两个权限的申请结果。如果申请成功,就可以做你的事情了~

当然,到此我们的权限申请的不走,基本介绍就如上述。不过还有个API值得提一下:

// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
        Manifest.permission.READ_CONTACTS)) 
    // Show an expanation to the user *asynchronously* -- don't block
    // this thread waiting for the user's response! After the user
    // sees the explanation, try again to request the permission.

}

这个API主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请。也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权,则使用该方法。

那么将上述几个步骤结合到一起就是:

 // Here, thisActivity is the current activity
 if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {

// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
        Manifest.permission.READ_CONTACTS)) {

    // Show an expanation to the user *asynchronously* -- don't block
    // this thread waiting for the user's response! After the user
    // sees the explanation, try again to request the permission.

} else {

    // No explanation needed, we can request the permission.

    ActivityCompat.requestPermissions(thisActivity,
            new String[]{Manifest.permission.READ_CONTACTS},
            MY_PERMISSIONS_REQUEST_READ_CONTACTS);

    // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
    // app-defined int constant. The callback method gets the
    // result of the request.
}
}

3.简单的例子

package com.zhy.android160217;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity
{

private static final int MY_PERMISSIONS_REQUEST_CALL_PHONE = 1;

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

public void testCall(View view)
{
    if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.CALL_PHONE)
            != PackageManager.PERMISSION_GRANTED)
    {

        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.CALL_PHONE},
                MY_PERMISSIONS_REQUEST_CALL_PHONE);
    } else
    {
        callPhone();
    }
}

public void callPhone()
{
    Intent intent = new Intent(Intent.ACTION_CALL);
    Uri data = Uri.parse("tel:" + "10086");
    intent.setData(data);
    startActivity(intent);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{

    if (requestCode == MY_PERMISSIONS_REQUEST_CALL_PHONE)
    {
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
        {
            callPhone();
        } else
        {
            // Permission Denied
            Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show();
        }
        return;
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

在Android 6.x上运行是,点击testCall,即会弹出授权窗口,如何你Allow则直接拨打电话,如果Denied则Toast弹出”Permission Denied”。

例子很简单,不过需要注意的是,对于Intent这种方式,很多情况下是不需要授权的甚至权限都不需要的,比如:你是到拨号界面而不是直接拨打电话,就不需要去申请权限;打开系统图库去选择照片;调用系统相机app去牌照等。更多请参考Consider Using an Intent
.

当然,上例也说明了并非所有的通过Intent的方式都不需要申请权限。一般情况下,你是通过Intent打开另一个app,让用户通过该app去做一些事情,你只关注结果(onActivityResult),那么权限是不需要你处理的,而是由打开的app去处理。

4、封装库

https://github.com/lovedise/PermissionGen
https://github.com/hongyangAndroid/MPermissions.

二、安卓7.0适配

【转载】Android7.0适配教程,心得

1、使用FileProvider

FileProvider使用大概分为以下几个步骤:

manifest中申明FileProvider
res/xml中定义对外暴露的文件夹路径
生成content://类型的Uri
给Uri授予临时权限
使用Intent传递Uri

1.manifest中申明FileProvider:

		<manifest>

		  <application>
		
		    <provider
		        android:name="android.support.v4.content.FileProvider"
		        android:authorities="com.demo.fileprovider"
		        android:exported="false"
		        android:grantUriPermissions="true">
		        <meta-data
		            android:name="android.support.FILE_PROVIDER_PATHS"
		            android:resource="@xml/paths" />
		    </provider>
	
		  </application>
		</manifest>

android:name:provider你可以使用v4包提供的FileProvider,或者自定义的,只需要在name申明就好了,一般使用系统的就足够了。

android:authorities:类似schema,命名空间之类,后面会用到。

android:exported:false表示我们的provider不需要对外开放。

android:grantUriPermissions:申明为true,你才能获取临时共享权限。

2. res/xml中定义对外暴露的文件夹路径:

新建paths.xml,文件名随便起,后面会引用到。

<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <files-path name="my_images" path="images"/>
</paths>

name:一个引用字符串。

path:文件夹“相对路径”,完整路径取决于当前的标签类型。

path可以为空,表示指定目录下的所有文件、文件夹都可以被共享。

paths这个元素内可以包含以下一个或多个,具体如下:

<files-path name="name" path="path" />

物理路径相当于Context.getFilesDir() + /path/。

<cache-path name="name" path="path" />

物理路径相当于Context.getCacheDir() + /path/。

<external-path name="name" path="path" />

物理路径相当于Environment.getExternalStorageDirectory() + /path/。

<external-files-path name="name" path="path" />

物理路径相当于Context.getExternalFilesDir(String) + /path/。

<external-cache-path name="name" path="path" />

物理路径相当于Context.getExternalCacheDir() + /path/。

3.生成content://类型的Uri

我们通常通过File生成Uri的代码是这样:

File picFile = xxx;
Uri picUri = Uri.fromFile(picFile);

这样生成的Uri,路径格式为file://xxx。这种Uri是无法在App之间共享的,我们需要生成content://xxx类型的Uri,方法就是通过Context.getUriForFile来实现:

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.demo.fileprovider", newFile)

4.给Uri授予临时权限

	intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
	               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

FLAG_GRANT_READ_URI_PERMISSION:表示读取权限;
FLAG_GRANT_WRITE_URI_PERMISSION:表示写入权限。

5.使用Intent传递Uri

以开头的拍照代码作为示例,需要这样改写:

File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);

// 授予目录临时共享权限

	intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
	               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
	startActivityForResult(intent, 100);   

2、广播

于Android N后台的优化主要是关闭了三项系统广播:网络状态变更广播、拍照广播以及录像广播。

网络变化的广播(CONNECTIVITY_CHANGE),当网络发生变化时所有注册了隐式监听网络变化的app都会被启动。删除这些广播可以显著提升设备性能和用户体验。同样地,拍照广播和录视频广播(ACTION_NEW_PICTURE or ACTION_NEW_VIDEO)也会出现上述情况。

在Android N平台下即使在Manifest.xml清单文件中注册了 CONNECTIVITY_ACTION广播,在网络发生变化时也不会接收到任何的信息。但是正在前台运行的应用程序如果在主线程中通过Context.registerReceiver()动态注册了CONNECTIVITY_ACTION广播,该应用程序仍然可以接收到该广播。

3、分屏

Android N允许用户一次在屏幕中使用两个App,用户可以左右并排/上下摆放两个App来使用。用户还可以左右/上下拖拽中间的那个小白线来改变两个App的尺寸。
20170504149388867443363.png 20170504149388869827151.png

如何操作来进入分屏模式的:

点击右下角的方块,进入任务管理器,长按一个App的标题栏,将其拖入屏幕的高亮区域,这个App金进入了分屏模式。然后在任务管理器中选择另一个App,单击它使得这个App也进入分屏模式。
打开一个App,然后长按右下角的方块,此时已经打开的这个App将进入分屏模式。然后在屏幕上的任务管理器中选择另外一个App,单击它使得这个App也进入分屏模式。

1、分屏模式的生命周期

官方说法:在分屏模式下,用户最近操作、激活过的Activity将被系统视为topmost。而其他的Activity都属于paused状态,即使它是一个对用户可见的Activity。但是这些可见的处于paused状态的Activity将比那些不可见的处于paused状态的Activity得到更高优先级的响应。当用户在一个可见的paused状态的Activity上操作时,它将得到恢复resumed状态,并被系统视为topmost。而之前那个那个处于topmpst的Activity将变成paused状态。

那么这种可见的pause的状态将带来什么影响呢?

在分屏模式中,一个App可以在对用户可见的状态下进入paused状态,所以你的App在处理业务时,应该知道自己什么时候应该真正的暂停。例如一个视频播放器,如果进入了分屏模式,就不应该在onPaused()回调中暂停视频播放,而应该在onStop()回调中才暂停视频,然后在onStart回调中恢复视频播放。关于如果知道自己进入了分屏模式,在Android N的Activity类中,增加了一个void onMultiWindowChanged(boolean inMultiWindow)回调,所以我们可以在这个回调知道App是不是进入了分屏模式。

分屏时Activity的生命周期

当前显示自己的应用页面,长按多任务键时出现分屏

onConfigurationChanged()-> onMultiWindowModeChanged()-> onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()

分屏时长按多任务键,全屏显示自己的应用时

onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()-> onConfigurationChanged()-> onMultiWindowModeChanged()-> onResume()

2、如何设置App的分屏模式

怎样才能让App进入分屏模式呢?有下面这几个属性。

android:resizeableActivity

直接在AndroidManifest.xml中的或者标签下设置新的属性android:resizeableActivity=”true”。

设置了这个属性后,你的App/Activity就可以进入分屏模式了。

如果这个属性被设为false,那么你的App将无法进入分屏模式,如果你在打开这个App时,长按右下角的小方块,App将仍然处于全屏模式,系统会弹出Toast提示你无法进入分屏模式。这个属性在你target到Android N后,android:resizeableActivity的默认值就是true。

注意:假如你没有适配到Android N(targetSDKVersion < Android N),打包App时的compileSDKVersion < Android N,你的App也是可以支持分屏的!!!!原因在于:如果你的App没有设置 仅允许Activity竖屏/横屏,即没有设置android:screenOrientation=”XXX”属性时,运行Android N系统的设备还是可以将你的App分屏!! 但是这时候系统是不保证运行时的稳定性的,在进入分屏模式时,系统首先也会弹出Toast来提示你说明这个风险。

最新的Android N SDK中,Activity类中增加了下面的方法。

inMultiWindow():返回值为boolean,调用此方法可以知道App是否处于分屏模式。
onMultiWindowChanged(boolean inMultiWindow):当Activity进入或者退出分屏模式时,系统会回调这个方法来通知开发者。回调的参数inMultiWindow为boolean类型,如果inMultiWindow为true,表示Activity进入分屏模式;如果inMultiWindow为false,表示退出分屏模式。

3、支持拖拽

现在可以实现在两个分屏模式的Activity之间拖动内容。Android N Preview SDK中,View已经增加支持Activity之间拖动的API。具体的类和方法主要用到下面几个新的接口:

View.startDragAndDrop():View.startDrag() 的替代方法,需要传递View.DRAG_FLAG_GLOBAL来实现跨Activity拖拽。如果需要将URI权限传递给接收方Activity,还可以根据需要设置View.DRAG_FLAG_GLOBAL_URI_READ或者View.DRAG_FLAG_GLOBAL_URI_WRITE。
View.cancelDragAndDrop():由拖拽的发起方调用,取消当前进行中的拖拽。
View.updateDragShadow():由拖拽的发起方调用,可以给当前进行的拖拽设置阴影。
android.view.DropPermissions:接收方App所得到的权限列表。
Activity.requestDropPermissions():传递URI权限时,需要调用这个方法。传递的内容存储在DragEvent中的ClipData里。返回值为前面的android.view.DropPermissions。
20170505149395235143362.png

在FirstActivity中,发起拖拽。

imageView.setOnTouchListener(new View.OnTouchListener() {
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            /**
             *  构造一个ClipData,将需要传递的数据放在里面
             */
            ClipData.Item item = new ClipData.Item((CharSequence) view.getTag());
            String[] mimeTypes = {ClipDescription.MIMETYPE_TEXT_PLAIN};
            ClipData dragData = new ClipData(view.getTag().toString(), mimeTypes, item);
            View.DragShadowBuilder shadow = new View.DragShadowBuilder(imageView);
            /**
             * startDragAndDrop是Android N SDK中的新方法,替代了以前的startDrag,
             * flag需要设置为DRAG_FLAG_GLOBAL
             */
            view.startDragAndDrop(dragData, shadow, null, View.DRAG_FLAG_GLOBAL);
            return true;
        } else {
            return false;
        }
    }
});

在SecondActivity中,接收这个拖拽的结果,在ACTION_DROP事件中,把结果显示出来。

dropedText.setOnDragListener(new View.OnDragListener() {
    @Override
    public boolean onDrag(View view, DragEvent dragEvent) {
        switch (dragEvent.getAction()) {
            case DragEvent.ACTION_DRAG_STARTED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_STARTED");
                break;
            case DragEvent.ACTION_DRAG_ENTERED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENTERED");
                break;
            case DragEvent.ACTION_DRAG_EXITED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_EXITED");
                break;
            case DragEvent.ACTION_DRAG_LOCATION:
                break;
            case DragEvent.ACTION_DRAG_ENDED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENDED");
                break;
            case DragEvent.ACTION_DROP:
                Log.d(TAG, "ACTION_DROP event");
                //在这里显示接收到的结果
                dropedText.setText(dragEvent.getClipData().getItemAt(0).getText());
                break;
            default:
                break;
        }
        return true;
    }
});

4、分屏原理

分屏功能的实现主要依赖于ActivityManagerService与WindowManagerService这两个系统服务,它们都位于system_server进程中。该进程是Android系统中一个非常重要的系统进程。Framework中的很多服务都位于这个进程中。

整个Android的架构是CS的模型,应用程序是Client,而system_server进程就是对应的Server。

应用程序调用的很多API都会发送到system_server进程中对应的系统服务上进行处理,例如startActivity这个API,最终就是由ActivityManagerService进行处理。

而由于应用程序和system_server在各自独立的进程中运行,因此对于系统服务的请求需要通过Binder进行进程间通讯(IPC)来完成调用,以及调用结果的返回。

ActivityManagerService负责Activity管理。

对于应用中创建的每一个Activity,在ActivityManagerService中都会有一个与之对应的ActivityRecord,这个ActivityRecord记录了应用程序中的Activity的状态。ActivityManagerService会利用这个ActivityRecord作为标识,对应用程序中的Activity进程调度,例如生命周期的管理。

实际上,ActivityManagerService的职责远超出的它的名称,ActivityManagerService负责了所有四大组件(Activity,Service,BroadcastReceiver,ContentProvider)的管理,以及应用程序的进程管理。

WindowManagerService负责Window管理。包括:

窗口的创建和销毁
窗口的显示与隐藏
窗口的布局
窗口的Z-Order管理
焦点的管理
输入法和壁纸管理
等等 每一个Activity都会有一个自己的窗口,在WindowManagerService中便会有一个与之对应的WindowState。WindowManagerService以此标示应用程序中的窗口,并用这个WindowState来存储,查询和控制窗口的状态。

ActivityManagerService与WindowManagerService需要紧密配合在一起工作,因为无论是创建还是销毁Activity都牵涉到Actiivty对象和窗口对象的创建和销毁。这两者是既相互独立,又紧密关联在一起的。

三.安卓8.0适配

【转载】android 8.0 适配(总结)

android 8.0 对应的 sdk 版本 26

1. 通知栏

Android 8.0 引入了通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。用户界面将通知渠道称之为通知类别。

针对 8.0 的应用,创建通知前需要创建渠道,创建通知时需要传入 channelId,否则通知将不会显示。示例代码如下:

// 创建通知渠道
private void initNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = mContext.getString(R.string.app_name);
NotificationChannel channel = new NotificationChannel(mChannelId, name, NotificationManager.IMPORTANCE_DEFAULT);
mNotificationManager.createNotificationChannel(channel);
}
}
// 创建通知传入channelId

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationBarManager.getInstance().getChannelId());

2. 后台执行限制

如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException。

我们无法得知系统如何判断是否允许应用创建后台服务,所以我们目前只能简单 try-catch startService(),保证应用不会 crash,示例代码:

Intent intent = new Intent(getApplicationContext(), InitializeService.class);
intent.setAction(InitializeService.INITIALIZE_ACTION);
intent.putExtra(InitializeService.EXTRA_APP_INITIALIZE, appInitialize);
ServiceUtils.safeStartService(mApplication, intent);

public static void safeStartService(Context context, Intent intent) {
    try { 
        context.startService(intent);
    } catch (Throwable th) {
        DebugLog.i("service", "start service: " + intent.getComponent() + "error: " + th);
        ExceptionUtils.printExceptionTrace(th);
    }
}

或者:

系统不允许后台应用创建后台服务, Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务.

将原来 startService方式启动服务修改为 startForegroundService启动服务

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
     context.startForegroundService(checkIntent);
} else {
     context.startService(checkIntent);
}

程序有通知的情况下:

在系统创建服务后,应用有五秒的时间来调用该服务的startForeground()方法以显示新服务的用户可见通知(如果应用在此时间限制内未调用 startForeground(),则系统将停止服务并声明此应用为 ANR),在服务的onCreate()方法中调用startForeground()即可。

@Override
public void onCreate() {
  super.onCreate();
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    createNotificationChannel();
    Notification notification = new Notification.Builder(getApplicationContext(), channelID).build();
    startForeground(1, notification);
  }
}

3. 允许安装未知来源应用

针对 8.0 的应用需要在 AndroidManifest.xml 中声明 REQUEST_INSTALL_PACKAGES 权限,否则将无法进行应用内升级。

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

4. 主题的 Activity 设置屏幕方向

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:windowIsTranslucent">true</item>
</style>

<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    android:theme="@style/AppTheme">
</activity>

将会抛出以下异常:

java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation大概意思是:只有不透明的全屏Activity可以自主设置界面方向即使满足上述条件,该异常也并非一定会出现,为什么这么说,看下面两种表现:

targetSdk=26,满足上述条件,API 26 手机没问题,API 27 手机没问题
targetSdk=27,满足上述条件,API 26 手机Crash,API 27 手机没问题
有点摸不清 Google 的套路了……

可知,targetSdk=26 时,API 26 和 27 都没有问题,所以这个坑暂时放在适配 API 27 时再填吧。

5. 桌面图标适配

针对 8.0 的应用如果不适配桌面图标,则应用图标在 Launcher 中将会被添加白色背景:
在这里插入图片描述

适配方法:一起来学习Android 8.0系统的应用图标适配吧

https://mp.weixin.qq.com/s/WxgHJ1stBjokPi6lTUd1Mg

适配后的效果:
在这里插入图片描述
配置:
在这里插入图片描述

6. 隐式广播

由于 Android 8.0 引入了新的广播接收器限制,因此您应该移除所有为隐式广播 Intent 注册的广播接收器。将它们留在原位并不会在构建时或运行时令应用失效,但当应用运行在 Android 8.0 上时它们不起任何作用。

显式广播 Intent(只有您的应用可以响应的 Intent)在 Android 8.0 上仍以相同方式工作。

这个新增限制有一些例外情况。如需查看在以 Android 8.0 为目标平台的应用中仍然有效的隐式广播的列表,请参阅隐式广播例外。

developer.android.com/about/versi…

我对隐式广播的理解:

未指定广播接收器类名,通过 Action 发送。如有不妥,还请指教。

需要检查应用静态注册的隐式广播,需要改为动态注册。

7. 网络连接和 HTTP(S) 连接

Android 8.0 对网络连接和 HTTP(S) 连接行为做出了以下变更:

无正文的 OPTIONS 请求具有 Content-Length: 0 标头。之前,这些请求没有 Content-Length 标头。

HttpURLConnection 在包含斜线的主机或颁发机构名称后面附加一条斜线,使包含空路径的网址规范化。例如,它将 example.com 转化为 example.com/。

通过 ProxySelector.setDefault() 设置的自定义代理选择器仅针对所请求的网址(架构、主机和端口)。因此,仅可根据这些值选择代理。传递至自定义代理选择器的网址不包含所请求的网址的路径、查询参数或片段。

URI 不能包含空白标签。 之前,平台支持一种权宜方法,即允许主机名称中包含空白标签,但这是对 URI 的非法使用。此权宜方法只是为了确保与旧版 libcore 兼容。开发者如果对 API 使用不当,将会看到一条 ADB 消息:“URI example…com 的主机名包含空白标签。此格式不正确,将不被未来的 Android 版本所接受。”Android 8.0 废除了此权宜方法;系统对格式错误的 URI 会返回 null。

Android 8.0 在实现 HttpsURLConnection 时不会执行不安全的 TLS/SSL 协议版本回退。

对隧道 HTTP(S) 连接处理进行了如下变更: 在通过连接建立隧道 HTTP(S) 连接时,系统会在 Host 行中正确放置端口号 (:443) 并将此信息发送至中间服务器。之前,端口号仅出现在 CONNECT 行中。 系统不再将隧道连接请求中的 user-agent 和 proxy-authorization 标头发送至代理服务器。 在建立隧道时,系统不再将隧道 Http(s)URLConnection 中的 proxy-authorization 标头发送至代理。相反,由系统生成 proxy-authorization 标头,在代理响应初始请求发送 HTTP 407 后将其发送至此代理。

同样地,系统不再将 user-agent 标头由隧道连接请求复制到建立隧道的代理请求。相反,库为此请求生成 user-agent 标头。

如果之前执行的 connect() 函数失败,send(java.net.DatagramPacket) 函数将会引发 SocketException。 如果存在内部错误,DatagramSocket.connect() 会引发 pendingSocketException。对于 Android 8.0 之前的版本,即使 send() 调用成功,后续的 recv() 调用也会引发 SocketException。为确保一致性,现在这两个调用均会引发 > SocketException。
在回退到 TCP Echo 协议之前,InetAddress.isReachable() 会尝试执行 ICMP。 对于某些屏蔽端口 7 (TCP Echo) 的主机(例如 google.com),如果它们接受 ICMP Echo 协议,现在也许能够访问它们。 对于确实无法访问的主机,此项变更意味着调用需要两倍的时间才能返回结果。
developer.android.com/about/versi…

这点应用一般无需适配

8. 视图焦点

可点击的 View 对象现在默认也可以成为焦点。如果您希望 View 对象可点击但不可成为焦点,请在包含 View 的布局 XML 文件中将 android:focusable 属性设置为 false,或者将 false 传递至应用界面逻辑中的 setFocusable()。

这点基本无需适配

9. 权限

在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。

对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 > WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 > WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

developer.android.com/about/versi…

考拉中的权限都是按需申请的,不需要修改。

10. Tinker

特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。

github.com/Tencent/tin…

经测试,Tinker在8.0上功能正常。

11、悬浮窗要使用类型TYPE_APPLICATION_OVERLAY,原来的类型TYPE_SYSTEM_ALERT从Android8.0开始被舍弃了。 设置悬浮窗类型的兼容代码示例如下:

WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams();
// 设置为TYPE_SYSTEM_ALERT类型,才能悬浮在其它页面之上
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 注意TYPE_SYSTEM_ALERT从Android8.0开始被舍弃了
wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
} else {
// 从Android8.0开始悬浮窗要使用TYPE_APPLICATION_OVERLAY
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}

12、属性动画组合AnimatorSet增加了setCurrentPlayTime和reverse方法,从而允许倒过来播放属性动画组合。 setCurrentPlayTime和reverse方法的调用方式示例如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        animSet.setCurrentPlayTime(0); // 设置当前播放的时间点
        animSet.reverse(); // 从动画尾巴开始倒播至setCurrentPlayTime设置的时间点
    }  

13、分屏

我们知道Activity是默认支持分屏模式的,但我们也需要声明Activity是允许分屏的,再增加支持画中画模式:
在这里插入图片描述

App页面从全屏模式切换到画中画模式,它的Activity生命周期也会经历销毁后重建的过程,如果开发者想保持App页面不被重建,则需给该页面的activity节点加上以下的属性描述:

android:configChanges=“srceenLayout|orientation”

对于视频播放页面,Activity代码同样不在onPause方法中暂停播放视频,而应当在onStop方法中暂停播放,并在onStart方法中恢复播放视频;

在这里插入图片描述
在这里插入图片描述

进入画中画模式:

在这里插入图片描述

四、安卓9.0适配

【转载】Android 9.0 适配指南

targetSdkVersion=28

1、网络

1.Http请求失败

在9.0中默认情况下启用网络传输层安全协议 (TLS),默认情况下已停用明文支持。也就是不允许使用http请求,要求使用https。

比如我使用的是okhttp,会报错:

java.net.UnknownServiceException: CLEARTEXT communication to xxxx not permitted by network security policy

解决方法是需要我们添加网络安全配置。首先在 res 目录下新建xml文件夹,添加network_security_config.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

AndroidManifest.xml中的application添加:

<manifest ... >
    <application android:networkSecurityConfig="@xml/network_security_config">
            ...
    </application>
</manifest>

以上这是一种简单粗暴的配置方法,要么支持http,要么不支持http。为了安全灵活,我们可以指定支持的http域名:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
	<!-- Android 9.0 上部分域名时使用 http -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">secure.example.com</domain>
        <domain includeSubdomains="true">cdn.example1.com</domain>
    </domain-config>
</network-security-config>

当然不止这些配置,还有抓包配置、设置自定义CA以及各种场景下灵活的配置,详细的方法可以查看官方文档。

2.Apache HTTP 客户端弃用

在 Android 6.0 时,就已经取消了对 Apache HTTP 客户端的支持。 从 Android 9.0 开始,默认情况下该库已从 bootclasspath 中移除。但是耐不住有些SDK中还在使用,比如我见到的友盟QQ分享报错问题。

所以要想继续使用Apache HTTP,需要在应用的 AndroidManifest.xml 文件中添加:

<manifest ... >
    <application>
		<uses-library android:name="org.apache.http.legacy" android:required="false"/>
            ...
    </application>
</manifest>

2、前台服务

可以试着搜索一下你的代码,看是否有调用startForegroundService或 startForeground 方法来启动一个前台服务。

startForegroundService 主要来源估计都是8.0适配时候加上的:

Intent intentService = new Intent(this, MyService.class);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    startForegroundService(intentService);
} else {
    startService(intentService);
}

9.0 要求创建一个前台服务需要请求 FOREGROUND_SERVICE 权限,否则系统会引发 SecurityException。

java.lang.RuntimeException: Unable to start service com.weilu.test.MyService@81795be with Intent { cmp=com.weilu.test/.MyService }: 
java.lang.SecurityException: Permission Denial: startForeground from pid=28631, uid=10626 requires android.permission.FOREGROUND_SERVICE
        at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3723)
        at android.app.ActivityThread.access$1700(ActivityThread.java:201)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1705)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:207)
        at android.app.ActivityThread.main(ActivityThread.java:6820)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

解决方法就是AndroidManifest.xml中添加FOREGROUND_SERVICE权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

3.启动Activity

在9.0 中,不能直接非 Activity 环境中(比如Service,Application)启动 Activity,否则会崩溃报错:

 java.lang.RuntimeException: Unable to create service com.weilu.test.MyService: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
        at android.app.ActivityThread.handleCreateService(ActivityThread.java:3578)
        at android.app.ActivityThread.access$1400(ActivityThread.java:201)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1690)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:207)
        at android.app.ActivityThread.main(ActivityThread.java:6820)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

这类问题一般会在点击推送消息跳转页面这类场景,解决方法就是 Intent 中添加标志FLAG_ACTIVITY_NEW_TASK,

Intent intent = new Intent(this, TestActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

4.异形屏适配

这类异形屏叫法很多,刘海屏、水滴屏、挖孔屏、美人尖。。。

其实如果你的页面不需要全屏显示,那么不需要额外的适配工作。

如果页面是全屏显示(比如启动页)。为了防止你的内容被遮挡,大部分场景下都是可以使用获取状态栏高度来处理遮挡的适配问题。因为状态栏的高度都是大于等于刘海的高度。

当然,如果你想利用起来刘海区域,就需要获取刘海位置等信息进行适配。在Android 9.0中官方提供了DisplayCutout 类,可以确定刘海区域的位置,国内的部分厂商在8.0就有了自己的适配方案。

具体的我就不过多介绍了,推荐大家看以下文章:

Android P 刘海屏适配全攻略.

Android刘海屏、水滴屏全面屏适配方案

5.权限

首先是权限组的变更:
在这里插入图片描述
上图可以看到,在9.0 中新增权限组CALL_LOG 并将 READ_CALL_LOG、WRITE_CALL_LOG 和 PROCESS_OUTGOING_CALLS 权限从PHONE中移入该组。

1.限制访问通话记录

如果应用需要访问通话记录或者需要处理去电,则您必须向 CALL_LOG权限组明确请求这些权限。 否则会发生 SecurityException。

2.限制访问电话号码

要通过 PHONE_STATE Intent 操作读取电话号码,同时需要 READ_CALL_LOG 权限和 READ_PHONE_STATE 权限。
要从 PhoneStateListener的onCallStateChanged() 中读取电话号码,只需要 READ_CALL_LOG 权限。 不需要 READ_PHONE_STATE 权限。

6.其他

在 Android 9 中,调用Build.SERIAL 会始终返回 UNKNOWN 以保护用户的隐私。如果你的应用需要访问设备的硬件序列号,那么需要先请求 READ_PHONE_STATE 权限,然后调用 Build.getSerial()。

注意非 SDK 接口的限制。主要是一些热修复、插件化框架涉及比较多,注意及时升级新版本。

多进程使用WebView注意无法共用同一数据目录。 详细点击查看

总的来说,9.0的适配工作需要改动和注意的点相比较以前版本的适配来说并不多,从本篇的篇幅就可以看出来,详细的变化可以参看文末的链接。后面如果遇到什么坑,我也会及时补充进来。感谢你的阅读!!

五、安卓10适配

【转载】开源中国客户端 Android 10 经验适配指南,含代码

targetSdkVersion =29
在这里插入图片描述
现在进入填坑适配指南,包含实际经验代码,绝不照搬翻译文档

1.Region.Op相关异常:java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed

当 targetSdkVersion >= Build.VERSION_CODES.P 时调用 canvas.clipPath(path, Region.Op.XXX); 引起的异常,参考源码如下:

@Deprecated
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) {
     checkValidClipOp(op);
     return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt);
}

private static void checkValidClipOp(@NonNull Region.Op op) {
     if (sCompatiblityVersion >= Build.VERSION_CODES.P
         && op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) {
         throw new IllegalArgumentException(
                    "Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed");
     }
}

我们可以看到当目标版本从Android P开始,Canvas.clipPath(@NonNull Path path, @NonNull Region.Op op) ; 已经被废弃,而且是包含异常风险的废弃API,只有 Region.Op.INTERSECT 和 Region.Op.DIFFERENCE 得到兼容,目前不清楚google此举目的如何,仅仅如此简单就抛出异常提示开发者适配,几乎所有的博客解决方案都是如下简单粗暴:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    canvas.clipPath(path);
} else {
    canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等
}

但我们一定需要一些高级逻辑运算效果怎么办?如小说的仿真翻页阅读效果,解决方案如下,用Path.op代替,先运算Path,再给canvas.clipPath:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
    Path mPathXOR = new Path();
    mPathXOR.moveTo(0,0);
    mPathXOR.lineTo(getWidth(),0);
    mPathXOR.lineTo(getWidth(),getHeight());
    mPathXOR.lineTo(0,getHeight());
    mPathXOR.close();
    //以上根据实际的Canvas或View的大小,画出相同大小的Path即可
    mPathXOR.op(mPath0, Path.Op.XOR);
    canvas.clipPath(mPathXOR);
}else {
    canvas.clipPath(mPath0, Region.Op.XOR);
}

2.明文HTTP限制

当 targetSdkVersion >= Build.VERSION_CODES.P 时,默认限制了HTTP请求,并出现相关日志:

java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy

第一种解决方案:在AndroidManifest.xml中Application添加如下节点代码

第二种解决方案:在res目录新建xml目录,已建的跳过 在xml目录新建一个xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下节点代码

android:networkSecurityConfig="@xml/network_config"

名字随机,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

3.Android Q(10)中的媒体资源读写

相关的Android Q 行为变更不做细说,网上大部分博客关于Android Q 适配都在说行为变更,我们将根据实际遇到的问题,实际解决

1、扫描系统相册、视频等,图片、视频选择器都是通过ContentResolver来提供,主要代码如下:

private static final String[] IMAGE_PROJECTION = {
            MediaStore.Images.Media.DATA,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.BUCKET_ID,
            MediaStore.Images.Media.BUCKET_DISPLAY_NAME};

 Cursor imageCursor = mContext.getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[4] + " DESC");

String path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
String name = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
int id = imageCursor.getInt(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));
String folderPath = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[3]));
String folderName = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[4]));

//Android Q 公有目录只能通过Content Uri + id的方式访问,以前的File路径全部无效,如果是Video,记得换成MediaStore.Videos
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
      path  = MediaStore.Images.Media
                       .EXTERNAL_CONTENT_URI
                       .buildUpon()
                       .appendPath(String.valueOf(id)).build().toString();
 }

2、判断公有目录文件是否存在,自Android Q开始,公有目录File API都失效,不能直接通过new File(path).exists();判断公有目录文件是否存在,正确方式如下:

public static boolean isAndroidQFileExists(Context context, String path){
        if (context == null) {
            return false;
        }
        AssetFileDescriptor afd = null;
        ContentResolver cr = context.getContentResolver();
        try {
            Uri uri = Uri.parse(path);
            afd = cr.openAssetFileDescriptor(Uri.parse(path), "r");
            if (afd == null) {
                return false;
            } else {
                close(afd);
            }
        } catch (FileNotFoundException e) {
            return false;
        }finally {
            close(afd);
        }
        return true;
}

3、保存或者下载文件到公有目录,保存Bitmap同理,如Download,MIME_TYPE类型可以自行参考对应的文件类型,这里只对APK作出说明

public static void copyToDownloadAndroidQ(Context context, String sourcePath, String fileName, String saveDirName){
        ContentValues values = new ContentValues();
        values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
        values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
        values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/" + saveDirName.replaceAll("/","") + "/");

    Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
    ContentResolver resolver = context.getContentResolver();

    Uri insertUri = resolver.insert(external, values);
    if(insertUri == null) {
        return;
    }

    String mFilePath = insertUri.toString();

    InputStream is = null;
    OutputStream os = null;
    try {
        os = resolver.openOutputStream(insertUri);
        if(os == null){
            return;
        }
        int read;
        File sourceFile = new File(sourcePath);
        if (sourceFile.exists()) { // 文件存在时
            is = new FileInputStream(sourceFile); // 读入原文件
            byte[] buffer = new byte[1444];
            while ((read = is.read(buffer)) != -1) {
                os.write(buffer, 0, read);
            }
            is.close();
            os.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    finally {
        close(is,os);
    }

}

4、保存图片相关

 /**
     * 通过MediaStore保存,兼容AndroidQ,保存成功自动添加到相册数据库,无需再发送广告告诉系统插入相册
     *
     * @param context      context
     * @param sourceFile   源文件
     * @param saveFileName 保存的文件名
     * @param saveDirName  picture子目录
     * @return 成功或者失败
     */
 
public static boolean saveImageWithAndroidQ(Context context,
                                              File sourceFile,
                                              String saveFileName,
                                              String saveDirName) {
    String extension = BitmapUtil.getExtension(sourceFile.getAbsolutePath());

    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
    values.put(MediaStore.Images.Media.DISPLAY_NAME, saveFileName);
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
    values.put(MediaStore.Images.Media.TITLE, "Image.png");
    values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + saveDirName);

    Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    ContentResolver resolver = context.getContentResolver();

    Uri insertUri = resolver.insert(external, values);
    BufferedInputStream inputStream = null;
    OutputStream os = null;
    boolean result = false;
    try {
        inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
        if (insertUri != null) {
            os = resolver.openOutputStream(insertUri);
        }
        if (os != null) {
            byte[] buffer = new byte[1024 * 4];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            os.flush();
        }
        result = true;
    } catch (IOException e) {
        result = false;
    } finally {
        Util.close(os, inputStream);
    }
    return result;
}

4.EditText默认不获取焦点,不自动弹出键盘

该问题出现在 targetSdkVersion >= Build.VERSION_CODES.P 情况下,且设备版本为Android P以上版本,目前我们没有从源码中查到相关判断改动,解决方法在onCreate中加入如下代码:

mEditText.post(() -> {
       mEditText.requestFocus();
       mEditText.setFocusable(true);
       mEditText.setFocusableInTouchMode(true);
});

5.Only fullscreen activities can request orientation 异常

该问题出现在 targetSdkVersion >= Build.VERSION_CODES.O_MR1 ,也就是 API 27,当设备为Android 26时(27以上已经修复,也许google觉得不妥当,又改回来了),如果非全面屏透明activity固定了方向,则出现该异常,但是当我们在小米、魅族等Android 26机型测试的时候,并没有该异常,华为机型则报该异常,这是何等的卧槽。。。没办法,去掉透明style或者去掉固定方向代码即可,其它无解

6.安装APK Intent及其它文件相关Intent

/*
* 自Android N开始,是通过FileProvider共享相关文件,但是Android Q对公有目录 File API进行了限制
* 从代码上看,又变得和以前低版本一样了,只是必须加上权限代码Intent.FLAG_GRANT_READ_URI_PERMISSION
*/ 

	private void installApk() {
	        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
	            //适配Android Q,注意mFilePath是通过ContentResolver得到的,上述有相关代码
	            Intent intent = new Intent(Intent.ACTION_VIEW);
	            intent.setDataAndType(Uri.parse(mFilePath) ,"application/vnd.android.package-archive");
	            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
	            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
	            startActivity(intent);
	            return ;
	        }

        File file = new File(saveFileName + "osc.apk");
        if (!file.exists())
            return;
        Intent intent = new Intent(Intent.ACTION_VIEW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
		    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "net.oschina.app.provider", file);
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        startActivity(intent);
}

7.Activity透明相关,windowIsTranslucent属性

Android10又一个天坑,如果你要显示一个半透明的Activity,这在android10之前普通样式Activity只需要设置windowIsTranslucent=true即可,但是到了Android10,它没有效果了,而且如果动态设置View.setVisibility(),界面还会多出残影…

解决办法:使用Dialog样式Theme,且设置windowIsFloating=true,此时问题又来了,如果Activity根布局没有设置fitsSystemWindow=true,默认会对根布局 padddingTop一个状态栏高度。使界面看上去正常,所以如果你的Activity代码有动态适配状态栏高度,需要对根布局设置fitsSystemWindow=true,否则会发现多出来一个padddingTop状态栏高度

8.剪切板兼容

只有当应用处于可交互情况才能访问剪切板和监听剪切板变化,在onResume回调也无法直接访问剪切板,这么做的好处是避免了一些后台应用疯狂监听响应剪切板的内容。

我们APK开发实践中暂时遇到的坑就这些,当然Android Q的改动是相当大的,例如还有App私有沙箱文件、定位权限和后台弹出Activity限制,这些都必须根据自身实践去踩坑适配,有条件的尽可能去阅读官方文档,参考改进。

六、安卓11的适配

targetSdkVersion =30

【转载】Android11最全适配实践指南–应用端

1、分区存储强制执行

关于分区存储,在Android 10就已经推行了,简单的说,就是应用对于文件的读写只能在沙盒环境,也就是属于自己应用的目录里面读写。其他媒体文件可以通过MediaStore进行访问。

但是在 Android 10 的时候,Google 还是为开发者考虑,留了一手。在targetSdkVersion = 29应用中,设置android:requestLegacyExternalStorage=“true”,就可以不启动分区存储,让以前的文件读取正常使用。但是targetSdkVersion = 30中不行了,强制开启分区存储。

当然,作为人性化的 Android,还是为开发者留了一小手,如果是覆盖安装呢,可以增加android:preserveLegacyExternalStorage=“true”,暂时关闭分区存储,好让开发者完成数据迁移的工作。为什么是暂时呢?因为只要卸载重装,就会失效了。以下是关于分区存储会遇到的

所有情况
,给大家罗列出来了,先上代码:

	fun saveFile() {        
	    if (checkPermission()) {            
	        //getExternalStoragePublicDirectory被弃用,分区存储开启后就不允许访问了            
	        val filePath = Environment.getExternalStoragePublicDirectory("").toString() + "/test3.txt"            
	        val fw = FileWriter(filePath)            
	        fw.write("hello world")            
	        fw.close()            
	        showToast("文件写入成功")        
	    }    
	}	
  1. targetSdkVersion = 29,添加android:requestLegacyExternalStorage=”true”(不启用分区存储),读写正常不报错
  2. targetSdkVersion = 30,不删除应用,targetSdkVersion 由 29 修改到 30,读写报错,程序崩溃 (open failed: EACCES (Permission denied))
  3. targetSdkVersion = 30,不删除应用,targetSdkVersion 由 29 修改到 30,增加 android:preserveLegacyExternalStorage=”true”,读写正常不报错
  4. targetSdkVersion = 30,删除应用,重新运行,读写报错,程序崩溃 (open failed: EACCES (Permission denied))

ok,那到底应该怎么改呢?三种方法访问文件:

1.应用专属目录

//分区存储空间
val file = File(context.filesDir, filename) 
//应用专属外部存储空间
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename) 

2.访问公共媒体目录文件

val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {    
    while (cursor.moveToNext()) {        
        val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))        
        val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)        
        println("image uri is $uri")    
    }    
    cursor.close()
} 

3.SAF (存储访问框架–Storage Access Framework)

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)    
intent.addCategory(Intent.CATEGORY_OPENABLE)    
intent.type = "image/*"    
startActivityForResult(intent, 100)     

@RequiresApi(Build.VERSION_CODES.KITKAT)    
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {        
    super.onActivityResult(requestCode, resultCode, data)        
    if (data == null || resultCode != Activity.RESULT_OK) 
        return        
    if (requestCode == 100) {            
        val uri = data.data            
        println("image uri is $uri")        
    }    
} 

说到这里可能又有人问了,那我的应用就是个手机管理器,总不能不让我清其他应用的缓存了吧,有办法!Android 提供了两个 intent 入口:

调用ACTION_MANAGE_STORAGE intent 操作检查可用空间。
调用ACTION_CLEAR_APP_CACHE intent 操作清除所有缓存。
说来说去,反正应用数据私有化是大势所趋,还是早点适配分区存储。

2.所有文件访问权限

虽然说了这么多,但是还有些应用就要访问所有文件,比如杀毒软件,文件管理器。放心,有办法!MANAGE_EXTERNAL_STORAGE 这不来了吗。这个权限就是用来获取所有文件的管理权限。

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />     
val intent = Intent()    
intent.action= Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION    
startActivity(intent)     
//判断是否获取MANAGE_EXTERNAL_STORAGE权限:    
val isHasStoragePermission= Environment.isExternalStorageManager()

来张截图过过瘾:
在这里插入图片描述

3.电话号码相关权限

Android 11 更改了您的应用在读取电话号码时使用的与电话相关的权限。
具体改了什么呢?其实就是两个 API:

TelecomManager 类中的 getLine1Number() 方法
TelecomManager 类中的 getMsisdn() 方法
也就是当用到这两个 API 的时候,原来的READ_PHONE_STATE权限不管用了,需要READ_PHONE_NUMBERS权限才行。

下面具体说说,targetSdkVersion修改到 30,然后运行一个获取电话号码的程序:

ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE), 100)         
btn2.setOnClickListener {            
    val tm = this.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager            
    val phoneNumber = tm.line1Number            
    showToast(phoneNumber)        
} 

崩溃了:

java.lang.SecurityException: getLine1NumberForDisplay: Neither user 10151 nor current process has android.permission.READ_PHONE_STATE, android.permission.READ_SMS, or android.permission.READ_PHONE_NUMBERS 

预想之中哈,Andmanifest.xml中注册好权限,并且添加动态权限申请:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />    
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />     
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE,Manifest.permission.READ_PHONE_NUMBERS), 100) 

搞定,如果你只需要获取手机号码这一个功能,也可以只申请READ_PHONE_NUMBERS这一个权限:

<uses-permission android:name="android.permission.READ_PHONE_STATE"  android:maxSdkVersion="29" />    
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />

4.自定义消息框视图被屏蔽

从 Android 11 开始,已弃用自定义消息框视图。如果您的应用以 Android 11 为目标平台,包含自定义视图的消息框在从后台发布时会被屏蔽

可能有人会奇怪了,什么是自定义消息框视图啊?我说英文你就知道了,英文是custom toast views,也就是自定义 toast。简单写个代码:

Toast toast = new Toast(context);    
toast.setDuration(show_length);    
toast.setView(view);    
toast.show();

糟了糟了,自定义 toast 被弃用了?我们项目就是用的这个啊!不用担心,只是不允许自定义 toast 从后台显示了。比如我写一个 3 秒后再显示 toast,然后应用一打开就进入后台,看看会发生什么:

Handler().postDelayed({          
    IToast.show("你好,我是自定义toast")   
}, 3000)       
W/NotificationService: Blocking custom toast from package com.example.studynote due to package not in the foreground

啥也没显示,只是发出来一个警告。所以不用太过担心,如果实在需要后台显示,就用普通的 toast 吧!

5.现在需要 APK 签名方案 v2

对于以 Android 11 (API 级别 30) 为目标平台,且目前仅使用 APK 签名方案 v1 签名的应用,现在还必须使用 APK 签名方案 v2 或更高版本进行签名。用户无法在搭载 Android 11 的设备上安装或更新仅通过 APK 签名方案 v1 签名的应用。

这个介绍已经很明显了吧,如果你的targetSdkVersion修改到 30,那么你就必须要加上 v2 签名才行。否则无法安装和更新。

6.媒体intent操作需要系统默认相机

从 Android 11 开始,只有预装的系统相机应用可以响应以下 intent 操作:

android.media.action.VIDEO_CAPTURE
android.media.action.IMAGE_CAPTURE
android.media.action.IMAGE_CAPTURE_SECURE

也就是说,如果我调用intent唤起照相机,使用VIDEO_CAPTURE的 action,只有系统的相机能够响应,而第三方的相机应用不会响应了。

val intent=Intent()    
intent.action=android.provider.MediaStore.ACTION_IMAGE_CAPTURE    
startActivity(intent)     
//无法唤起第三方相机了,只能唤起系统相机

这点对普通的相机应用还是有点打击的,官方给的建议是如果要使用特定的第三方相机应用来代表其捕获图片或视频,可以通过为intent设置软件包名称或组件来使这些 intent 变得明确。

7.5G

Android 11 添加了在您的应用中支持 5G 的功能

新的 Android 11 也是支持了5G 相关的一些功能,包括:

检测是否连接到了 5G 网络
检查按流量计费性
首先是检测 5G 网络,通过TelephonyManager的监听方法:

private fun getNetworkType(){        
    val tManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager        
    tManager.listen(object : PhoneStateListener() {             
    @RequiresApi(Build.VERSION_CODES.R)            
    override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {      
        if (ActivityCompat.checkSelfPermission(this@Android11Test2Activity, android.Manifest.permission.READ_PHONE_STATE) != android.content.pm.PackageManager.PERMISSION_GRANTED) {                    
            return                
        }                
        super.onDisplayInfoChanged(telephonyDisplayInfo)                 
        when(telephonyDisplayInfo.networkType) {                    
            TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO -> showToast("高级专业版 LTE (5Ge)")                    
            TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA -> showToast("NR (5G) - 5G Sub-6 网络")                    
            TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE -> showToast("5G+/5G UW - 5G mmWave 网络")                    
            else -> showToast("other")                
        }            
    }         
    }, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)    
}

如果是 5g 网络,就免不了要去判断是不是按流量计费的,否则 5G 的流量可不是开玩笑的。

检测流量计费方法也很简单,监听网络,在回调中判断:

val manager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager     
manager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {    
    override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {          
    super.onCapabilitiesChanged(network, networkCapabilities)             
    //true 代表连接不按流量计费            
    val isNotFlowPay=networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ||                            
        networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)         
    }    
})

判断该值,如果为 true,则将连接视为不按流量计费。

8.后台位置信息访问权限

  1. 从 Android 10 系统的设备开始,就需要请求后台位置权限(ACCESS_BACKGROUND_LOCATION),并选择Allow all the time (始终允许)才能获得后台位置权 限。Android 11 设备上再次加强对后台权限的管理,主要表现在系统对话框上,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要在设置页面选择始终允许才能获得后台位置权限。

  2. 在搭载Android 11 系统的设备上,targetVersion 小于 11 的时候,可以前台后台位置权限一起申请,并且对话框提供了文字说明,表示需要随时获取用户位置信息,进入设置选择始终允许即可。但是 targetVersion 为 30 的时候,你必须单独申请后台位置权限,而且要在获取前台权限之后,顺序不能乱。并且无任何提示,需要开发者自己设计提示样式。

可能有点绕,操作几个例子说明:

1)Android 10 设备,申请前台和后台位置权限 (任意 targetSdkVersion):

requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

执行效果:
在这里插入图片描述
2)Android 11 设备,targetSdkVersion<=29(Android 10),申请前台和后台位置权限:

requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

执行效果:

在这里插入图片描述
3)Android 11 设备,targetSdkVersion=30(Android 11),申请前台和后台位置权限:

requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

执行无反应

4)Android 11设备,targetSdkVersion=30(Android 11),先申请前台位置权限,后申请后台位置权限:

requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 100)

执行效果:
在这里插入图片描述

requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

执行效果 (直接跳转到设置页面,无任何说明):
在这里插入图片描述
所以,该怎么适配呢?

  1. targetSdkVersion<30情况下,如果你之前就有判断过前台和后台位置权限,那就无需担心,没有什么需要适配。
  2. targetSdkVersion>30情况下,需要分开申请前后台位置权限,并且对后台位置权限申请做好说明和引导,当然也是为了更好的服务用户。

9.软件包可见性

Android 11 中,如果你想去获取其他应用的信息,比如包名,名称等等,不能直接获取了,必须在清单文件中添加元素,告知系统你要获取哪些应用信息或者哪一类应用。

在Android 11版本,只能查询到自己应用和系统应用的信息,查不到其他应用的信息了。怎么呢?添加元素,两种方式:

1)元素中加入具体包名

<manifest package="com.example.game">    
<queries>        
    <package android:name="com.example.store" />        
    <package android:name="com.example.services" />    
</queries>    
...</manifest>

2)元素中加入固定过滤的intent

<manifest package="com.example.game">    <queries>        <intent>            <action android:name="android.intent.action.SEND" />            <data android:mimeType="image/jpeg" />        </intent>    </queries></manifest>

可能还是有人会疑惑,那我的应用是浏览器或者设备管理器咋办呢?我就要获取所有包名啊?放心,Android 11 还引入了 QUERY_ALL_PACKAGES 权限,清单文件中加入即可。

10.文档访问限制

为让开发者有时间进行测试,以下与存储访问框架 (SAF) 相关的变更只有在应用以 Android 11 为目标平台时才会生效。

上文存储的时候说过可以通过SAF(存储访问框架–Storage Access Framework) 来访问公共目录,但是 Android 11 再次升级,部分目录和文件不能访问了,具体如下:

无法再使用 ACTION_OPEN_DOCUMENT_TREE intent 操作请求访问以下目录:

内部存储卷的根目录。
设备制造商认为可靠的各个 SD 卡卷的根目录,无论该卡是模拟卡还是可移除的卡。可靠的卷是指应用在大多数情况下可以成功访问的卷。
Download 目录。
无法再使用 ACTION_OPEN_DOCUMENT_TREEACTION_OPEN_DOCUMENT intent 操作请求用户从以下目录中选择单独的文件:

Android/data/ 目录及其所有子目录。
Android/obb/ 目录及其所有子目录。

11.限制对 APN 数据库的读取访问

以 Android 11 为目标平台的应用现在必须具备 Manifest.permission.WRITE_APN_SETTINGS 特权,才能读取或访问电话提供程序 APN 数据库。如果在不具备此权限的情况下尝试访问 APN 数据库,会生成安全异常。

12.自动重置权限

如果应用以 Android 11 为目标平台并且数月未使用,系统会通过自动重置用户已授予应用的运行时敏感权限来保护用户数据。此操作与用户在系统设置中查看权限并将应用的访问权限级别更改为拒绝的做法效果一样。如果应用已遵循有关在运行时请求权限的最佳做法,那么您不必对应用进行任何更改。这是因为,当用户与应用中的功能互动时,您应该会验证相关功能是否具有所需权限。

官方说明说的很清楚了,而且只要应用遵循有关在运行时请求权限的最佳做法,也就是每次需要调用权限的时候都会去判断,那么就不会有什么问题。

如果需要关闭这个功能怎么办呢?只有引导用户去设置页面关闭了,可以调用包含Settings.ACTION_APPLICATION_DETAILS_SETTINGS action的 Intent 将用户定向到系统设置中应用的页面。

怎么检查应用是否停用自动重置功能呢?调用 PackageManager 的isAutoRevokeWhitelisted() 方法。如果此方法返回 true,代表系统不会自动重置应用的权限。

13.前台服务类型

从 Android 9 开始,应用仅限于在前台访问摄像头和麦克风。为了进一步保护用户,Android 11 更改了前台服务访问摄像头和麦克风相关数据的方式。如果您的应用以 Android 11 为目标平台并且在某项前台服务中访问这些类型的数据,您需要在该前台服务的声明的 foregroundServiceType 属性中添加新的 camera 和 microphone 类型。

举例,如果应用某项前台服务需要访问位置信息、摄像头和麦克风,那么就这样添加:

<manifest>    <service ...        android:foregroundServiceType="location|camera|microphone" /></manifest>

14.适配 Android 11 手机

此模块的修改内容针对所有项目在Android 11手机上存在的改动,与targetSdkVersion无关。

1.数据访问审核

为了让应用及其依赖项访问用户私密数据的过程更加透明,Android 11
引入了数据访问审核功能。借助此流程得出的见解,您可以更好地识别和纠正可能出现的意外数据访问。

哪些范畴属于用户私密数据呢?其实就是危险权限的调用,所以这个功能就是提供了可以监听危险权限调用的监听。主要涉及到的方法是AppOpsManager.OnOpNotedCallback。无论是应用本身,还是依赖库或者 SDK 中的代码,只要访问到私密数据 (危险权限),都会回调给我们。

对于工程庞大或者使用较多 SDK 的工程比较适合用上这个功能,让自己应用的私有数据管理更加透明规范,否则对于私有数据的使用和管理并不全面和方便。而且还可以对权限使用添加归因,也就是一个 tag,标志权限用到了什么地方。方便回调的时候知晓哪里使用了

私有数据<font face="-apple-system, system-ui, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif" color="#333333"><span style="font-size: 16px; background-color: rgb(255, 255, 255);">。</span></font>

其中 OnOpNotedCallback 一共三个回调方法:

  1. onNoted 正常情况下都会回调到该方法
  2. onAsyncNoted 如果数据访问并非发生在应用调用API期间,就会调用onAsyncNoted(),比如一些监听器的回调。
  3. onSelfNoted 在极少数情况下,如果应用将自身的UID传递到 noteOp(),需要调用 onSelfNoted()。

最后可以看到权限代码: android:coarse_location 以及归因 shareLocation

2.单次授权

在 Android 11 中,每当应用请求与位置信息、麦克风或摄像头相关的权限时,面向用户的权限对话框会包含仅限这一次选项。如果用户在对话框中选择此选项,系统会向应用授予临时的单次授权。

简单的说,就是在申请与位置信息、麦克风或摄像头相关的权限时,系统会自动提供一个单次授权的选项,只供这一次权限获取。然后用户下次打开 app 的时候,系统会再次提示用户授予权限。这个影响应该不大,只要我们每次使用的时候都去判断权限,没有就去申请即可。放一张新版本权限获取样式:

在这里插入图片描述

3.应用使用情况统计信息

为了更好地保护用户,Android 11 将每个用户的应用使用情况统计信息存储在凭据加密存储空间中。

这就涉及到了UsageStatsManager,UsageStatsManager是 Android 提供统计应用使用情况的服务。通过这个服务可以获取指定时间区间内应用使用统计数据、组件状态变化事件统计数据以及硬件配置信息统计数据。

比如queryAndAggregateUsageStats方法,可以获取指定时间区间内使用统计数据,以应用包名为键值进行数据合并。

但是在 Android 11 设备中,不好意思,不能随意使用这些信息了。只有当isUserUnlocked()方法返回 true 的时候,才能正常访问这些数据。也就是以下两种情况:

  1. 用户在系统启动后首次解锁其设备
  2. 用户在设备上切换到自己的帐号

4.JobScheduler API 调用限制调试

JobScheduler任务调度器,可以在设备空闲时做一些任务处理。Android 11 中如果你设置为debug模式(debuggable 清单属性设置为 true),超出速率限制的JobScheduler API调用将返回 RESULT_FAILURE。这个有什么用呢?应该可以帮助我们发现一些性能问题,感兴趣的可以自己试试。

顺便提下,Jetpack 组件WorkManager也是用到了 JobScheduler,不熟悉的同学可以去了解下,JobScheduler是由 SystemServer 进程启动的一个系统服务,所以才可以有这么大的权限。

5.非SDK接口限制

Android 11 包含更新后的受限制非 SDK 接口列表 (基于与 Android 开发者之间的协作以及最新的内部测试)。在限制使用非
SDK 接口之前,我们会尽可能确保提供公开替代方案。

老样子,Android 11 也会限制一些接口,包括灰名单和白名单,具体看非 SDK 接口列表。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页