到了一家新公司,接手了一套别人的代码。代码的minSdkVersion 是21,targetSdkVersion 是23。导致应用在Android手机系统6.0
的手机因一些“危险权限”未得到用户许可而造成应用崩溃。(虽然公司给员工的安装手册上明确指出要打开某某权限,但是自己觉得这样
太不够人性化。)随着自己解决了Android 6.0 运行时权限的问题,一直在犹豫要不要写一篇关于Android 6.0运行时权限处理的博客
客。毕竟我也是站在巨人的来肩膀上得到的成果。但是想想,一来:网上关于Android 6.0运行时权限的博客很多,方法也有很多,
很多我尝试了,对我来说实现起来确实有点繁琐;第二点:我在借鉴别人的成果上优化了代码,并添加了一些更为人性化的逻辑代
码;第三点:Android 6.0 运行时权限,目前为止,这个问题还算新颖。贡献出来,对后来者还是有用的。哈哈,总结下经验,分享
下成果,对自己,对别人,终归是好的。废话不多说,上干货!
首先普及下哪些权限需要进行运行时权限的处理。运行时权限主要处理的是一些“危险权限”,所谓“危险权限”,指的是涉及到用
户隐私的权限,比如存储、悬浮框、相机等。具体如下:
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA
permission:android.permission.CAMERA
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
permission:android.permission.SYSTEM_ALERT_WINDOW
解决方案一:
或许有些朋友会把targetSdkVersion调成21,来规避Android 6.0 系统的运行时权限的问题。但是这并没有从根本上去解决问题。
往后会有大批用户还是会升级到Android 6.0系统,问题还是要面对的。不做处理,对于自己来说,自己也没有什么进步!这种处理方式,
不推荐!
解决方案二:(推荐)
大致逻辑要考虑到:
1. 用户正常允许(这个简单,不做分析);
2. 用户禁止打开某权限,可以考虑引导用户去设置中打开,打开后返回应用;
3. 用户点击“记住选择”强制禁止(本人测试手机三星S6和小米Redmi 4(公司用户只用这两款手机办公)没有这个选项,
这个不做考虑)。
考虑到在项目中可能在多处地方会用到权限问题,所以优先建议创建公共基类,在基类中编写权限逻辑,在子Activity
或子Fragment中(本博客不做介绍)调用。
1. AndroidManifest.xml
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />虽然运行时权限会对“危险性权限”进行访问,但是清单里也必须有相应的权限声明。
2. BaseActivity.java
import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import org.xutils.common.util.LogUtil; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import sg.com.jcdecaux.demo.R; import sg.com.jcdecaux.demo.action.OtherAction; import sg.com.jcdecaux.demo.data.Constants; import sg.com.jcdecaux.demo.util.ActivityUtil; /** * Created by ah on 2017/5/16. * Activity基类 */ public abstract class BaseActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityUtil.addActivity(this); } protected boolean isPermissionGranted(String permissionName, int questCode) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } //判断是否需要请求允许权限 int hasPermission = checkSelfPermission(permissionName); if (hasPermission != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{permissionName}, questCode); return false; } return true; } protected boolean isPermissionsAllGranted(String[] permArray, int questCode) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } //获得批量请求但被禁止的权限列表 List<String> deniedPerms = new ArrayList<String>(); for (int i = 0; permArray != null && i < permArray.length; i++) { if (PackageManager.PERMISSION_GRANTED != checkSelfPermission(permArray[i])) { deniedPerms.add(permArray[i]); } } //进行批量请求 int denyPermNum = deniedPerms.size(); if (denyPermNum != 0) { requestPermissions(deniedPerms.toArray(new String[denyPermNum]), questCode); return false; } return true; } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (grantResults.length == 0) { return; } switch (requestCode) { case Constants.QUEST_CODE_LOCTION: showWarningDialog(grantResults, getResources().getString(R.string.action_location), getResources().getString(R.string.text_location)); break; case Constants.QUEST_CODE_CAMERA: showWarningDialog(grantResults, getResources().getString(R.string.action_camera), getResources().getString(R.string.text_camera)); break; case Constants.QUEST_CODE_STORAGE: showWarningDialog(grantResults, getResources().getString(R.string.action_storage), getResources().getString(R.string.text_storage)); break; case Constants.QUEST_ALERT_WINDOW: { showWarningDialog(grantResults, getResources().getString(R.string.action_alert_window), getResources().getString(R.string.text_alert_window)); } break; case Constants.QUEST_CODE_ALL: doPermissionAll(Constants.permArray, grantResults); break; default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); break; } } private void doPermissionAll(String[] permissions, int[] grantResults) { int grantedPermNum = 0; int totalPermissons = permissions.length; int totalResults = grantResults.length; if (totalPermissons == 0 || totalResults == 0) { return; } Map<String, Integer> permResults = new HashMap<String, Integer>(); //初始化Map容器,用于判断哪些权限被授予 for (String perm : Constants.permArray) { permResults.put(perm, PackageManager.PERMISSION_DENIED); } //根据授权的数目和请求授权的数目是否相等来判断是否全部授予权限 for (int i = 0; i < totalResults; i++) { permResults.put(permissions[i], grantResults[i]); if (permResults.get(permissions[i]) == PackageManager.PERMISSION_GRANTED) { grantedPermNum++; } LogUtil.d("权限:" + permissions[i] + "-->" + grantResults[i]); } if (grantedPermNum == totalPermissons) { //用于授予全部权限 } else { // OtherAction.showToast("批量申请权限失败,将会影响正常使用!"); } } public void popAlterDialog(final String msgFlg, String msgInfo) { new AlertDialog.Builder(BaseActivity.this) .setTitle(getResources().getString(R.string.title_warning_file_not_satisfy_required)) .setMessage(msgInfo) .setNegativeButton(getResources().getString(R.string.cancel_fault_report_remark), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (TextUtils.equals(msgFlg, getResources().getString(R.string.action_alert_window))) { OtherAction.showToast(getResources().getString(R.string.toast_refuse_permission)); ActivityUtil.finishAllActivity(); } else dialog.dismiss(); } }) .setPositiveButton(getResources().getString(R.string.action_settings), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //前往应用详情界面 try { Uri packUri = Uri.parse("package:" + getPackageName()); Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packUri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); BaseActivity.this.startActivity(intent); } catch (Exception e) { e.printStackTrace(); LogUtil.e("跳转失败"); } dialog.dismiss(); } }).create().show(); } /** * 权限被拒绝时,弹出警告框 * * @param grantResults 权限组 * @param action 某权限 * @param text 权限详述 */ private void showWarningDialog(int[] grantResults, String action, String text) { if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { popAlterDialog(action, String.format( getResources().getString(R.string.toast_permission_ask), action, text, action)); } else { OtherAction.showToast(String.format(getResources().getString(R.string.toast_permission_allow), action)); } } }
其中isPermissionGrated用于申请单个权限,isPermissionAllGrated用于批量申请权限,OnRequestPermissionResult
用户权限回调的处理。
里面的代码在isPermissionGranted方法中进行权限判断,如果已同意,则返回true;否则执行底层代码requestPermission()
方法,请求权限。在OnRequestPermissionResult回调方法中进行判断,并弹框提示shouWarningDialog()。如果已允许,则直接
吐司;否则弹框:(1)取消操作,可以自行设置,我这里进行了自己产品的判断处理,在下面我会讲解;(2)设置操作,直接
引导用户跳到手机设置里去打开权限。
3. Constants.java
import android.Manifest; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import org.xutils.x; import sg.com.jcdecaux.demo.action.OtherAction; /** * Created ah on 2017/3/24. * 常量类 */ public class Constants { public static final int QUEST_CODE_LOCATION =1; // 位置信息权限请求标志 public static final int QUEST_CODE_CAMERA = 2; // 摄像头权限标志 public static final int QUEST_CODE_ALL = 3; // 批量请求权限 public static final int QUEST_CODE_STORAGE = 4; // 文件存储权限 public static final int QUEST_ALERT_WINDOW = 5; // 悬浮框权限 public static final String[] permissionStorage = // 文件读/写权限 {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}; public static final String[] permArray = // 批量要申请的权限 {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.SYSTEM_ALERT_WINDOW,}; }
4. strings.xml
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="action_location">位置</string> <string name="text_location">定位</string> <string name="action_camera">相机</string> <string name="text_camera">拍照</string> <string name="action_storage">存储</string> <string name="text_storage">照片存储</string> <string name="action_alert_window">悬浮窗</string> <string name="text_alert_window">版本更新提示</string> <string name="toast_permission_allow">恭喜,已授予 <xliff:g>%s</xliff:g>权限! </string> <string name="toast_permission_ask"> <xliff:g>%1$s</xliff:g>权限被禁止, <xliff:g>%2$s</xliff:g>功能无法正常使用。是否开启该权限?(步骤:应用信息->权限->“勾选” <xliff:g>%3$s</xliff:g>)! </string>
</resources>
在这里,我用到了"
在代码里我申请了3个权限,READ_EXTERNAL_STORAGE(读权限)、WRITE_EXTERNAL_STORAGE(写权限),
二者合称“存储”权限。另外还有CAMERA(相机权限)。
5. OtherAction.java强大吐司
** * Created by ah on 2017/4/10. * 常规Action */ public class OtherAction { public static Toast toast = null; public static void showToast(String msg) { if (toast == null) { toast = Toast.makeText(x.app(), msg, Toast.LENGTH_SHORT); } else { toast.setText(msg); } toast.show(); }}
6. ActivityUtil.java
import android.app.Activity; import java.util.ArrayList; import java.util.List; /** * Created by ah on 2017/5/24. * Activity管理类 */ public class ActivityUtil { public static List<Activity> activities = new ArrayList<Activity>(); public static void addActivity(Activity activity) { activities.add(activity); } public static void removeActivity(Activity activity) { activities.remove(activity); } public static void finishAllActivity() { for (Activity activity : activities) { if (!activity.isFinishing()) { activity.finish(); } } } }
7. 在子Activity中进行调用
比如,我的项目中需要这3个权限,我会这么处理:
先封装方法,具体如下
/** * 是否允许拍照: 相机权限+读/写权限 * * @return */ private boolean isAllowCamera() { if (isPermissionGranted(Manifest.permission.CAMERA, Constants.QUEST_CODE_CAMERA) && isPermissionsAllGranted(Constants.permissionStorage, Constants.QUEST_CODE_STORAGE)) { return true; } return false; }紧接着,在点击的时候进行判断操作:
if(isAllowCamera){
// 开始搞事情
}
由于项目属于公司私有,这里我就不上传demo了。复制我的代码是没有问题的。现在,运行时权限问题就算华丽丽
得处理完了。是不是很简单呢?
----------------------------------------------------------我是分割线---------------------------------------------------------------------------------
上面我用到了ActivityUtil.java,是因为我们有一个产品逻辑(这个逻辑在很多应用里都是这么用的),就是在引导用户
去设置权限的弹框中点击“取消”时,直接退出应用,因为如果用户不授权CAMERA权限和存储权限,无法实现拍照和照片存
储,这个模块的功能就无法使用(有些应用在刚进入主页面的时候不打开某个权限,如果拒绝权限整个应用都不会让使用)。
SYSTEM_ALERT_DIALOG 悬浮框
另外,还有一个最终重要的权限,我要重新声明下,SYSTEM_ALERT_DIALOG权限,用isPermissionGranted(Manifest.
permission.SYSTEM_ALERT_WINDOW,Constants.QUEST_ALERT_WINDOW)在用户已经被引导设置并打开完权限以后,
应用中引导打开设置的弹框还会再弹出来。说明这种判断并不适用了。解决问题如下:
/** * 检查悬浮框权限 * * @param activity activity * @return true/false */ public static boolean checkPermission(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(activity)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName())); activity.startActivityForResult(intent, Constants.QUEST_ALERT_WINDOW); return false; } } return true; }在子Activity中点击操作时候,执行这个判断,然后在子Activity中,添加OnActivityResult回调方法,代码如下:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 1) { updateData(true); } if (requestCode == Constants.QUEST_ALERT_WINDOW) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(this)) { popAlterDialog(getResources().getString(R.string.action_alert_window), String.format( getResources().getString(R.string.toast_permission_ask), getResources().getString(R.string.action_alert_window), getResources().getString(R.string.text_alert_window), getResources().getString(R.string.action_alert_window))); } } } }这样就行了,悬浮框的权限问题也解决了。有没有很兴奋?
网上有人说,让BaseActivity实现OnRequestPermissionsResultCallback()监听,才能实现onRequestPermissionsResult()
回调方法。在这里我可以明确 的给大家说,可以不用实现这个监听,onRequestPermissionsResult()方法依然奏效!
在这里,我要感谢,这些文章让我做的参考,谢谢:
http://blog.csdn.net/andrexpert/article/details/53331836
http://blog.csdn.net/lmj623565791/article/details/50709663
http://blog.csdn.net/yanzhenjie1003/article/details/52503533/