应用在运行时,向用户索取(相机、存储)等权限,未同步告知权限申请的使用目的,不符合相关法律法规要求--教你如何解决华为市场上架难题
作为android移动端开发者,在上架华为市场时候,都遇到过这一个问题。
应用在运行时,向用户索取(相机、存储)等权限,未同步告知权限申请的使用目的,不符合相关法律法规要求。
此要求在23年左右时候上架,其他平台均未有此要求,因此增加了app的开发量,没有经验的开发者都会不知道如何修改,华为官方给了如下方案:
修改建议:
APP在申请敏感权限时,应同步说明权限申请的使用目的,包括但不限于申请权限的名称、服务的具体功能、用途;告知方式不限于弹窗、蒙层、浮窗、或者自定义操作系统权限弹框等,且权限申请使用目的说明不应自动消失。请排查应用内所有的权限申请行为,确保不存在类似问题。 您可参考《审核指南》第7.21项:https://developer.huawei.com/consumer/cn/doc/app/50104-07#h3-1683701612940-2 APP常见个人信息保护问题FAQ您可参考:https://developer.huawei.com/consumer/cn/doc/app/FAQ-faq-05#h1-1698326401789-0 【权限申请说明同步告知】修改指导赋能帖:https://developer.huawei.com/consumer/cn/forum/topic/0208158494714878699?fid=0102104600515103427
例子如下图:
我在同博客上,也看到了一个老哥23年分享的一个很标准的答案,这里我下面给出文章地址和作者名字,但是老哥给出的版本是java的,我在他的基础上优化成了kotlin版本,并且写法进行了更加完善的优化。
老哥账号名字:夢鑰
文章地址:app上架-您的应用在运行时,未同步告知权限申请的使用目的,向用户索取(相机)等权限,不符合华为应用市场审核标准。_app在申请敏感权限时,应同步说明权限申请的使用目的,包括但不限于申请权限的名称-CSDN博客
下面的是我的优化方案:
我优化了部分逻辑,将java改成kotlin。并且不依赖于snackbar,防止你使用的东西与snackbar冲突。增加了关于弹窗时间的控制,可以选择控制时间为无限亦或者固定时间。此外我还新增了图片部分,你也可以选择有图片或者无图片(隐藏)。
SnackBarUtil.kt
package com.help10000.rms.ui.utilsimport android.app.Activity
import android.os.Build
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.help10000.rms.Robject SnackBarUtil {private var currentSnackbar: Snackbar? = null// 新增imageResId参数:用于设置ImageView的src(默认0表示不显示图片)fun show(activity: Activity,view: View,msg: String,tip: String,duration: Int = Snackbar.LENGTH_INDEFINITE,imageResId: Int = 0 // 图片资源ID,如R.mipmap.ddr) {try {dismiss() // 关闭之前的提示val snackbar = Snackbar.make(view, "", duration)val snackbarView = snackbar.viewif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {snackbarView.elevation = 0f}snackbarView.setBackgroundColor(ContextCompat.getColor(activity, android.R.color.transparent))snackbarView.setPadding(0, 0, 0, 0)val statusBarHeight = getStatusBarHeight(activity)val flp = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT).apply {gravity = Gravity.CENTER or Gravity.TOPtopMargin = statusBarHeight}snackbarView.layoutParams = flp// 加载布局并设置控件内容val inflate = LayoutInflater.from(activity).inflate(R.layout.snack_bar_layout, null)// 设置标题和提示文本inflate.findViewById<TextView>(R.id.snacl_bar_title).text = msginflate.findViewById<TextView>(R.id.snacl_bar_tip).text = tip// 设置图片(根据传入的imageResId动态修改)val imageView = inflate.findViewById<ImageView>(R.id.snacl_bar_image)if (imageResId != 0) {imageView.visibility = View.VISIBLEimageView.setImageResource(imageResId) // 动态设置图片资源} else {imageView.visibility = View.GONE // 不传入图片时隐藏}(snackbarView as ViewGroup).addView(inflate)snackbar.show()currentSnackbar = snackbar} catch (e: Exception) {e.printStackTrace()}}fun dismiss() {currentSnackbar?.dismiss()currentSnackbar = null}private fun getStatusBarHeight(activity: Activity): Int {var result = 0val resourceId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")if (resourceId > 0) {result = activity.resources.getDimensionPixelSize(resourceId)}return result}
}
snack_bar_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="vertical"><androidx.cardview.widget.CardViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="@dimen/dp_15"android:layout_marginRight="@dimen/dp_15"android:layout_marginBottom="@dimen/dp_10"android:background="@drawable/line_gradient_bg_shape"app:cardCornerRadius="@dimen/dp_5"app:cardElevation="@dimen/dp_3"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="14dp"android:layout_marginEnd="14dp"android:orientation="horizontal"><ImageViewandroid:id="@+id/snacl_bar_image"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_enter_order02"android:layout_gravity="center"android:layout_margin="15dp"/><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:id="@+id/snacl_bar_title"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:text="标题"android:textColor="@color/black"android:textSize="19sp" /><TextViewandroid:id="@+id/snacl_bar_tip"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:layout_marginRight="15dp"android:text="提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示提示"android:textColor="@color/black"android:textSize="16sp" /></LinearLayout></LinearLayout></androidx.cardview.widget.CardView>
</FrameLayout>
如何使用分为三个板块:
1.activity下使用
创建方法
private fun showPermissionLoading() {val rootView = findViewById<View>(android.R.id.content)SnackBarUtil.show(activity = this,view = rootView,msg = "拍照权限使用说明",tip = "拍照权限将帮助您用于更改个人头像、完善工单信息、完善订单信息",imageResId = R.mipmap.sczp)}
然后直接调用
showPermissionLoading()
1)注意因为我没有设置时间,所以默认是无限长,所以你在使用无限长的时候获取完权限后要调用
SnackBarUtil.dismiss()
在其他异常也同样如此,比如:
// 检查相机权限并启动扫码private fun checkPermissionAndCamera() {showPermissionLoading()if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED) {// 没有权限,申请requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)} else {SnackBarUtil.dismiss()// 已授权,直接开始扫码openCamera()}} else {SnackBarUtil.dismiss()// Android 5.x 及以下系统,无需权限申请,直接开始扫码openCamera()}}
2)或者你直接设置个时间,加个参数:
duration = 2000
单位是毫秒,时间你自己定,怎么加?直接加咯
private fun showPermissionLoading() {val rootView = findViewById<View>(android.R.id.content)SnackBarUtil.show(activity = this,view = rootView,msg = "拍照权限使用说明",duration = 2000,//注意单位是毫秒好吗tip = "拍照权限将帮助您用于更改个人头像、完善工单信息、完善订单信息",imageResId = R.mipmap.sczp)}
2.Fragment
创建方法
private fun showPermissionLoading() {rootView?.let {SnackBarUtil.show(activity = requireActivity(),view = it,msg = "拍照权限使用说明",tip = "拍照权限将帮助您用于更改个人头像、完善工单信息、完善订单信息",imageResId = R.mipmap.sczp)}}
然后直接调用
showPermissionLoading()
不设置时间,在关闭时候调用SnackBarUtil.dismiss()。
3.自己封装的utils下使用
创建方式
这次你要自己获取activity和rootView
val activity = context as? Activityval rootView = activity?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0)
SnackBarUtil.show(activity = activity,view = rootView,msg = "位置授权使用说明",tip = "位置授权将帮助您用于上报位置信息给公司、规划导航路线、保障账号安全、方便您查看附近工单。",imageResId = R.mipmap.ddr)
例如我自己封装出来的定位
package com.help10000.rms.utilsimport android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Looper
import android.util.Log
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.amir.common.utils.LogUtils
import com.amir.common.utils.PermissionUtils
import com.help10000.rms.R
import com.help10000.rms.ui.activitys.OrderActivity
import com.tencent.map.geolocation.TencentLocation
import com.tencent.map.geolocation.TencentLocationListener
import com.tencent.map.geolocation.TencentLocationManager
import com.tencent.map.geolocation.TencentLocationRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume// 导入你的SnackBarUtil
import com.help10000.rms.ui.utils.SnackBarUtilobject LocationManager {private var hasRequestedPermission = falseprivate var permissionResultCallback: ((Boolean) -> Unit)? = nullprivate var locationSuccessCallback: (() -> Unit)? = nullfun resetPermissionState() {hasRequestedPermission = falsepermissionResultCallback = nulllocationSuccessCallback = null}fun onRequestPermissionsResult(grantResults: IntArray, permissions: Array<out String>) {val hasFineLocation = permissions.contains(android.Manifest.permission.ACCESS_FINE_LOCATION) &&grantResults[permissions.indexOf(android.Manifest.permission.ACCESS_FINE_LOCATION)] == PackageManager.PERMISSION_GRANTEDval hasCoarseLocation = permissions.contains(android.Manifest.permission.ACCESS_COARSE_LOCATION) &&grantResults[permissions.indexOf(android.Manifest.permission.ACCESS_COARSE_LOCATION)] == PackageManager.PERMISSION_GRANTEDval isGranted = hasFineLocation || hasCoarseLocationpermissionResultCallback?.invoke(isGranted)permissionResultCallback = null}suspend fun getLocation(context: Context, onSuccess: (() -> Unit)? = null): Pair<String, String> {locationSuccessCallback = onSuccessreturn getLocationInternal(context)}// 仅展示关键修改部分private suspend fun getLocationInternal(context: Context): Pair<String, String> {val activity = context as? Activity ?: run {LogUtils.E("定位失败:上下文不是Activity")return Pair("0", "0")}val rootView = activity.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)val requiredPermissions = mutableListOf<String>().apply {addAll(LOCATION_PERMISSIONS)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isNeedBackgroundLocation()) {add(ACCESS_BACKGROUND_LOCATION)}}.toTypedArray()if (checkPermissions(context, requiredPermissions)) {return getRealTimeLocation(context)}if (!hasRequestedPermission) {hasRequestedPermission = truereturn suspendCancellableCoroutine { continuation ->permissionResultCallback = { granted ->CoroutineScope(Dispatchers.Main).launch {// 权限请求关闭,直接调用工具类的dismiss()关闭SnackbarSnackBarUtil.dismiss()if (granted) {(context as? OrderActivity)?.loadData()val location = getRealTimeLocation(context)continuation.resume(location)} else {continuation.resume(Pair("0", "0"))}}}// 发起权限请求前显示Snackbar(工具类内部会管理实例)try {SnackBarUtil.show(activity = activity,view = rootView,msg = "位置授权使用说明",tip = "位置授权将帮助您用于上报位置信息给公司、规划导航路线、保障账号安全、方便您查看附近工单。",imageResId = R.mipmap.ddr)} catch (e: Exception) {e.printStackTrace()}PermissionUtils.checkReadPermission(requiredPermissions,PermissionUtils.REQUEST_LOCATION,activity)// 协程取消时也关闭Snackbarcontinuation.invokeOnCancellation {SnackBarUtil.dismiss()}}} else {return Pair("0", "0")}}private suspend fun getRealTimeLocation(context: Context): Pair<String, String> {return suspendCancellableCoroutine { continuation ->val locationManager = TencentLocationManager.getInstance(context) ?: run {// 2. 在当前作用域内重新获取rootView(关键修复)val activity = context as? Activityval rootView = activity?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0)if (activity != null && rootView != null) {
// SnackBarUtil.show(
// activity = activity,
// view = rootView,
// msg = "定位服务异常",
// tip = "无法初始化定位服务,请稍后重试"
// )} else {Toast.makeText(context, "定位服务初始化失败", Toast.LENGTH_SHORT).show()}continuation.resume(Pair("0", "0"))return@suspendCancellableCoroutine}val request = TencentLocationRequest.create().apply {interval = 0isAllowGPS = truerequestLevel = TencentLocationRequest.REQUEST_LEVEL_GEO}val listener = object : TencentLocationListener {override fun onStatusUpdate(provider: String?, status: Int, message: String?) {locationManager.removeUpdates(this)if (!continuation.isCancelled) {LogUtils.E("定位状态异常:$message")// 3. 在此作用域内重新获取rootViewval activity = context as? Activityval rootView = activity?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0)if (activity != null && rootView != null) {
// SnackBarUtil.show(
// activity = activity,
// view = rootView,
// msg = "定位失败",
// tip = "状态异常:$message"
// )}continuation.resume(Pair("0", "0"))}}override fun onLocationChanged(location: TencentLocation?, errorCode: Int, message: String?) {locationManager.removeUpdates(this)if (continuation.isCancelled) {LogUtils.E("定位已取消,不处理结果")return}if (location != null && errorCode == TencentLocation.ERROR_OK) {val latValid = location.latitude.isValid()val lngValid = location.longitude.isLongitudeValid()if (latValid && lngValid) {val result = Pair(location.latitude.toString(),location.longitude.toString())locationSuccessCallback?.invoke()continuation.resume(result)} else {// 4. 重新获取rootViewval activity = context as? Activityval rootView = activity?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0)if (activity != null && rootView != null) {
// SnackBarUtil.show(
// activity = activity,
// view = rootView,
// msg = "定位无效",
// tip = "无法获取有效位置信息",
// 2000
// )}continuation.resume(Pair("0", "0"))}} else {// 5. 重新获取rootViewval activity = context as? Activityval rootView = activity?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0)if (activity != null && rootView != null) {
// SnackBarUtil.show(
// activity = activity,
// view = rootView,
// msg = "定位失败",
// tip = "请检查网络和定位设置",
// 2000
// )}continuation.resume(Pair("0", "0"))}}private fun Double.isValid(): Boolean {return this >= -90.0 && this <= 90.0}private fun Double.isLongitudeValid(): Boolean {return this >= -180.0 && this <= 180.0}}val requestCode = locationManager.requestSingleFreshLocation(request, listener, Looper.getMainLooper())if (requestCode < 0) {LogUtils.E("定位请求发起失败,code=$requestCode")locationManager.removeUpdates(listener)// 6. 重新获取rootViewval activity = context as? Activityval rootView = activity?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0)if (activity != null && rootView != null) {
// SnackBarUtil.show(
// activity = activity,
// view = rootView,
// msg = "定位请求失败",
// tip = "无法发起定位请求,请重试",
// 2000
// )}continuation.resume(Pair("0", "0"))}continuation.invokeOnCancellation {locationManager.removeUpdates(listener)}}}fun getLocationForJS(context: Context, callback: (String) -> Unit) {CoroutineScope(Dispatchers.IO).launch {val (lat, lng) = getLocationInternal(context)val json = if (lat != "0" && lng != "0") {"""{"code":1,"msg":"实时定位成功","result":{"lng":"$lng","lat":"$lat"}}"""} else {"""{"code":0,"msg":"实时定位失败","result":{"lng":"0","lat":"0"}}"""}withContext(Dispatchers.Main) {callback(json)}}}private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {return permissions.all {ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED}}private fun isNeedBackgroundLocation(): Boolean {return false}private val LOCATION_PERMISSIONS = arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION,android.Manifest.permission.ACCESS_COARSE_LOCATION)private const val ACCESS_BACKGROUND_LOCATION = android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
}
知道怎么处理了吗,搞定了就给我点个赞吧,放出实际效果图