Android 10 适配攻略,移动终端软件开发颜色演示

上图将外部存储空间分为了三部分:

  • 特定目录(App-specific),使用getExternalFilesDir()或 getExternalCacheDir()方法访问。无需权限,且卸载应用时会自动删除。

  • 照片、视频、音频这类媒体文件。使用MediaStore 访问,访问其他应用的媒体文件时需要READ_EXTERNAL_STORAGE权限。

  • 其他目录,使用存储访问框架SAF(Storage Access Framwork)

所以在Android 10上即使你拥有了储存空间的读写权限,也无法保证可以正常的进行文件的读写操作。

适配

最简单粗暴的方法就是在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"来请求使用旧的存储模式。

但是我不推荐此方法。因为在下一个版本的Android中,此条配置将会失效,将强制采用外部储存限制。其实早在Android Q Beta 3之前都是强制的,但为了给开发者适配的时间才没有强制执行。所以如果你不抓住这段时间去适配,那么今年下半年出了Android 11。。。直接开花~~

如果你已经适配Android 10,这里有个现象要注意一下:

如果应用通过升级安装,那么还会使用以前的储存模式(Legacy View)。只有通过首次安装或是卸载重新安装才能启用新模式(Filtered View)。

所以在适配时,我们的判断代码如下:

// 使用Environment.isExternalStorageLegacy()来检查APP的运行模式

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()) {

}

这样的好处是你可以在用户升级后,能方便的将用户的数据移动至应用的特定目录。否则

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

浏览器打开:qq.cn.hn/FTe 免费领取

你只能通过SAF去移动,这样会非常麻烦。如果你要移动数据注意只适用于Android 10下,所以现在适配反而是一个好时机。当然如果你不需要迁移数据,那适配会更省事。

下面就说说推荐适配方案:

  • 对于应用中涉及的文件操作,修改一下你的文件路径。

以前我们习惯使用Environment.getExternalStorageDirectory()方法,那么现在可以使用getExternalFilesDir()方法(包括下载的安装包这类的文件)。如果是缓存类型文件,可以放到getExternalCacheDir()路径下。

或者使用MediaStore,将文件存至对应的媒体类型中(图片:MediaStore.Images ,视频:MediaStore.Video,音频:MediaStore.Audio),不过仅限于多媒体文件。

下面代码将图片保存到公共目录下,返回Uri:

public static Uri createImageUri(Context context) {

ContentValues values = new ContentValues();

// 需要指定文件信息时,非必须

values.put(MediaStore.Images.Media.DESCRIPTION, “This is an image”);

values.put(MediaStore.Images.Media.DISPLAY_NAME, “Image.png”);

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/test”);

return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

}

  • 对于媒体资源的访问:比如图片选择器这类的场景。无法直接使用File,而应使用Uri。否则报错如下:

java.io.FileNotFoundException: open failed: EACCES (Permission denied)

比如我在适配项目中使用的图片选择器时,首先修改了Glide 通过加载File的方式显示图片。改为加载Uri的方式,否则图片无法显示出来。

Uri的获取方式还是使用MediaStore:

String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));

Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

其次为了便于不影响之前选择图片返回File的逻辑(因为一般都是上传File,没有直接上传Uri的操作),所以我将最终选择的文件又转存进了getExternalFilesDir(),主要代码如下:

File imgFile = this.getExternalFilesDir(“image”);

if (!imgFile.exists()){

imgFile.mkdir();

}

try {

File file = new File(imgFile.getAbsolutePath() + File.separator +

System.currentTimeMillis() + “.jpg”);

// 使用openInputStream(uri)方法获取字节输入流

InputStream fileInputStream = getContentResolver().openInputStream(uri);

FileOutputStream fileOutputStream = new FileOutputStream(file);

byte[] buffer = new byte[1024];

int byteRead;

while (-1 != (byteRead = fileInputStream.read(buffer))) {

fileOutputStream.write(buffer, 0, byteRead);

}

fileInputStream.close();

fileOutputStream.flush();

fileOutputStream.close();

// 文件可用新路径 file.getAbsolutePath()

} catch (Exception e) {

e.printStackTrace();

}

  • 如果你要获取图片中的地理位置信息,需要申请ACCESS_MEDIA_LOCATION权限,并使用MediaStore.setRequireOriginal()获取。下面是官方的示例代码:

Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,

cursor.getString(idColumnIndex));

final double[] latLong;

// 从ExifInterface类获取位置信息

photoUri = MediaStore.setRequireOriginal(photoUri);

InputStream stream = getContentResolver().openInputStream(photoUri);

if (stream != null) {

ExifInterface exifInterface = new ExifInterface(stream);

double[] returnedLatLong = exifInterface.getLatLong();

// If lat/long is null, fall back to the coordinates (0, 0).

latLong = returnedLatLong != null ? returnedLatLong : new double[2];

// Don’t reuse the stream associated with the instance of “ExifInterface”.

stream.close();

} else {

// Failed to load the stream, so return the coordinates (0, 0).

latLong = new double[2];

}

这样下来,一个图片选择器就基本适配完了。

补充

应用在卸载后,会将App-specific目录下的数据删除,如果在AndroidManifest.xml中声明:android:hasFragileUserData="true"用户可以选择是否保留。

对于SAF的使用,可以查看我之前写的SAF使用攻略,这里就不展开说了。

最后这里有一个介绍Scoped Storage的视频,推荐观看:

准备好使用分区存储 | ADS 中文字幕视频

准备好使用分区存储

2.权限变化

=================================================================

从6.0开始,基本每次都会有权限方面变动,这次也不例外。(前几天发布了Android 11的预览版,看来也有权限方面的变化。。。单次权限即将到来)

1.在后台运行时访问设备位置信息需要权限

Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限(危险权限)。

该权限允许应用程序在后台访问位置。如果请求此权限,则还必须请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限。只请求此权限无效果。

在Android 10的设备上,如果你的应用的 targetSdkVersion < 29,则在请求ACCESS_FINE_LOCATION 或ACCESS_COARSE_LOCATION权限时,系统会自动同时请求ACCESS_BACKGROUND_LOCATION。在请求弹框中,选择“始终允许”表示同意后台获取位置信息,选择“仅在应用使用过程中允许”或"拒绝"选项表示拒绝授权。

如果你的应用的 targetSdkVersion >= 29,则请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限表示在前台时拥有访问设备位置信息的权。在请求弹框中,选择“始终允许”表示前后台都可以获取位置信息,选择“仅在应用使用过程中允许”只表示拥有前台的权限。

总结一下就是下图:

在这里插入图片描述

其实官方不推荐你使用申请后台访问权的方式,因为这样的结果无非就是多请求一个权限,那么这像变更还有什么意义?申请过多的权限,也会造成用户的反感。所以官方推荐使用前台服务来实现,在前台服务中获取位置信息。

  • 首先在清单中对应的service中添加 android:foregroundServiceType=“location”:

<service

android:name=“MyNavigationService”

android:foregroundServiceType=“location” … >

  • 启动前台服务前检查是否具有前台的访问权限:

boolean permissionApproved = ActivityCompat.checkSelfPermission(this,

Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;

if (permissionApproved) {

// 启动前台服务

} else {

// 请求前台访问位置权限

}

如此一来就可以在Service中获取位置信息。

2.一些电话、蓝牙和WLAN的API需要精确位置权限

下面列举了Android 10中必须具有 ACCESS_FINE_LOCATION 权限才能使用类和方法:

  • 电话

TelephonyManager

getCellLocation()

getAllCellInfo()

requestNetworkScan()

requestCellInfoUpdate()

getAvailableNetworks()

getServiceState()

TelephonyScanManager

requestNetworkScan()

TelephonyScanManager.NetworkScanCallback

onResults()

PhoneStateListener

onCellLocationChanged()

onCellInfoChanged()

onServiceStateChanged()

  • WLAN

WifiManager

startScan()

getScanResults()

getConnectionInfo()

getConfiguredNetworks()

WifiAwareManager

WifiP2pManager

WifiRttManager

  • 蓝牙

BluetoothAdapter

startDiscovery()

startLeScan()

BluetoothAdapter.LeScanCallback

BluetoothLeScanner

startScan()

我们可以根据上面提供的具体类和方法,在适配项目中检查是否有使用到并及时处理。

3.ACCESS_MEDIA_LOCATION

Android 10新增权限,上面有提到,不赘述了。

4.PROCESS_OUTGOING_CALLS

Android 10上该权限已废弃。

3.后台启动 Activity 的限制

==============================================================================

简单解释就是应用处于后台时,无法启动Activity。比如点开一个应用会进入启动页或者广告页,一般会有几秒的延时再跳转至首页。如果这期间你退到后台,那么你将无法看到跳转过程。而在之前的版本中,会强制弹出页面至前台。

既然是限制,那么肯定有不受限的情况,主要有以下几点:

  • 应用具有可见窗口,例如前台 Activity。

  • 应用在前台任务的返回栈中已有的 Activity。

  • 应用在 Recents 上现有任务的返回栈中已有的 Activity。Recents 就是我们的任务管理列表。

  • 应用收到系统的 PendingIntent 通知。

  • 应用收到它应该在其中启动界面的系统广播。示例包括 ACTION_NEW_OUTGOING_CALL 和 SECRET_CODE_ACTION。应用可在广播发送几秒钟后启动 Activity。

  • 用户已向应用授予 SYSTEM_ALERT_WINDOW 权限,或是在应用权限页开启后台弹出页面的开关。

因为此项行为变更适用于在 Android 10 上运行的所有应用,所以这一限制导致最明显的问题就是点击推送信息时,有些应用无法进行正常的跳转(具体的实现问题导致)。所以针对这类问题,可以采取PendingIntent的方式,发送通知时使用setContentIntent方法。

当然你也可以申请相应权限或者白名单:

在这里插入图片描述

不过申请白名单这种方法受各种手机厂商所限,很麻烦。感觉还不如引导用户手动开启权限。。。

对于全屏 intent,注意设置最高优先级和添加USE_FULL_SCREEN_INTENT权限,这是一个普通权限。比如微信来语音或者视频通话时,弹出的接听页面就是使用这一功能。

Intent fullScreenIntent = new Intent(this, CallActivity.class);

PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,

fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Builder notificationBuilder =

new NotificationCompat.Builder(this, CHANNEL_ID)

.setSmallIcon(R.drawable.notification_icon)

.setContentTitle(“Incoming call”)

.setContentText("(919) 555-1234")

.setPriority(NotificationCompat.PRIORITY_HIGH) // <— 高优先级

.setCategory(NotificationCompat.CATEGORY_CALL)

// Use a full-screen intent only for the highest-priority alerts where you

// have an associated activity that you would like to launch after the user

// interacts with the notification. Also, if your app targets Android 10

// or higher, you need to request the USE_FULL_SCREEN_INTENT permission in

// order for the platform to invoke this notification.

.setFullScreenIntent(fullScreenPendingIntent, true); // <— 全屏 intent

Notification incomingCallNotification = notificationBuilder.build();

注意:在部分手机上,直接设置setPriority无效(或者说以渠道优先级为准)。所以需要创建通知渠道时将重要性设置为IMPORTANCE_HIGH。

NotificationChannel channel = new NotificationChannel(channelId, “xxx”, NotificationManager.IMPORTANCE_HIGH);

后台启动 Activity 的限制的目的是为了减少对用户操作的中断。如果你有要弹出的页面,推荐你先弹出通知,让用户自己选择接下来的操作,而不是一股脑的强制弹出。(如果你的全屏intent都让用户反感,那他也可以关掉你的通知,不至于任你摆布。)

4.深色主题

=================================================================

Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并不是强制适配项,但是它可以带给用户更好的体验:

  • 可大幅减少耗电量。 OLED 屏幕中每个像素都是自主发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭来达到省电的效果。

  • 为弱视以及对强光敏感的用户提高可视性。深色可以降低屏幕的整体视觉亮度,减少对眼睛的视觉压力。

  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页
评论

打赏作者

微软技术开源

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值