
作为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版本,并且写法进行了更加完善的优化。
老哥账号名字:夢鑰
下面的是我的优化方案:
我优化了部分逻辑,将java改成kotlin。并且不依赖于snackbar,防止你使用的东西与snackbar冲突。增加了关于弹窗时间的控制,可以选择控制时间为无限亦或者固定时间。此外我还新增了图片部分,你也可以选择有图片或者无图片(隐藏)。
SnackBarUtil.kt
package com.help10000.rms.ui.utils
import 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.R
object 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.view
if (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.TOP
topMargin = 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 = msg
inflate.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.VISIBLE
imageView.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 = 0
val 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.CardView
android: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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:orientation="horizontal"
>
<ImageView
android: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"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android: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" />
<TextView
android: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? Activity
val 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.utils
import 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.SnackBarUtil
object LocationManager {
private var hasRequestedPermission = false
private var permissionResultCallback: ((Boolean) -> Unit)? = null
private var locationSuccessCallback: (() -> Unit)? = null
fun resetPermissionState() {
hasRequestedPermission = false
permissionResultCallback = null
locationSuccessCallback = 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_GRANTED
val hasCoarseLocation = permissions.contains(android.Manifest.permission.ACCESS_COARSE_LOCATION) &&
grantResults[permissions.indexOf(android.Manifest.permission.ACCESS_COARSE_LOCATION)] == PackageManager.PERMISSION_GRANTED
val isGranted = hasFineLocation || hasCoarseLocation
permissionResultCallback?.invoke(isGranted)
permissionResultCallback = null
}
suspend fun getLocation(context: Context, onSuccess: (() -> Unit)? = null): Pair<String, String> {
locationSuccessCallback = onSuccess
return 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 = true
return suspendCancellableCoroutine { continuation ->
permissionResultCallback = { granted ->
CoroutineScope(Dispatchers.Main).launch {
// 权限请求关闭,直接调用工具类的dismiss()关闭Snackbar
SnackBarUtil.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
)
// 协程取消时也关闭Snackbar
continuation.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? Activity
val 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 = 0
isAllowGPS = true
requestLevel = 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. 在此作用域内重新获取rootView
val activity = context as? Activity
val 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. 重新获取rootView
val activity = context as? Activity
val 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. 重新获取rootView
val activity = context as? Activity
val 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. 重新获取rootView
val activity = context as? Activity
val 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
}
知道怎么处理了吗,搞定了就给我点个赞吧,放出实际效果图
