精华内容
下载资源
问答
  • Android 10适配要点,作用域存储

    万次阅读 多人点赞 2020-04-14 08:42:48
    距离Android 10系统正式发布已经过去大半年左右的时间了,你的应用程序已经对它进行适配了吗?在Android 10众多的行为变更当中,有一点是非常值得引起我们重视的,那就是作用域存储。这个新功能直接颠覆了长久以来...

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

    距离Android 10系统正式发布已经过去大半年左右的时间了,你的应用程序已经对它进行适配了吗?

    在Android 10众多的行为变更当中,有一点是非常值得引起我们重视的,那就是作用域存储。这个新功能直接颠覆了长久以来我们一直惯用的外置存储空间的使用方式,因此大量App都将面临着较多代码模块的升级。

    然而,对于作用域存储这个新功能,官方的资料并不多,很多人也没有搞明白它的用法。另外它也不属于《第一行代码》现有的知识架构体系,虽然我有想过在第3版中加入这部分内容的讲解,但几经思考之后还是决定以一讲单独文章的方式来讲解这部分内容,也算是作为《第一行代码 第3版》的内容扩展吧。

    本篇文章对作用域存储进行了比较全面的解析,相信看完之后你将能够轻松地完成Android 10作用域存储的适配升级。

    理解作用域存储

    Android长久以来都支持外置存储空间这个功能,也就是我们常说的SD卡存储。这个功能使用得极其广泛,几乎所有的App都喜欢在SD卡的根目录下建立一个自己专属的目录,用来存放各类文件和数据。

    那么这么做有什么好处吗?我想了一下,大概有两点吧。第一,存储在SD卡的文件不会计入到应用程序的占用空间当中,也就是说即使你在SD卡存放了1G的文件,你的应用程序在设置中显示的占用空间仍然可能只有几十K。第二,存储在SD卡的文件,即使应用程序被卸载了,这些文件仍然会被保留下来,这有助于实现一些需要数据被永久保留的功能。

    然而,这些“好处”真的是好处吗?或 许对于开发者而言这算是好处吧,但对于用户而言,上述好处无异于一些流氓行为。因为这会将用户的SD卡空间搞得乱糟糟的,而且即使我卸载了一个完全不再使用的程序,它所产生的垃圾文件却可能会一直保留在我的手机上。

    另外,存储在SD卡上的文件属于公有文件,所有的应用程序都有权随意访问,这也对数据的安全性带来了很大的挑战。

    为了解决上述问题,Google在Android 10当中加入了作用域存储功能。

    那么到底什么是作用域存储呢?简单来讲,就是Android系统对SD卡的使用做了很大的限制。从Android 10开始,每个应用程序只能有权在自己的外置存储空间关联目录下读取和创建文件,获取该关联目录的代码是:context.getExternalFilesDir()。关联目录对应的路径大致如下:

    /storage/emulated/0/Android/data/<包名>/files
    

    将数据存放到这个目录下,你将可以完全使用之前的写法来对文件进行读写,不需要做任何变更和适配。但同时,刚才提到的那两个“好处”也就不存在了。这个目录中的文件会被计入到应用程序的占用空间当中,同时也会随着应用程序的卸载而被删除。

    那么有些朋友可能会问了,我就是需要访问其他目录该怎么办呢?比如读取手机相册中的图片,或者向手机相册中添加一张图片。为此,Android系统针对文件类型进行了分类,图片、音频、视频这三类文件将可以通过MediaStore API来进行访问,而其他类型的文件则需要使用系统的文件选择器来进行访问。

    另外,我们的应用程序向媒体库贡献的图片、音频或视频,将会自动拥有其读写权限,不需要额外申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。而如果你要读取其他应用程序向媒体库贡献的图片、音频或视频,则必须要申请READ_EXTERNAL_STORAGE权限才行。WRITE_EXTERNAL_STORAGE权限将会在未来的Android版本中废弃。

    好了,关于作用域存储的理论知识就先讲到这里,相信你已经对它有了一个基本的了解了,那么接下来我们就开始上手操作吧。

    我一定要升级吗?

    一定会有很多朋友关心这个问题,因为每当适配升级面临着需要更改大量代码的时候,大多数人的第一想法都是能不升就不升,或者能晚升就晚升。而在作用域存储这个功能上面,恭喜大家,暂时确实是可以不用升级的。

    目前Android 10系统对于作用域存储适配的要求还不是那么严格,毕竟之前传统外置存储空间的用法实在是太广泛了。如果你的项目指定的targetSdkVersion低于29,那么即使不做任何作用域存储方面的适配,你的项目也可以成功运行到Android 10手机上。

    而如果你的targetSdkVersion已经指定成了29,也没有关系,假如你还不想进行作用域存储的适配,只需要在AndroidManifest.xml中加入如下配置即可:

    <manifest ... >
      <application android:requestLegacyExternalStorage="true" ...>
        ...
      </application>
    </manifest>
    

    这段配置表示,即使在Android 10系统上,仍然允许使用之前遗留的外置存储空间的用法来运行程序,这样就不用对代码进行任何修改了。当然,这只是一种权宜之计,在未来的Android系统版本中,这段配置随时都可能会失效(Android 11中已强制启用作用域存储,这段配置在Android 11当中已不再有效)。因此,我们还是非常有必要现在就来学习一下,到底该如何对作用域存储进行适配。

    另外,本篇文章中演示的所有示例,都可以到ScopedStorageDemo这个开源库中找到其对应的源码。

    开源库地址是:https://github.com/guolindev/ScopedStorageDemo

    获取相册中的图片

    首先来学习一下如何在作用域存储当中获取手机相册里的图片。注意,虽然本篇文章中我是以图片来举例的,但是获取音频、视频的用法也是基本相同的。

    不同于过去可以直接获取到相册中图片的绝对路径,在作用域存储当中,我们只能借助MediaStore API获取到图片的Uri,示例代码如下:

    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()
    }
    

    上述代码中,我们先是通过ContentResolver获取到了相册中所有图片的id,然后再借助ContentUris将id拼装成一个完整的Uri对象。一张图片的Uri格式大致如下所示:

    content://media/external/images/media/321
    

    那么有些朋友可能会问了,获取到了Uri之后,我又该怎样将这张图片显示出来呢?这就有很多种办法了,比如使用Glide来加载图片,它本身就支持传入Uri对象来作为图片路径:

    Glide.with(context).load(uri).into(imageView)
    

    而如果你没有使用Glide或其他图片加载框架,想在不借助第三方库的情况下直接将一个Uri对象解析成图片,可以使用如下代码:

    val fd = contentResolver.openFileDescriptor(uri, "r")
    if (fd != null) {
        val bitmap = BitmapFactory.decodeFileDescriptor(fd.fileDescriptor)
    	fd.close()
        imageView.setImageBitmap(bitmap)
    }
    

    上述代码中,我们调用了ContentResolver的openFileDescriptor()方法,并传入Uri对象来打开文件句柄,然后再调用BitmapFactory的decodeFileDescriptor()方法将文件句柄解析成Bitmap对象即可。

    Demo效果:

    这样我们就将获取相册中图片的方式掌握了,并且这种方式在所有的Android系统版本中都适用。

    那么接下来,我们开始学习如何将一张图片添加到相册。

    将图片添加到相册

    将一张图片添加到手机相册要相对稍微复杂一点,因为不同系统版本之间的处理方式是不太一样的。

    我们还是通过一段代码示例来直观地学习一下,代码如下所示:

    fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) {
        val values = ContentValues()
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
        values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
        } else {
            values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
        }
        val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        if (uri != null) {
            val outputStream = contentResolver.openOutputStream(uri)
            if (outputStream != null) {
                bitmap.compress(compressFormat, 100, outputStream)
    			outputStream.close()
            }
        }
    }
    

    这段代码演示了如何将一个Bitmap对象添加到手机相册当中,我来简单解释一下。

    想要将一张图片添加到手机相册,我们需要构建一个ContentValues对象,然后向这个对象中添加三个重要的数据。一个是DISPLAY_NAME,也就是图片显示的名称,一个是MIME_TYPE,也就是图片的mime类型。还有一个是图片存储的路径,不过这个值在Android 10和之前的系统版本中的处理方式不一样。Android 10中新增了一个RELATIVE_PATH常量,表示文件存储的相对路径,可选值有DIRECTORY_DCIM、DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSIC等,分别表示相册、图片、电影、音乐等目录。而在之前的系统版本中并没有RELATIVE_PATH,所以我们要使用DATA常量(已在Android 10中废弃),并拼装出一个文件存储的绝对路径才行。

    有了ContentValues对象之后,接下来调用ContentResolver的insert()方法即可获得插入图片的Uri。但仅仅获得Uri仍然是不够的,我们还需要向该Uri所对应的图片写入数据才行。调用ContentResolver的openOutputStream()方法获得文件的输出流,然后将Bitmap对象写入到该输出流当中即可。

    以上代码即可实现将Bitmap对象存储到手机相册当中,那么有些朋友可能会问了,如果我要存储的图片并不是Bitmap对象,而是一张网络上的图片,或者是当前应用关联目录下的图片该怎么办呢?

    其实方法都是相似的,因为不管是网络上的图片还是关联目录下的图片,我们都能获取到它的输入流,只要不断读取输入流中的数据,然后写入到相册图片所对应的输出流当中就可以了,示例代码如下:

    fun writeInputStreamToAlbum(inputStream: InputStream, displayName: String, mimeType: String) {
        val values = ContentValues()
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
        values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
        } else {
            values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
        }
        val bis = BufferedInputStream(inputStream)
        val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        if (uri != null) {
            val outputStream = contentResolver.openOutputStream(uri)
            if (outputStream != null) {
                val bos = BufferedOutputStream(outputStream)
                val buffer = ByteArray(1024)
                var bytes = bis.read(buffer)
                while (bytes >= 0) {
                    bos.write(buffer, 0 , bytes)
                    bos.flush()
                    bytes = bis.read(buffer)
                }
                bos.close()
            }
        }
        bis.close()
    }
    

    这段代码中只是将输入流和输出流的部分重新编写了一下,其他部分和之前存储Bitmap的代码是完全一致的,相信很好理解。

    Demo效果:

    好了,这样我们就将相册图片的读取和存储问题都解决了,下面我们来探讨另外一个常见的需求,如何将文件下载到Download目录。

    下载文件到Download目录

    执行文件下载操作是一个很常见的场景,比如说下载pdf、doc文件,或者下载APK安装包等等。在过去,这些文件我们通常都会下载到Download目录,这是一个专门用于存放下载文件的目录。而从Android 10开始,我们已经不能以绝对路径的方式访问外置存储空间了,所以文件下载功能也会受到影响。

    那么该如何解决呢?主要有以下两种方式。

    第一种同时也是最简单的一种方式,就是更改文件的下载目录。将文件下载到应用程序的关联目录下,这样不用修改任何代码就可以让程序在Android 10系统上正常工作。但使用这种方式,你需要知道,下载的文件会被计入到应用程序的占用空间当中,同时如果应用程序被卸载了,该文件也会一同被删除。另外,存放在关联目录下的文件只能被当前的应用程序所访问,其他程序是没有读取权限的。

    以上几个限制条件如果不能满足你的需求,那么就只能使用第二种方式,对Android 10系统进行代码适配,仍然将文件下载到Download目录下。

    其实将文件下载到Download目录,和向相册中添加一张图片的过程是差不多的,Android 10在MediaStore中新增了一种Downloads集合,专门用于执行文件下载操作。但由于每个项目下载功能的实现都各不相同,有些项目的下载实现还十分复杂,因此怎么将以下的示例代码融合到你的项目当中是你自己需要思考的问题。

    fun downloadFile(fileUrl: String, fileName: String) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            Toast.makeText(this, "You must use device running Android 10 or higher", Toast.LENGTH_SHORT).show()
            return
        }
        thread {
    		try {
    			val url = URL(fileUrl)
    			val connection = url.openConnection() as HttpURLConnection
    			connection.requestMethod = "GET"
    			connection.connectTimeout = 8000
    			connection.readTimeout = 8000
    			val inputStream = connection.inputStream
    			val bis = BufferedInputStream(inputStream)
    			val values = ContentValues()
    			values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
    			values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
    			val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
    			if (uri != null) {
    				val outputStream = contentResolver.openOutputStream(uri)
    				if (outputStream != null) {
    					val bos = BufferedOutputStream(outputStream)
    					val buffer = ByteArray(1024)
    					var bytes = bis.read(buffer)
    					while (bytes >= 0) {
    						bos.write(buffer, 0 , bytes)
    						bos.flush()
    						bytes = bis.read(buffer)
    					}
    					bos.close()
    				}
    			}
    			bis.close()
    		} catch(e: Exception) {
    			e.printStackTrace()
    		}
        }
    }
    

    这段代码总体来讲还是比较好理解的,主要就是添加了一些Http请求的代码,并将MediaStore.Images.Media改成了MediaStore.Downloads,其他部分几乎是没有变化的,我就不再多加解释了。

    注意,上述代码只能在Android 10或更高的系统版本上运行,因为MediaStore.Downloads是Android 10中新增的API。至于Android 9及以下的系统版本,请你仍然使用之前的代码来进行文件下载。

    Demo效果:


    使用文件选择器

    如果我们要读取SD卡上非图片、音频、视频类的文件,比如说打开一个PDF文件,这个时候就不能再使用MediaStore API了,而是要使用文件选择器。

    但是,我们不能再像之前的写法那样,自己写一个文件浏览器,然后从中选取文件,而是必须要使用手机系统中内置的文件选择器。示例代码如下:

    const val PICK_FILE = 1
    
    private fun pickFile() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.type = "*/*"
        startActivityForResult(intent, PICK_FILE)
    }
    
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            PICK_FILE -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val uri = data.data
                    if (uri != null) {
                        val inputStream = contentResolver.openInputStream(uri)
    					// 执行文件读取操作
                    }
                }
            }
        }
    }
    

    这里在pickFile()方法当中通过Intent去启动系统的文件选择器,注意Intent的action和category都是固定不变的。而type属性可以用于对文件类型进行过滤,比如指定成image/就可以只显示图片类型的文件,这里写成/*表示显示所有类型的文件。注意type属性必须要指定,否则会产生崩溃。

    然后在onActivityResult()方法当中,我们就可以获取到用户选中文件的Uri,之后通过ContentResolver打开文件输入流来进行读取就可以了。

    Demo效果:


    第三方SDK不支持作用域存储怎么办?

    阅读完了本篇文章之后,相信你对Android 10作用域存储的用法和适配基本上都已经掌握了。然而我们在实际的开发工作当中还可能会面临一个非常头疼的问题,就是我自己的代码当然可以进行适配,但是项目中使用的第三方SDK还不支持作用域存储该怎么办呢?

    这个情况确实是存在的,比如我之前使用的七牛云SDK,它的文件上传功能要求你传入的就是一个文件的绝对路径,而不支持传入Uri对象,大家应该也会碰到类似的问题。

    由于我们是没有权限修改第三方SDK的,因此最简单直接的办法就是等待第三方SDK的提供者对这部分功能进行更新,在那之前我们先不要将targetSdkVersion指定到29,或者先在AndroidManifest文件中配置一下requestLegacyExternalStorage属性。

    然而如果你不想使用这种权宜之计,其实还有一个非常好的办法来解决此问题,就是我们自己编写一个文件复制功能,将Uri对象所对应的文件复制到应用程序的关联目录下,然后再将关联目录下这个文件的绝对路径传递给第三方SDK,这样就可以完美进行适配了。这个功能的示例代码如下:

    fun copyUriToExternalFilesDir(uri: Uri, fileName: String) {
        val inputStream = contentResolver.openInputStream(uri)
        val tempDir = getExternalFilesDir("temp")
        if (inputStream != null && tempDir != null) {
            val file = File("$tempDir/$fileName")
            val fos = FileOutputStream(file)
            val bis = BufferedInputStream(inputStream)
            val bos = BufferedOutputStream(fos)
            val byteArray = ByteArray(1024)
            var bytes = bis.read(byteArray)
            while (bytes > 0) {
                bos.write(byteArray, 0, bytes)
                bos.flush()
                bytes = bis.read(byteArray)
            }
            bos.close()
            fos.close()
        }
    }
    

    好的,关于Android 10作用域存储的重要知识点就讲到这里,相信你已经可以完全掌握了。下篇文章中我们会继续学习Android 10适配,讲一讲深色主题的功能,详见链见: Android 10适配要点,深色主题

    注:本篇文章中演示的所有示例,都可以到ScopedStorageDemo这个开源库中找到其对应的源码。

    开源库地址是:https://github.com/guolindev/ScopedStorageDemo

    关于作用域存储在Android 11上的适配,请参考 Android 11新特性,Scoped Storage又有了新花样


    本篇文章是《第一行代码 第3版》的配套扩展文章,目前《第一行代码 第3版》已经出版,Kotlin、Jetpack、MVVM,你所关心的知识点都在这里,详情点击这里查看

    京东购买地址

    当当购买地址

    天猫购买地址


    关注我的技术公众号,每天都有优质技术文章推送。

    微信扫一扫下方二维码即可关注:

    展开全文
  • Android 10正式版发布,看看都有哪些新特性

    万次阅读 多人点赞 2019-09-04 11:04:23
    谷歌在今年3月推出了Android 10.0的首个测试版,昨天,Android 10.0的正式版正式向外发布,而最先尝到新版本的自然是亲儿子Pixel手机...基于强大的多窗口支持,Android 10扩展了跨应用程序窗口的多任务处理,并在设...

    谷歌在今年3月推出了Android 10.0的首个测试版,昨天,Android 10.0的正式版正式向外发布,而最先尝到新版本的自然是亲儿子Pixel手机。

    新特性解读

    根据Android官网的介绍,Android 10.0将聚焦于隐私可控、手机自定义与使用效率,此版本主要带来了十大新特性。
    在这里插入图片描述

    创新与新体验

    可折叠

    基于强大的多窗口支持,Android 10扩展了跨应用程序窗口的多任务处理,并在设备折叠或展开时提供屏幕连续性来维护应用程序状态。有关如何优化可折叠应用程序的详细信息,请参阅开发人员指南

    5G 网络

    Android 10承诺提供持续更快的速度和更低的延迟,并增加了对5G的平台支持,并扩展了现有api,以帮助您利用这些增强。您可以使用连接性api来检测设备是否具有高带宽连接,并检查连接是否已计量。有了这些,你的应用程序和游戏可以为5G以上的用户定制丰富的身临其境的体验。

    Live Caption

    此功能将自动向视频、播客和音频消息添加说明文字。这些说明是实时性和系统性,因此它们不限于特定的应用程序。Live Caption 文本框可以调整大小并在屏幕周围移动。Live Caption 不仅对那些发现自己处于音频无法选择的情况下的用户很有帮助,而且对听力障碍者来说也非常有益。

    具体参考 https://youtu.be/YL-8Xfx6S5o

    智能回复通知

    使用机器学习来预测你在回复信息时可能会说些什么,这项功能在 Android P 中已经有提供,但仅限于谷歌专用的应用程序。Android 10 中,它已经内置到整个通知系统中,并且不仅提供对信息的回复建议,还可以获得建议的操作。比如,如果朋友要你出去吃饭,你的手机会建议你发送回应短信,并且它还会在 Google 地图中直接显示位置信息。此功能也适用于 Signal 等消息应用。
    在这里插入图片描述

    暗黑主题

    Android Q 引入的另一项新功能是新系统暗黑主题,它适用于 Android 系统 UI 和 Android 设备上运行的应用。暗黑主题为开发人员带来许多好处,比如能够降低功耗、对于低视力和对强光敏感的用户来说可以提高屏幕内容可见度。
    在这里插入图片描述

    手势导航

    引入手势导航后,应用程序不仅可以实现全屏幕的内容显示,还能最大限度地减少系统导航键的可见程度,这对于当下主流的全面屏手机尤为重要。
    在这里插入图片描述

    用户可以从左下角或右下角斜向滑动以启动 Google Assistant 助手,在触发的角落会有相应的 “小手柄” 指示器来作为视觉提示。

    另外,团队还为包含导航抽屉(Navigation Drawer)的应用增加了一个名为 peek 的动作:用户轻压屏幕边缘然后再向内划动,便可拉出应用抽屉页面。所有版本的 DrawerLayout 均提供 peek 支持,其中以 DrawerLayout 1.1.0-alpha02 的体验最优。

    在这里插入图片描述

    隐私与安全

    隐私是Android 10的核心关注点,从平台中更强的保护到考虑隐私的新功能。在之前版本的基础上,Android 10在保护隐私和给用户控制权方面做了广泛的改变,改进了系统UI,更严格的权限,并限制了数据应用程序的使用。有关如何在应用程序中支持这些功能的详细信息,请参阅Android 10开发人员站点

    位置数据控制

    用户可以通过一个新的权限选项更好地控制他们的位置数据——他们现在可以允许一个应用程序只在应用程序实际使用时(在前台运行)访问位置。对于大多数应用程序来说,这提供了足够的访问权限,而对于用户来说,这在透明度和控制方面是一个很大的改进。要了解更多关于位置更改的信息,请参阅开发人员指南我们的博客文章

    保护网络位置数据

    大多数用于扫描网络的api已经需要粗定位权限。Android 10增加了对这些api的保护,转而要求良好的位置权限,具体参考Android 10 保护网络位置数据

    防止设备跟踪

    应用程序不能再访问可用于跟踪的不可重置设备标识符,包括设备IMEI、序列号和类似标识符。默认情况下,当连接到Wi-Fi网络时,设备的MAC地址也是随机的。阅读最佳实践,以帮助您为用例选择正确的标识符,并在这里查看详细信息。

    保护外部存储数据

    Android 10引入了一些变化,让用户可以更好地控制外部存储中的文件和应用程序数据。应用程序可以将自己的文件存储在私有沙箱中,但必须使用MediaStore访问共享的媒体文件,并使用系统文件选择器访问新下载集合中的共享文件。点击链接了解更多

    阻止不必要的干扰

    Android 10防止应用程序从后台启动,而后台的应用程序会出人意料地跳到前台,并从另一个应用程序转移注意力。

    安全

    在Android上,我们一直在评估系统的安全性。通过和第三方的手机安全平台合作,我们分析师研究如Gartner的2019年5月手机操作系统和设备的安全,一个比较的Android平台报告(需要订阅),得分最高的评级在26个30类别,提前在多个点从网络安全身份验证和恶意软件保护。
    在Android 10中,我们引入了更多的特性,通过在加密、平台强化和身份验证方面的改进来确保用户的安全。

    存储加密

    所有兼容Android 10的设备都需要加密用户数据,为了提高安全效率,Android 10使用了我们的新加密模式Adiantum

    TLS 1.3

    Android 10默认情况下支持TLS 1.3,这是对TLS标准的一个重大修订,具有性能优势和增强的安全性。

    平台硬化

    Android 10还包括对该平台几个安全关键领域的增强,以及对BiometricPrompt框架的更新,该框架在隐式和显式身份验证中都提供了对人脸和指纹的健壮支持。点击这里阅读更多关于Android 10安全更新的信息。

    相机与多媒体

    照片动态深度

    应用程序现在可以请求一个动态深度图像,它由一个JPEG、XMP元数据(与深度相关的元素相关)和一个嵌入在同一文件中的深度和置信度图组成。动态深度是生态系统的一种开放格式,我们正在与合作伙伴合作,将其引入运行Android 10或更高版本的设备中。
    在这里插入图片描述

    音频播放捕获

    现在,任何播放音频的应用程序都可以让其他应用程序使用新的音频回放捕获API捕获其音频流。除了启用标题和副标题之外,该API还允许您支持流行的用例,比如实时流媒体游戏。我们在构建这个新功能时考虑到了隐私和版权保护,因此一个应用程序捕捉另一个应用程序的音频的能力受到了限制。请阅读我们的博客文章

    音频和视频编解码器

    Android 10增加了对开源视频编解码器AV1的支持,它允许媒体供应商使用更少的带宽向Android设备传输高质量的视频内容。此外,Android 10支持使用Opus进行音频编码,Opus是一种开放的、免版税的编解码器,针对语音和音乐流媒体进行了优化,HDR10+用于支持Opus的设备上的高动态范围视频。

    本地MIDI 接口

    对于用c++执行音频处理的应用程序,Android 10引入了一个本地MIDI API,通过NDK与MIDI设备通信。该API允许使用非阻塞读取在音频回调中检索MIDI数据,从而支持对MIDI消息进行低延迟处理。在这里用示例应用程序和源代码试一试。

    Vulkan

    Vulkan 1.1现在是所有运行Android 10或更高版本的64位设备的必备版本,也是所有32位设备的推荐版本。我们已经在生态系统中看到了支持Vulkan的强大势头——在运行Android N或更高版本的设备中,超过半数支持Vulkan 1.0.3或更高版本。随着Android 10的新要求,我们预计在未来的一年里,Android的使用率会进一步上升。

    连接优化

    改进的点对点和互联网连接

    我们对Wi-Fi堆栈进行了重构,以提高隐私和性能,还改进了一些常见的用例,比如管理物联网设备和建议互联网连接——而不需要位置许可。网络连接api使得通过本地Wi-Fi管理物联网设备变得更加容易,可以实现配置、下载或打印等对等功能。网络建议api允许应用程序在互联网连接方面显示用户更喜欢的Wi-Fi网络。

    无线模式

    应用程序现在可以通过启用高性能和低延迟模式来请求自适应Wi-Fi。如果低延迟对用户体验非常重要,比如实时游戏、活动语音呼叫和类似的用例,那么这将是一个巨大的优势。该平台配合设备固件工作,以满足最低功耗的要求。

    系统优化

    ART优化

    ART运行时的改进可以帮助您的应用程序启动得更快、消耗更少的内存、运行得更流畅——而不需要您做任何工作。由谷歌Play提供的艺术简介,让艺术在运行之前就预先编译应用程序的部分。在运行时,分代垃圾收集使垃圾收集在时间和CPU方面更高效,减少jank,并帮助应用程序在低端设备上更好地运行。

    在这里插入图片描述

    神经网络1.2

    我们添加了60个新操作,包括ARGMAX、ARGMIN、量化LSTM,以及一系列性能优化。这为加速更大范围的模型奠定了基础,比如用于目标检测和图像分割的模型。我们正在与硬件供应商和流行的机器学习框架(如TensorFlow)合作,优化和推出对NNAPI 1.2的支持。

    更快更流行

    通过Android 10,我们将继续专注于更快地将新平台引入设备,与我们的设备制造商和高通(Qualcomm)等硅合作伙伴密切合作。Treble项目发挥了关键作用,帮助我们将18个合作伙伴设备以及8个像素设备纳入今年的Beta测试项目,比去年增加了一倍多。更棒的是,我们预计这些设备将在今年年底前获得官方的Android 10更新,我们正在与几家合作伙伴合作推出其他新的旗舰产品和更新。我们已经看到Android 10的强大势头,在未来的几个月里,将有比以往任何Android版本更多的设备获得这个新版本。

    Android 10也是第一个支持Project Mainline(官方名称为谷歌Play system updates)的版本,这是我们的新技术,用于保护Android用户,并通过重要的代码更改保持他们的设备的新鲜——直接来自谷歌Play。通过谷歌播放系统更新,我们可以在所有运行Android 10或更高版本的设备上更新特定的内部组件,而不需要设备制造商进行完整的系统更新。我们希望在接下来的几个月里为消费者设备带来第一次更新。

    对于开发人员来说,我们希望Android 10中的这些更新能够促进平台实现在不同设备上的一致性,并随着时间的推移带来更大的一致性,从而降低开发和测试成本。

    应用开发跟进Android 10

    现在,随着今天Android 10的公开发布和设备更新即将到来,我们要求所有Android开发人员尽快更新您当前的应用程序,以保证兼容性,让您的用户顺利过渡到Android 10。

    下面,我们给出开发的几点建议:

    Android 10上安装应用

    从谷歌Play将当前应用程序安装到运行Android 10或模拟器的像素或其他设备上,然后进行测试。你的应用程序应该看起来很棒,运行良好,功能齐全,并能正确处理Android 10的所有行为变化。观察隐私更改、手势导航、对仿生库的动态链接器路径的更改等方面的影响。

    测试Android 10的隐私功能

    测试的内容包括新的位置权限、范围存储、对后台活动启动的限制、对数据和标识符的更改等等。要开始查看顶级隐私更改清单,并查看隐私更改文档以了解更多测试领域。

    测试受限制的非sdk接口

    测试应用程序中的库和sdk

    如果发现问题,请尝试更新到最新版本的SDK,或者向SDK开发人员寻求帮助。

    更新和发布兼容的应用程序

    当您完成测试并进行任何更新时,我们建议您立即发布兼容的应用程序。当用户更新到Android 10时,这将帮助您向他们提供一个平稳的过渡。

    让应用程序经过测试并为新版本的Android做好准备,对于整个生态系统中更快的平台更新是至关重要的,所以如果可能的话,请优先考虑Android 10的适配工作。

    体验链接

    • 模拟器
      https://developer.android.google.cn/studio/run/managing-avds.html

    • Android 10 的各项行为变更
      https://developer.android.google.cn/about/versions/10/behavior-changes-all

    • 隐私变更
      https://developer.android.google.cn/about/versions/10/privacy/changes

    • 手势导航
      https://developer.android.google.cn/guide/navigation/gesturenav

    • 生物验证库的动态链接路径变化
      https://developer.android.google.cn/about/versions/10/behavior-changes-all#bionic

    • 新的位置权限
      https://developer.android.google.cn/about/versions/10/privacy/changes#app-access-device-location

    • 分区储存
      https://developer.android.google.cn/about/versions/10/privacy/changes#scoped-storage

    • 从后台启动 activity
      https://developer.android.google.cn/about/versions/10/privacy/changes#background-activity-starts

    • 关于数据和设备识别符方面的变更
      https://developer.android.google.cn/about/versions/10/privacy/changes#data-ids

    • 隐私特性清单
      https://developer.android.google.cn/about/versions/10/privacy#top-privacy-changes

    • 行为变更文档
      https://developer.android.google.cn/about/versions/10/privacy/changes

    • 《非 SDK 接口在 Android 10 中的受限情况出现变化》
      https://developer.android.google.cn/about/versions/10/non-sdk-q

    Android 10 新功能和APIs

    我们推荐每个应用程序都可以尝试下如下的一些功能:

    • 黑暗主题:通过添加一个dark主题或启用Force dark,为启用系统范围的dark主题的用户提供一致的体验。
    • 手势导航:在你的应用程序中支持手势导航,从边缘到边缘,并确保你的自定义手势是系统导航手势的补充。
    • 折叠优化:通过优化可折叠设备,为当今的创新设备提供无缝体验。

    链接

    • 深色主题
      https://developer.android.google.cn/guide/topics/ui/look-and-feel/darktheme
    • Force Dark 功能
      https://developer.android.google.cn/guide/topics/ui/look-and-feel/darktheme#force_dark
    • 手势导航
      https://developer.android.google.cn/guide/navigation/gesturenav
    • 针对折叠屏为应用进行优化
      https://developer.android.google.cn/guide/topics/ui/foldables

    除此之外,我们也推荐以下内容:

    • 互动通知:如果您的通知包含消息,请在通知中启用智能回复,以吸引用户并让他们立即采取行动。
    • 生物识别技术:如果您使用生物特征验证,请移动到BiometricPrompt,这是在现代设备上支持指纹验证的首选方法。
    • 音频播放捕获:要支持字幕或游戏录制,请在应用程序中启用音频回放捕捉功能——这是接触更多用户并使应用程序更易访问的好方法。
    • 编解码器:对于媒体应用程序,可以尝试AV1用于视频流,HDR10+用于高动态范围的视频。对于语音和音乐流,可以使用Opus编码,对于音乐家,可以使用本地MIDI API。
    • 网络api优化:如果您的应用程序通过Wi-Fi管理物联网设备,请尝试使用新的网络连接api来实现配置、下载或打印等功能。

    链接

    • 通知内的智能回复及建议操作
      https://developer.android.google.cn/about/versions/10/features#smart-suggestions
    • BiometricPrompt
      https://developer.android.google.cn/training/sign-in/biometric-auth
    • 音频回放捕捉功能
      https://developer.android.google.cn/preview/features/playback-capture
    • AV1
      https://en.wikipedia.org/wiki/AV1
    • Opus
      http://opus-codec.org/
    • HDR 10+
      https://en.wikipedia.org/wiki/High-dynamic-range_video#HDR10+
    • 原生 MIDI API
      https://developer.android.google.cn/ndk/guides/audio/midi
    • 网络连接 API
      https://developer.android.google.cn/guide/topics/connectivity/wifi-bootstrap

    参考链接:

    Welcoming Android 10!

    android官网

    Android 10 开发者官网
    Android Studio 3.5 稳定版

    按照操作步骤

    展开全文
  • Android 10 完美适配

    万次阅读 2020-10-30 15:09:24
    2019 年 9 月 3 日,Google 发布了 Android 10 正式版。Android 10 聚焦移动创新、安全隐私和数字健康三大主题,全面打造最佳的用户体验。 在Android 10 版本中,官方的改动较大,相应的开发者适配成本还是很高的。...

    背景

    2019 年 9 月 3 日,Google 发布了 Android 10 正式版。Android 10 聚焦移动创新、安全隐私和数字健康三大主题,全面打造最佳的用户体验。

    在Android 10 版本中,官方的改动较大,相应的开发者适配成本还是很高的。基于前期调研,我们主要基于以下几方面进行Android 10的适配:

    • Android X
    • 分区存储
    • 设备ID
    • 明文HTTP限制

    1. AndroidX

    AndroidX 对原始 Android Support库进行了重大改进,后者现在已不再维护。AndroidX 软件包完全取代了支持库,不仅提供同等的功能,而且提供了新的库。

    1.1 什么是AndroidX

    Android 系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功,因此也不可能在一开始的时候就将它的 API 考虑的非常周全。随着 Android 系统版本不断地迭代更新,每个版本中都会加入很多新的 API 进去,但是新增的 API 在老版系统中并不存在,因此这就出现了一个向下兼容的问题。

    于是 Android 团队推出了一个鼎鼎大名的 Android Support Library,用于提供向下兼容的功能。比如我们熟知的support-v4 库,appcompat-v7 库都是属于 Android Support Library 的。4在这里指的是 Android API 版本号,对应的系统版本是1.6。support-v4 的意思就是这个库中提供的 API 会向下兼容到 Android 1.6 系统,它对应的包名为 android.support.v4.app。类似地,appcompat-v7 指的是将库中提供的 API 向下兼容至 API 7,也就是 Android 2.1 系统,它对应的包名为 android.support.v7.app。可以发现,Android Support Library 中提供的库,它们的包名都是以 **android.support.***开头的。

    但是随着时间的推移,Android1.6、Android2.1 系统早已被淘汰了,现在 Android 官方支持的最低系统版本已经是 4.0.1,对应的 API 版本号是 15。support-v4、appcompat-v7 库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。

    Android 团队也意识到这种命名已经非常不合适了,于是对这些 API 的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX 本质上其实就是对 Android Support Library 进行的一次升级。升级内容主要在于以下两个方面。

    1. 包名:之前 Android Support Library 中的 API,它们的包名都是在**android.support.***下面的,而 AndroidX 库中所有 API 的包名都变成了在 **androidx.* **下面。这是一个很大的变化,意味着以后凡是 android.* 包下面的 API 都是随着 Android 操作系统发布的,而 androidx.* 包下面的 API 都是随着扩展库发布的,这些 API 基本不会依赖于操作系统的具体版本。

    2. 命名规则:吸取了之前命名规则的弊端,AndroidX 所有库的命名规则里都不会再包含具体操作系统 API 的版本号了。比如,像 appcompat-v7 库,在 AndroidX 中就变成了 appcompat 库。

    AndroidX 的依赖库格式如下所示:

    implementation 'androidx.appcompat:appcompat:1.2.0'
    

    总的来说,AndroidX 并不是什么全新的东西,只不过是对 Android Support Library 的一次升级,因此 AndroidX 上手起来也没有任何困难的地方,比如经常使用的 RecyclerView、ViewPager 等等库,在 AndroidX 中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化,但是有一点需要注意,AndroidX 和 Android Support Library 中的库是非常不建议混合在一起使用的,因为它们可能会产生很多不兼容的问题。最好的做法是,要么全部使用 AndroidX 中的库,要么全部使用 Android Support Library 中的库。

    现在 Android Support Library 已经不再建议使用,并会慢慢停止维护,未来都会为 AndroidX 为主。另外,从Android Studio 3.4.2开始,新建的项目已经默认使用 AndroidX 架构了。

    1.2 迁移方法

    那么对于老项目的迁移应该怎么办呢?由于涉及到了包名的改动,如果从 Android Support Library 升级到AndroidX 需要手动去改每一个文件的包名,那可真得要改死了。为此,Android Studio 提供了一个一键迁移的功能,只需要对着你的项目名右击 → Refactor → Migrate to AndroidX,就会弹出如下图所示的窗口。在这里插入图片描述

    这里点击Migrate,Android Studio 就会自动检查你项目中所有使用 Android Support Library 的地方,并将它们全部改成 AndroidX 中对应的库。另外 Android Studio 还会将你原来的项目备份成一个zip文件,这样即使迁移之后的代码出现了问题你还可以随时还原回之前的代码。

    2. 分区存储

    2.1 背景介绍

    为了更好的保护用户数据并限制设备冗余文件增加,以 Android 10(API 级别 29)以及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储), 对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件。

    Android Q 在外部存储设备中为每个应用提供了一个“隔离存储沙盒”。任何其他应用都无法直接访问您应用的沙盒文件。由于文件是您应用的私有文件,因此您不再需要任何权限即可在外部存储设备中访问和保存自己的文件。此变更可让您更轻松地保证用户文件的隐私性,并有助于减少应用所需的权限数量。应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用需要访问存放在应用的专有目录以及 MediaStore 之外的文件,否则最好使用分区存储。

    要点:

    • Android Q 文件存储机制修改成了沙盒模式
    • APP 只能访问自己目录下的文件和公共媒体文件
    • Android Q 版本以下机型,还是使用老的文件存储方式
    • Android Q 及以上版本机型,所有应用均需要分区存储, 所以应用需要提前确保支持分区存储

    需要注意:在适配 AndroidQ 的时候还要兼容 Q 系统版本以下的,使用 SDK_VERSION 区分。

    2.2 新特性概览

    外部存储被分为应用私有目录以及共享目录两个部分:

    • 应用私有目录:存储应用私有数据,外部存储应用私有目录对应Android/data/packagename,内部存储应用私有目录对应data/data/packagename;
    • 共享目录:存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、 Music、Movies、Download等目录。

    1)私有目录

    应用私有目录文件访问方式与之前Android版本一致,可以通过File path获取资源。

    2)共享目录

    共享目录文件需要通过 MediaStore API 或者 Storage Access Framework 方式访问。

    • MediaStore API 在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限
    • MediaStore API 访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过 ContentResolver 查询不到文件 Uri,即使通过其他方式获取到文件 Uri,读取或创建文件会抛出异常;
    • MediaStore API 不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过 Storage Access Framework 方式访问;

    2.3 受影响的变更

    2.3.1 图片位置信息

    一些图片会包含位置信息,因为位置对于用户属于敏感信息, Android 10 应用在分区存储模式下图片位置信息默认获取不到,应用通过以下两项设置可以获取图片位置信息:

    • 在manifest中申请ACCESS_MEDIA_LOCATION
    • 调用MediaStore setRequireOriginal(Uri uri)接口更新图片Uri

    2.3.2 访问数据

    MediaStore.Files 应用分区存储模式下,MediaStore.Files 集合只能够获取媒体文件信息(图片、音频、视频), 获取不到非media(pdf、office、doc、txt等)文件。

    2.3.3 File Path路径访问受影响接口

    开启分区存储新特性, Andrioid 10 不能够通过File Path路径直接访问共享目录下资源,以下接口通过File 路径操作文件资源,功能会受到影响,应用需要使用 MediaStore 或者 SAF 方式访问。

    1. File:createNewFile() 、delete() 、renameTo(File dest) 、mkdir() 、mkdirs() 。

    2. FileInputStream:FileInputStream(File file) 、FileInputStream(String name) 。

    3. FileOutputStream:FileOutputStream(String name) 、FileOutputStream(String name , boolean append) 、FileOutputStream(File file) 、FileOutputStream(File file , boolean append) 。

    4. BitmapFactory:decodeFile(String pathName) 、decodeFile(String pathName , Options opts) 。

    2.4 兼容模式

    应用未完成外部存储适配工作,可以临时以兼容模式运行, 兼容模式下应用申请存储权限,即可拥有外部存储完整目录访问权限,通过 Android10 之前文件访问方式运行,通过以下方式设置应用以兼容模式运行。

    tagretSDK 大于等于 Android 10(API level 29), 在 AndroidManifest中设置 requestLegacyExternalStorage 属性为true,代码如下所示:

    <manifest ...>
    ...
    <application android:requestLegacyExternalStorage="true" ... >
    ...
    </manifest>
    

    备注:应用已完成存储适配工作且已打开分区存储开关,如果当前应用以兼容模式运行,覆盖安装后应用仍然会以兼容模式运行,卸载重新安装应用才会以分区存储模式运行。

    2.5 适配方案

    2.5.1 方案概览

    分区存储适配包含文件迁移以及文件访问兼容性适配两个部分:

    1)文件迁移

    文件迁移是将应用共享目录文件迁移到应用私有目录或者 Android10 要求的 media 集合目录。

    • 针对只有应用自己访问并且应用卸载后允许删除的文件,需要迁移文件到应用私有目录文件,可以通过 File path 方式访问文件资源,降低适配成本。
    • 允许其他应用访问,并且应用卸载后不允许删除的文件,文件需要存储在共享目录,应用可以选择是否进行目录整改,将文件迁移到 Android10 要求的 media 集合目录。

    2)文件访问兼容性

    共享目录文件不能够通过 File path 方式读取,需要使用 MediaStore API 或者S torage Access Framework 框架进行访问。

    2.5.2 适配指导

    Android Q 中使用 ContentResolver 进行文件的增删改查。

    1)获取(创建)私有目录下的文件夹,

    1. 内部存储私有目录

    /storage/emulated/0/Android/data/{package name}/files/test

    //在外部存储私有目录下创建test文件夹
    File testFile = context.getExternalFilesDir("test");
    
    1. 外部存储私有目录

    /data/data/{package name}/files/test

    //在内部存储私有目录下创建test文件夹
      File testFile = new File(getFilesDir(), "test");
      if (!testFile.exists()) {
          testFile.mkdirs();
      }
    

    2)创建外部存储私有目录文件

    1. 通过输出流创建文件并写入数据:

    /storage/emulated/0/Android/data/{package name}/files/test/test1.txt

    String testFile = getExternalFilesDir("test").getAbsolutePath();
    File newFile = new File(testFile + File.separator + "test1.txt");
    OutputStream os = null;
    try {
        os = new FileOutputStream(newFile);
        if (os != null) {
            os.write("test1.txt file is created".getBytes(StandardCharsets.UTF_8));
            os.flush();
        }
    } catch (IOException e) {
    
    } finally {
        try {
            if (os != null) {
                os.close();
            }
        } catch (IOException e1) {
        }
    }
    
    1. 通过输入流获取数据
    File newFile = new File(apkFilePath + File.separator + "test1.txt");
    InputStream in = null;
    try {
        in = new FileInputStream(newFile);
        if (in != null) {
            int available = in.available();
            byte[] bytes = new byte[available];
            int len = 0;
            StringBuffer sb = new StringBuffer();
            while ((len = in.read(bytes)) != -1) {
                sb.append(new String(bytes, 0, len));
            }
            System.out.println("result  = " + sb.toString());
        }
    } catch (IOException e) {
    
    } finally {
        try {
            if (in != null) {
                in.close();
            }
        } catch (IOException e1) {
        }
    }
    
    1. 内部存储私有目录文件的创建、数据读取与外部存储私的方式一样,这里就不累赘了。

    3)创建共享目录文件夹

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        ContentResolver resolver = getContentResolver();
        ContentValues values = new ContentValues();
        values.put(MediaStore.Downloads.DISPLAY_NAME, "fileName");
        //设置文件类型
        values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
        //注意MediaStore.Downloads.RELATIVE_PATH需要targetVersion=29,
        //故该方法只可在Android10的手机上执行
        values.put(MediaStore.Downloads.RELATIVE_PATH, "Download" + File.separator + "apk");
        Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
        Uri insertUri = resolver.insert(external, values);
    } else {
        // ...
    }
    

    4)在共享目录指定文件夹下创建文件

    主要是在公共目录下创建文件或文件夹,拿到本地路径Uri,不同的Uri,可以保存到不同的公共目录中。接下来使用输入输出流就可以写入文件。

    重点:Android Q 中不支持 file://类型访问文件,只能通过 Uri 方式访问。

    /**
     * 创建图片地址Uri,用于保存拍照后的照片 Android 10以后使用这种方法
     */
    private Uri createImageUri() {
        String status = Environment.getExternalStorageState();
        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
        if (status.equals(Environment.MEDIA_MOUNTED)) {
            return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
        } else {
            return getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
        }
    }
    
    1. 保存或者下载文件到共享目录,如 Download,MIME_TYPE类型可以自行参考对应的文件类型,这里只对APK作出说明

      @RequiresApi(api = Build.VERSION_CODES.Q)
      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;
          }
          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 {
             if(os !=null){
                 try {
                     os.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
             if(is!=null){
                 try {
                     is.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
          }
      }
      
    2. 保存图片相关

      /**
       * 通过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) {
          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 is = null;
          OutputStream os = null;
          boolean result = false;
          try {
              is = 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 = is.read(buffer)) != -1) {
                      os.write(buffer, 0, len);
                  }
                  os.flush();
              }
              result = true;
          } catch (IOException e) {
              result = false;
          } finally {
              if(os !=null){
                  try {
                      os.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              if(is!=null){
                  try {
                      is.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
          return result;
      }
      

    5)通过 MediaStore API 读取公共目录下的文件

      Cursor imageCursor = getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[4] + " DESC");
    if (cursor != null && cursor.moveToFirst()) {
        do {
    
            int _id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
            Uri imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, _id);
    
        } while (!cursor.isLast() && cursor.moveToNext());
    } else {
    
    }
    
    // 通过uri获取bitmap
    public Bitmap getBitmapFromUri(Context context, Uri uri) {
        ParcelFileDescriptor parcelFileDescriptor = null;
        FileDescriptor fileDescriptor = null;
        Bitmap bitmap = null;
        try {
            parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
            if (parcelFileDescriptor != null && parcelFileDescriptor.getFileDescriptor() != null) {
                fileDescriptor = parcelFileDescriptor.getFileDescriptor();
                //转换uri为bitmap类型
                bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if (parcelFileDescriptor != null) {
                    parcelFileDescriptor.close();
                }
            }catch (IOException e) {
                }
            }
            return bitmap;
        }
    

    6)使用 MediaStore 删除文件

    getContentResolver().delete(fileUri, null, null);
    

    7)判断共享目录文件是否存在,自 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 {
            afd = cr.openAssetFileDescriptor(Uri.parse(path), "r");
            if (afd == null) {
                return false;
            }
        } catch (FileNotFoundException e) {
            return false;
        }finally {
           if(afd !=null){
               try {
                   afd.close();
               } catch (IOException e) {
                   
               }
           }
        }
        return true;
    }
    

    3. 设备ID

    从Android 10 开始已经无法完全标识一个设备,曾经用mac地址、IMEI等设备信息标识设备的方法,从 Android 10 开始统统失效。而且无论你的 APP 是否适配过 Android 10。

    3.1 IMEI等设备信息

    从Android10 开始普通应用不再允许请求 android.permission.READ_PHONE_STATE 权限。而且,无论你的 App是否适配过 Android 10(既targetSdkVersion是否大于等于29),均无法再获取到设备 IMEI 等设备信息。

    受影响的API:

    Build.getSerial();
    TelephonyManager.getImei();
    TelephonyManager.getMeid()
    TelephonyManager.getDeviceId();
    TelephonyManager.getSubscriberId();
    TelephonyManager.getSimSerialNumber();
    
    • targetSdkVersion<29 的应用,其在获取设备ID时,会直接返回null或者unknown。
    • targetSdkVersion>=29 的应用,其在获取设备ID时,会直接抛出异常SecurityException。

    如果您的App希望在Android 10以下的设备中仍然获取设备IMEI等信息,可按以下方式进行适配:

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

    3.2 Mac地址随机分配

    从Android10开始,默认情况下,在搭载 Android 10 或更高版本的设备上,系统会传输随机分配的 MAC 地址。(即从Android 10开始,普通应用已经无法获取设备的真正mac地址,标识设备已经无法使用mac地址)。

    3.3 如何标识设备唯一性

    3.3.1 Google解决方案:如果您的应用有追踪非登录用户的需求,可用ANDROID_ID来标识设备。

    • ANDROID_ID生成规则:签名+设备信息+设备用户
    • ANDROID_ID重置规则:设备恢复出厂设置时,ANDROID_ID将被重置
    String androidId = Settings.Secure.getString(this.getContentResolver(),
                    Settings.Secure.ANDROID_ID);
    

    3.3.2 信通院统一SDK(OAID)

    请参考 http://www.msa-alliance.cn/col.jsp?id=120 进行获取。

    4. 明文HTTP限制

    当 SDK 版本大于 API 28 时,默认限制了 HTTP 请求,并出现相关日志

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

    该问题有两种解决方案:

    1)在 AndroidManifest.xml 中 Application 节点添加如下代码

    <application android:usesCleartextTraffic="true">
    

    2)在 res 目录新建 xml 目录,已建的跳过,在xml目录新建一个network_security_config.xml文件,然后在AndroidManifest.xml 中 Application 添加如下节点代码。

    android:networkSecurityConfig="@xml/network_security_config"
    

    network_security_config.xml

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

    该问题有两种解决方案:

    1)在 AndroidManifest.xml 中 Application 节点添加如下代码

    <application android:usesCleartextTraffic="true">
    

    2)在 res 目录新建 xml 目录,已建的跳过,在xml目录新建一个network_security_config.xml文件,然后在AndroidManifest.xml 中 Application 添加如下节点代码。

    android:networkSecurityConfig="@xml/network_security_config"
    

    network_security_config.xml

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

    扫描下方二维码关注公众号,获取更多技术干货。

    在这里插入图片描述

    展开全文
  • Android 10 适配攻略

    万次阅读 多人点赞 2020-02-26 11:20:52
    相比较去年的写的Android 9适配,这次Android 10的内容有点多。没想到写了我整整两天,吐血中。。。

    相比较去年的写的Android 9适配,这次Android 10的内容有点多。没想到写了我整整两天,吐血中。。。

    在这里插入图片描述

    准备工作

    老规矩,首先将我们项目中的targetSdkVersion改为 29。

    1.Scoped Storage(分区存储)

    说明

    在Android 10之前的版本上,我们在做文件的操作时都会申请存储空间的读写权限。但是这些权限完全被滥用,造成的问题就是手机的存储空间中充斥着大量不明作用的文件,并且应用卸载后它也没有删除掉。为了解决这个问题,Android 10 中引入了Scoped Storage 的概念,通过添加外部存储访问限制来实现更好的文件管理。

    首先明确一个概念,外部储存和内部储存。

    • 内部储存:/data 目录。一般我们使用getFilesDir()getCacheDir() 方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。

    • 外部储存:/storage/mnt 目录。一般我们使用getExternalStorageDirectory()方法获取的路径来存取文件。

    因为不同厂商、系统版本的原因,所以上述的方法并没有一个固定的文件路径。了解了上面的概念,那我们所说的外部储存访问限制,可以认为是针对getExternalStorageDirectory()路径下的文件。具体的规则如下表:
    在这里插入图片描述
    上图将外部存储空间分为了三部分:

    • 特定目录(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()) {
              
        }
    

    这样的好处是你可以在用户升级后,能方便的将用户的数据移动至应用的特定目录。否则你只能通过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的视频,推荐观看

    准备好使用分区存储

    2.权限变化

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

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

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

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

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

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

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

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

    1. 首先在清单中对应的service中添加 android:foregroundServiceType="location"
     	<service
            android:name="MyNavigationService"
            android:foregroundServiceType="location" ... >
            ...
        </service>
    
    1. 启动前台服务前检查是否具有前台的访问权限:
    	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_CALLSECRET_CODE_ACTION。应用可在广播发送几秒钟后启动 Activity。

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

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

    当然你也可以申请相应权限或者白名单:
    在这里插入图片描述

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

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

    	<uses-permission android:name="android.permission.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 屏幕中每个像素都是自主发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭来达到省电的效果。

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

    • 让所有人都可以在光线较暗的环境中更轻松地使用设备。

    适配方法有两种:

    1.手动适配(资源替换)

    官方文档中提到的继承Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight的方法,但这只是将我们使用的各种View的默认样式进行了适配,并不太适用于实际项目的适配。因为具体的项目中的View都按照设计的风格进行了重定义。

    其实适配的方法很简单,类似屏幕适配、国际化的操作,并不需要继承上面的主题。比如你要修改颜色,就在res 下新建 values-night目录,创建对应的colors.xml文件。将具体要修改的色值定义在里面。图标之类的也是一个思路,创建对应的 drawable-night目录。

    只要你之前的代码不是硬编码且代码规范,那么适配起来还是很轻松。

    2.自动适配(Force Dark)

    Android 10 提供 Force Dark 功能。一如其名,此功能可让开发者快速实现深色主题背景,而无需明确设置 DayNight 主题背景。

    如果您的应用采用浅色主题背景,则 Force Dark 会分析应用的每个视图,并在相应视图在屏幕上显示之前,自动应用深色主题背景。有些开发者会混合使用 Force Dark 和本机实现,以缩短实现深色主题背景所需的时间。

    应用必须选择启用 Force Dark,方法是在其主题背景中设置 android:forceDarkAllowed="true"。此属性会在所有系统及 AndroidX 提供的浅色主题背景(例如 Theme.Material.Light)上设置。使用 Force Dark 时,您应确保全面测试应用,并根据需要排除视图。

    如果您的应用使用Dark Theme主题(例如Theme.Material),则系统不会应用 Force Dark。同样,如果应用的主题背景继承自 DayNight 主题(例如Theme.AppCompat.DayNight),则系统不会应用 Force Dark,因为会自动切换主题背景。

    您可以通过 android:forceDarkAllowed 布局属性或 setForceDarkAllowed(boolean) 在特定视图上控制 Force Dark。

    上述内容我直接照搬文档的说明。总结一下,使用Force Dark需要注意几点:

    • 如果使用的是 DayNightDark Theme 主题,则设置forceDarkAllowed 不生效。

    • 如果有需要排除适配的部分,可以在对应的View上设置forceDarkAllowed为false。

    这里说说我实际使用此方法的感受:整体还是不错的,设置的色值会自动取反。但也因此颜色不受控制,能否达到预期效果是个需要注意的问题。追求快速适配可以采取此方案。


    手动切换主题

    使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)方法,其中参数mode有以下几种:

    • 浅色 - MODE_NIGHT_NO
    • 深色 - MODE_NIGHT_YES
    • 由省电模式设置 - MODE_NIGHT_AUTO_BATTERY
    • 系统默认 - MODE_NIGHT_FOLLOW_SYSTEM

    下面的代码是官方Demo中的使用示例:

    public class ThemeHelper {
    
        public static final String LIGHT_MODE = "light";
        public static final String DARK_MODE = "dark";
        public static final String DEFAULT_MODE = "default";
    
        public static void applyTheme(@NonNull String themePref) {
            switch (themePref) {
                case LIGHT_MODE: {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                    break;
                }
                case DARK_MODE: {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                    break;
                }
                default: {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
                    } else {
                        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
                    }
                    break;
                }
            }
        }
    }
    

    通过AppCompatDelegate.getDefaultNightMode()方法,可以获取到当前的模式,这样便于代码中去适配。

    监听深色主题是否开启

    首先在清单文件中给对应的Activity配置 android:configChanges="uiMode"

    	<activity
        	android:name=".MyActivity"
        	android:configChanges="uiMode" />
    

    这样在onConfigurationChanged方法中就可以获取:

    	@Override
        public void onConfigurationChanged(@NonNull Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
            switch (currentNightMode) {
                case Configuration.UI_MODE_NIGHT_NO:
                    // 关闭
                    break;
                case Configuration.UI_MODE_NIGHT_YES:
                    // 开启
                    break;
                default:
                    break;    
            }
        }
    

    详细的内容你可以参看官方文档官方Demo

    判断深色主题是否开启

    其实和上面onConfigurationChanged方法同理:

        public static boolean isNightMode(Context context) {
            int currentNightMode = context.getResources().getConfiguration().uiMode & 
            	Configuration.UI_MODE_NIGHT_MASK;
            return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
        }
    
    

    5.标识符和数据

    对不可重置的设备标识符实施了限制

    受影响的方法包括:

    从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能正常使用以上这些方法。

    如果你的应用没有该权限,却仍然使用了以上的方法,则返回的结果会因目标 SDK 版本而异:

    • 如果应用以 Android 10 或更高版本为目标平台,则会发生 SecurityException
    • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException

    这项改动表示第三方应用无法获取Device ID这类唯一标识。如果你需要唯一标识符,请参阅文档:唯一标识符的最佳做法

    当然你也可以试试移动安全联盟(MSA)联合多家厂商共同开发的统一补充设备标识调用SDK。据说还有点不稳定,因为我暂时还没有尝试过,所以不做评价。

    限制了对剪贴板数据的访问权限

    除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。

    对启用和停用 WLAN 实施了限制

    以 Android 10 或更高版本为目标平台的应用无法启用或停用 WLAN。WifiManager.setWifiEnabled()方法始终返回 false。

    如果您需要提示用户启用或停用 WLAN,请使用设置面板

    6.其他

    最后,点赞鼓励一下~~

    参考

    展开全文
  • Android 10正式版发布

    万次阅读 2019-09-04 10:09:47
    Android10的亮点 Android 10主要有三大亮点 Android 10走在移动创新技术的前沿,具有先进的机器学习技术,同时也支持新兴设备,例如折叠屏设备和5G设备。 Android 10主要提升了隐私性和安全性,使用了接近50...
  • Android10(Android Q) 适配

    千次阅读 2020-03-27 13:41:05
    先适配Android X 然后继续适配Android10
  • android10之前imei唯一标识一个设备,我们用这个值做业务逻辑上的处理。 方案: Android 10+取消了获取IMEI的API,这就直接导致我们的程序相关功能不能再Android10设备上的使用,经过查找资料以及官方的建议,...
  • Android 10适配要点,深色主题

    万次阅读 多人点赞 2020-05-12 22:17:58
    在不久之前,我才发布了一篇Android 10适配的文章,讲的是作用域存储的相关内容。 而除了作用域存储之外,深色主题也是Android 10中的一大亮点,并且是需要开发者进行适配的。因此本篇文章我们就来探讨关于深色主题...
  • Android 调用相机拍照,适配到Android 10

    千次阅读 多人点赞 2019-09-18 11:39:37
    这是一个很常用的功能,并且这个功能在Android6.0、7.0、10.0等版本上实现都有所不同,需要对Android各个版本进行兼容适配,目前最新的Android版本是Android 10,所以这篇博客适配到Android 10。 调用相机,首先...
  • 树莓派4b安装Android10

    千次阅读 热门讨论 2020-10-10 20:22:20
    【更新】树莓派4b安装Android10 下载 由于镜像过大,我把Android10的压缩包上传上去 https://cloud.189.cn/t/FjqAJjjiMni2 而烧录工具我在上一篇博客中说过balenaetcher 然后呢我把官网也给出来官网 烧录镜像的话我...
  • Android 10(Android Q) 适配

    千次阅读 2019-09-23 11:50:51
    Android10中, 系统不允许普通App请求android.permission.READ_PHONE_STATE权限, 故新版App需要取消该动态权限的申请。 当前获取设备唯一ID的方式为使用SSAID, 若获取为空的话则使用UUID.randomUUID...
  • Android 10 网络权限

    千次阅读 2020-04-28 17:59:13
    Android 10 中,只在AndroidManifest.xml中使用: app不能访问网络。 需要在application中添加 android:usesCleartextTraffic=“true” 。例: “ ” 这样才能访问网络,刚开始没注意,略坑! ...
  • Android 10分区存储

    千次阅读 2020-02-12 21:40:16
    Android 10分区存储介绍及百度APP适配实践
  • android10 适配方案

    千次阅读 2019-10-21 21:37:00
    android10 之前,我们通常使用设备的deviceId来当作设备的唯一标识,这在开发中已经算是一个共识了,但是在android10之后呢,系统对于deviceId做了限制,也就是说我们拿不到设备id了。我们该怎样另寻他法而又要...
  • Android10 文件存储

    千次阅读 2020-03-17 15:52:15
    文章目录概述存储图像至沙盒沙盒中加载图像存储图像至...该篇代码部分是学习了 Android Q(10) 文件存储适配 后根据自身需要进行相应改写而成 以下内容基于Android 10(Q),即 targetSdkVersion > 28 的应用 A...
  • Android Q 正式命名为 Android 10

    千次阅读 2019-08-23 18:02:20
    根据官方博文,谷歌已经公布了 Android Q 的名称,它并不是想以前一样,以甜食命名,也不是以任何以字母 Q 开头来命名,而是简单称它为 Android 10。 该公司表示,它正在改变其发布版本的命名方式,以推动更大程度的...
  • Android 锁定屏幕方向 横向或竖向 支持Android10前言解决方法自定义style修改Activity修改AndroidManifest.xml完事 前言 显然,你应该知道百度搜出来的锁定屏幕方向的教程都太老了,在Android10 上完全没有用,或者...
  • Android 10 创建文件失败

    千次阅读 2020-04-16 18:08:32
    Android 10 改变了文件的存储方式不允许应用随意创建文件夹了,要用安卓提供的文件夹。 Android 10提供了一些文件夹: 例如:Context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),这个路径是在SD卡...
  • 从Android5.0到Android10 一、Android51.ANDROID 5.0 行为变更: 声音和振动 (1)如果您当前使用 Ringtone、MediaPlayer 或 Vibrator 类向通知中添加声音和振动,则移除此代码,以便系统可以在“优先”模式中...
  • android个版本对应的SDK level,最新包括android10.0 版本昵称 ... Android10 10 API level 29 Pie 9 API level 28 ...
  • android 10文件存储兼容

    千次阅读 2020-04-27 15:22:03
    Android Q 推送已经一段时间,手上几部测试机也已经升级到android 10 系统。google 对用户隐私是越来越上心了。简单介绍下适配10系统分区存储。 看图 如图10系统兼容脑图。包括兼容方案,延时兼容处理,兼容完成...
  • Android10创建文件Permission denied

    千次阅读 2020-03-09 14:46:33
    Android 10 创建文件一直报错:Permission denied; 原来 Android 10 改变了文件的存储方式 可以在Androidmainfest 里面的application添加 android:requestLegacyExternalStorage="true" ...
  • Android10 黑色主题 适配

    千次阅读 2020-03-13 09:14:19
    # Android10 黑色主题 适配 AndroidTenAdaptiveDemo 适配思路: 跟随系统设置自动适配DarkTheme 应用系统级自动适配 应用App内手动切换 白天黑夜模式 配置适配方案 自行适配 # 跟随系统设置自动适配DarkTheme ...
  • Android 10 加载手机本地图片

    千次阅读 多人点赞 2019-09-18 11:30:28
    Android 10由于文件权限的关系不能使用图片路径直接加载手机储存卡内的图片,除非图片是在应用的私有目录下,所以在Android 10以后,下面的代码无法加载图片。 Bitmap bitmap = BitmapFactory.decodeFile(path); ...
  • hook activity 兼容android 10 Q 未完待续

    万次阅读 2020-05-23 19:31:14
    android10 和Q以下的机型上如何hook startActivity函数并且实现我们想要的功能,例如本例中是启动了一个activity2 ,但是经过我们hook之后实际上启动的是activity3. 源码分析简陋,想要代码的同学请直达github 想要...
  • Android 获取IMEI(Android 10以下可用)

    万次阅读 2019-07-09 17:49:47
    1、获取手机IMEI,从android 5.0之后通过getImei获取 public static String getIMEI(Context context){ String imei = ""; try { TelephonyManager tm = (TelephonyManager) context.getSystemService...
  • 开始以为是该机型特有的问题,随后偶然看到其为Android10,这也是目前可供测试的唯一一部手机,考虑到安卓日益细分、严格的权限控制。我打开了 Android 10 隐私变更 ,终于解脱了。 原因 涉及到蓝牙功能如下,可以...
  • Android 10 开机自启策略

    千次阅读 2021-01-09 15:22:27
    Android10中, 当App的Activity不在前台时,其启动Activity会被系统拦截,导致无法启动。 这导致使用静态注册开机广播后,实现开机自启的方式不能用了。 解决方案:向用户申请SYSTEM_ALERT_WINDOW权限,系统就不会...
  • Android 10 创建文件夹问题

    千次阅读 2020-03-27 10:42:46
    https://blog.csdn.net/yanmantian/article/details/103975257?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task ...Android 10 改变了文件的存...
  • 适配Android10分区存储 Android10是分区存储的过度版本,不建议开启分区存储。我们可以android:requestLegacyExternalStorage="true"来关闭Andorid10的分区存储。 <application android:...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 67,387
精华内容 26,954
关键字:

android10