【Android 10 适配】隐私权限变更

更详细内容请参考 Android 10 中的隐私权变更

Android 10(API 级别 29)引入了多项功能和行为变更,旨在更好地保护用户的隐私。这些变更让用户更清楚地了解并更好地控制自己的数据及为应用提供的权能。

下面是 Android 10 中与隐私权限相关的主要变更。

1. 外部存储访问权限范围限定为应用文件和媒体【分区存储】

默认情况下,以 Android 10 及更高版本为目标平台【targetSdkVersion 30】的应用对外部存储空间进行分区访问(即分区存储)。此类应用可以查看外部存储设备内以下类型的文件,而无需请求任何与存储相关的用户权限:

  • 特定于应用的目录中的文件(使用 getExternalFilesDir() 访问)。
  • 应用创建的照片、视频和音频片段(通过媒体库访问)。

在 Android 10 及以上版本中常见存储类型的具体路径示例:

外部私有目录(应用专属目录):
内部存储:/storage/emulated/0/Android/data/包名/files/
外部存储:/storage/emulated/0/Android/data/包名/files/

外部公共目录(共享存储区域):
图片目录:/storage/emulated/0/Pictures/
音频目录:/storage/emulated/0/Music/
视频目录:/storage/emulated/0/Movies/
下载目录:/storage/emulated/0/Download/

getExternalFilesDir() 的使用

val fileDir = getExternalFilesDir(null)
if (fileDir != null) {
    val fileName = "example.txt"
    val file = File(fileDir, fileName)

    try {
        // 写入文件
        file.writeText("Hello, World!")

        // 读取文件
        val content = file.readText()
        Log.d("getExternalFilesDir", content)
    } catch (e: IOException) {
        e.printStackTrace()
    }
} else {
    Log.d("getExternalFilesDir", "Failed to get external files directory.")
}

保存应用创建的照片

fun createExternalStoragePrivatePicture() {
        // 在应用程序私有目录中创建一个图片路径,我们将放置我们自己的图片。
        // 注意,我们实际上不需要将图片放在 DIRECTORY_PICTURES 中,
        // 因为媒体扫描程序将会看到这些目录中的所有媒体文件;
        // 但是还有其他的媒体文件类型,例如 DIRECTORY_MUSIC,所以将媒体文件分类展示有利于用户交互。
        val path = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File(path, "DemoPicture.jpg")

        try {
            val inputStream = resources.openRawResource(R.drawable.add_ic)
            val outputStream = FileOutputStream(file)
            val data = ByteArray(inputStream.available())
            inputStream.read(data)
            outputStream.write(data)
            inputStream.close()
            outputStream.close()

            // 告诉媒体扫描程序有关新文件的信息,以便立即对用户可用。
            MediaScannerConnection.scanFile(this,
                arrayOf(file.toString()), null){ p, uri ->
                Log.d("ExternalStorage", "已扫描路径:$p")
                Log.d("ExternalStorage", "-> uri=$uri")
            }
        } catch (e: IOException) {
            // 无法创建文件,可能是因为外部存储当前未挂载。
            Log.d("ExternalStorage", "写入文件时出错:$file", e)
        }
    }

访问共享存储空间中的媒体文件

更多详细内容请参考
访问共享存储空间中的媒体文件

fun getAllExternalImages() :List<Uri>{
    val collection = MediaStore.Images.Media.getContentUri("external")
    val projection = arrayOf(
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME,
        MediaStore.Images.Media.DATE_TAKEN
    )
    val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"

    val query = contentResolver?.query(
        collection,
        projection,
        null,
        null,
        sortOrder
    )

    val uriList = mutableListOf<Uri>()

    query?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)

        while (cursor.moveToNext()) {
            val id = cursor.getLong(idColumn)
            val contentUri = ContentUris.withAppendedId(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                id
            )
            uriList.add(contentUri)
        }
    }

    return uriList
}

使用照片选择器

更详尽内容请参考
照片选择器
注意:为了简化照片选择器的集成,请添加 1.7.0 版或更高版本的 androidx.activity 库。

选择单个媒体项
// 注册一个照片选择器活动启动器,以单选模式运行。
val pickMedia = registerForActivityResult(PickVisualMedia()) { uri ->
    // 在用户选择媒体项目或关闭照片选择器后调用回调。
    if (uri != null) {
        Log.d("PhotoPicker", "Selected URI: $uri")
    } else {
        Log.d("PhotoPicker", "No media selected")
    }
}

// 启动照片选择器,并允许用户选择图像和视频。
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))

// 启动照片选择器,并仅允许用户选择图像。
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))

// 启动照片选择器,并仅允许用户选择视频。
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))

// 启动照片选择器,并仅允许用户选择特定 MIME 类型的图像/视频,例如 GIF。
val mimeType = "image/gif"
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.SingleMimeType(mimeType)))
选择多个媒体项
// 注册一个多选模式的照片选择器活动启动器。
// 在此示例中,应用程序允许用户选择最多5个媒体文件。
val pickMultipleMedia =
        registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
    // 用户选择媒体项目或关闭照片选择器后调用回调。
    if (uris.isNotEmpty()) {
        Log.d("PhotoPicker", "选择的项目数量:${uris.size}")
    } else {
        Log.d("PhotoPicker", "未选择媒体文件")
    }
}

// 启动照片选择器,并允许用户选择图像和视频。
pickMultipleMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))

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

Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限。

与 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限不同,ACCESS_BACKGROUND_LOCATION 权限仅会影响应用在后台运行时对位置信息的访问权限。

除非符合以下条件之一,否则应用会被视为在后台访问位置信息:

  • 属于该应用的 activity 可见。
  • 该应用运行的某个前台服务已声明前台服务类型为 location。

应用在后台访问位置信息需要设置ACCESS_BACKGROUND_LOCATION 权限

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

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

    

    <application>
        
    application>

manifest>

如果应用创建并监控地理围栏,并以 Android 10(API 级别 29)或更高版本为目标平台,那么也必须声明 ACCESS_BACKGROUND_LOCATION 权限。

地理围栏解释 地理围栏

以 Android 9 或更低版本为目标平台时自动授予访问权限

对于目标平台为 Android 9 或更低版本的应用,在 Android 10 或更高版本上运行时, 如果你在应用的清单文件中声明了 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限,系统会在安装过程中自动为 ACCESS_BACKGROUND_LOCATION 添加相应的权限声明。

也就是说,尽管你的应用的目标平台是 Android 9 或更低版本,但在 Android 10 或更高版本上运行时,系统会自动处理 ACCESS_BACKGROUND_LOCATION 权限的授予,无需额外的代码或处理。

但对于目标平台为 Android 10 或更高版本的应用,仍然需要显式请求 ACCESS_BACKGROUND_LOCATION 权限。

3. 标识符和数据

更多内容请参考 标识符和数据

应用程序无法直接访问 /proc/net

从 Android 10 开始,应用程序无法直接访问 /proc/net 文件系统,其中包含与设备的网络状态相关的信息。这是出于安全和隐私的考虑,以防止应用程序获取敏感的网络信息。

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

从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包括 IMEI 和序列号)。

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

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

4. 摄像头和连接性

对访问摄像头详情和元数据的权限实施了限制

Android 10 更改了 getCameraCharacteristics() 方法默认返回的信息的广度。具体而言,您的应用必须具有 CAMERA 权限才能访问此方法的返回值中可能包含的设备特定元数据。

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

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

如果您需要提示用户启用或停用 Wi-Fi,请使用下面的方式打开设置面板:

fun openWifiSetting() {
    val intent = Intent(Settings.ACTION_WIFI_SETTINGS)
    startActivity(intent)
}

对直接访问已配置的 Wi-Fi 网络实施了限制

详见 官网

如果您的应用以 Android 10 或更高版本为目标平台,并且它不是系统应用或 DPC,则以下方法不会返回有用数据:

  • getConfiguredNetworks() 方法始终返回一个空列表。
  • 每个返回整数值的网络操作方法(addNetwork() 和 updateNetwork())始终返回 -1。
  • 每个返回布尔值的网络操作(removeNetwork()、reassociate()、enableNetwork()、disableNetwork()、reconnect() 和 disconnect())始终返回 false。

可选方式:

  1. 如需触发与 Wi-Fi 网络的即时本地连接,请在标准 NetworkRequest 对象中使用 WifiNetworkSpecifier。

详情请参考官网
注意 :使用此 API 创建连接并不会提供与应用或设备的互联网连接。如需为设备上的应用提供互联网连接,请改用 Wi-Fi Suggestion API。

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiNetworkSpecifier
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    private val TAG = "WifiNetworkSpecifier"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取 ConnectivityManager 实例
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        // 创建 NetworkRequest 对象
        val requestBuilder = NetworkRequest.Builder()

        // 设置网络类型为 Wi-Fi
        requestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)

        // 创建 WifiNetworkSpecifier 对象
        val specifier = WifiNetworkSpecifier.Builder()
            .setSsid("Your_WiFi_SSID") // 设置要连接的 Wi-Fi 网络的 SSID
            .build()

        // 将 WifiNetworkSpecifier 添加到 NetworkRequest 中
        requestBuilder.setNetworkSpecifier(specifier)

        // 注册网络回调
        connectivityManager.requestNetwork(
            requestBuilder.build(),
            object : ConnectivityManager.NetworkCallback() {
                override fun onAvailable(network: Network) {
                    // 当网络可用时触发
                    Log.d(TAG, "Network available")

                    // 可以使用返回的 Network 对象进行网络操作
                    // ...
                }

                override fun onUnavailable() {
                    // 当网络不可用时触发
                    Log.d(TAG, "Network unavailable")
                }
            })
    }
}
  1. 使用 Wi-Fi Suggestion API。

详情请参考官网
注意 :Wi-Fi 建议不是已保存的网络,也不会显示在“已保存的网络”页面中。

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiNetworkSuggestion
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    private val TAG = "WifiNetworkSuggestion"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取 ConnectivityManager 实例
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        // 创建 WifiNetworkSuggestion 列表
        val suggestionsList = mutableListOf<WifiNetworkSuggestion>()

        // 创建一个 WifiNetworkSuggestion 对象
        val suggestion = WifiNetworkSuggestion.Builder()
            .setSsid("Your_WiFi_SSID") // 设置要添加的 Wi-Fi 网络的 SSID
            .setWpa2Passphrase("Your_WiFi_Password") // 设置 Wi-Fi 网络的密码
            .build()

        // 将 WifiNetworkSuggestion 添加到列表中
        suggestionsList.add(suggestion)

        // 添加网络建议
        connectivityManager.addNetworkSuggestions(suggestionsList)

        // 注册网络回调
        connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: android.net.Network) {
                // 当网络可用时触发
                Log.d(TAG, "Network available")

                // 可以使用返回的 Network 对象进行网络操作
                // ...
            }

            override fun onLost(network: android.net.Network) {
                // 当网络丢失时触发
                Log.d(TAG, "Network lost")
            }
        })
    }

    override fun onDestroy() {
        super.onDestroy()

        // 移除网络建议
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        connectivityManager.removeNetworkSuggestions(emptyList())
    }
}

5. 权限

详情请见 官网
注意 :本部分介绍的每项变更都会影响搭载 Android 10 或更高版本的设备上的所有应用,甚至是以 Android 9(API 级别
28)或更低版本为目标平台的应用。

限制对屏幕内容的访问

为了保护用户的屏幕内容,Android 10 更改了 READ_FRAME_BUFFER、CAPTURE_VIDEO_OUTPUT 和 CAPTURE_SECURE_VIDEO_OUTPUT 权限的作用域,从而禁止以静默方式访问设备的屏幕内容。从 Android 10 开始,这些权限只能通过签名访问。

需要访问设备屏幕内容的应用应使用 MediaProjection API,此 API 会显示提示,要求用户同意声明。

使用 MediaProjection API 请求屏幕捕获权限:

import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    private val REQUEST_MEDIA_PROJECTION = 1
    private lateinit var mediaProjectionManager: MediaProjectionManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

        // 请求屏幕捕获权限
        startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == REQUEST_MEDIA_PROJECTION && resultCode == RESULT_OK) {
            // 用户已授权屏幕捕获权限
            // 在这里可以开始捕获屏幕内容
            // ...
        }
    }
}

面向用户的权限检查(针对旧版应用)

如果您的应用以 Android 5.1(API 级别 22)或更低版本为目标平台,当用户首次在搭载 Android 10 或更高版本的设备上使用您的应用时,会看到权限屏幕。通过该界面,用户可以撤消系统先前在安装应用时授予应用的权限。

身体活动识别

Android 10 针对需要检测用户步数或对用户的身体活动(例如步行、骑车或坐车)进行分类的应用引入了 android.permission.ACTIVITY_RECOGNITION 运行时权限。此项权限旨在让用户了解设备传感器数据在“设置”中的使用方式。

从界面中移除了权限组

从 Android 10 开始,应用无法在界面中查询权限的分组方式。

Thank you for your reading, best regards!

你可能感兴趣的:(Android,日新月异,android)