Andriod运行时权限的适配
这篇文章解决的是android原生的运行时权限问题,本身跟uni-app的联系不大,但是这个问题是在uni-app加壳打包遇到的问题,加之自己并不是专业的android开发人员,后期也没有开设android专栏的计划,故将此文章放在uni-app专栏中
引言
在前面的文章04.uni-app发布成H5后,uni.chooseImage方法在android WebView上无法使用里我们实现了webview支持图片选择的功能。选择图片我们是支持拍照的,问题就处在拍照这个环节–运行时权限(对此不了解的可以看文章Android 运行时权限浅谈)。我们之前的方案是在app启动(也就是在MainActivity中)时一次性请求所有需要的权限但是实际测试时由于不同品牌的权限优化方案不同比如小米,如果第一次权限选择禁止或者仅本次允许,那么下次点击拍照时是会闪退的。
解决方案
引入权限适配方案
经过多方搜索,在文章这也许是Android权限适配更简单的解决方案提到了SoulPermission,不得不说大佬就是大佬不是我这种小虾米可以比的。首先我们把之前的代码优化一下,然后引入这个权限框架,完整MainActivity代码如下:
public class MainActivity extends BaseActivity {
private final static String TAG = "******";
@BindView(R.id.base_web_view)
BaseWebView baseWebView;
@BindView(R.id.progress_bar)
ProgressBar progressBar;
@BindView(R.id.layout_empty)
LinearLayout layoutEmpty;
private String url;
private int progressBarStyle = 0;
private MaterialDialog loadingDialog = null;
private boolean isLoadSuccess = true;
private boolean isCanReLoad = true;
//是否使用特殊的标题栏背景颜色,android5.0以上可以设置状态栏背景色,如果不使用则使用透明色值
protected boolean useThemestatusBarColor = false;
//是否使用状态栏文字和图标为暗色,如果状态栏采用了白色系,则需要使状态栏和图标为暗色,android6.0以上可以设置
protected boolean useStatusBarColor = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mod_main_activity);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
//实现沉浸式状态栏
setStatusBar();
ButterKnife.bind(this);
EventBus.getDefault().register(this);
// 初始化界面
init();
}
protected void setStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0及以上
View decorView = getWindow().getDecorView();
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
//根据上面设置是否对状态栏单独设置颜色
if (useThemestatusBarColor) {
getWindow().setStatusBarColor(getResources().getColor(R.color.base_white));//设置状态栏背景色
} else {
getWindow().setStatusBarColor(Color.TRANSPARENT);//透明
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {//4.4到5.0
WindowManager.LayoutParams localLayoutParams = getWindow().getAttributes();
localLayoutParams.flags = (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | localLayoutParams.flags);
} else {
Toast.makeText(this, "低于4.4的android系统版本不存在沉浸式状态栏", Toast.LENGTH_SHORT).show();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useStatusBarColor) {//android6.0以后可以对状态栏文字颜色和图标进行修改
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
private void init() {
try {
beforeInit();
baseWebView.setWebChromeClient(webChromeClient);
baseWebView.setWebViewClient(webViewClient);
baseWebView.loadUrl(url);
afterInit();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, R.string.base_data_error, Toast.LENGTH_SHORT).show();
}
}
protected void beforeInit() {
Bundle data = getIntent().getExtras();
if (null == data) {
Toast.makeText(MainActivity.this, R.string.base_data_error, Toast.LENGTH_SHORT).show();
return;
}
url = data.getString(ZhongZhanDriverConstant.WEB.WEB_DATA_KEY_URL, "");
progressBarStyle = data.getInt(ZhongZhanDriverConstant.WEB.WEB_DATA_KEY_PROGRESS_BAR_STYLE, ZhongZhanDriverConstant.WEB.PROGRESS_BAR_STYLE_PROGRESS);
if (TextUtils.isEmpty(url)) {
Toast.makeText(MainActivity.this, R.string.base_data_error, Toast.LENGTH_SHORT).show();
return;
}
}
protected void afterInit() {
if (isCanReLoad) {
layoutEmpty.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
isCanReLoad = false;
baseWebView.reload();
}
});
}
}
private Uri imageUri;
private ValueCallback<Uri> mUploadMessage;
private ValueCallback<Uri[]> mUploadCallbackAboveL;
private final static int PHOTO_REQUEST = 100;
private WebChromeClient webChromeClient = new WebChromeClient() {
public void onProgressChanged(WebView view, int newProgress) {
try {
onProgress(view, newProgress);
} catch (Exception e) {
e.printStackTrace();
}
}
// For Android 3.0-
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg)");
mUploadMessage = uploadMsg;
uploadImage();
}
// For Android 3.0+
public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
Log.d(TAG, "openFileChoose( ValueCallback uploadMsg, String acceptType )");
mUploadMessage = uploadMsg;
uploadImage();
}
//For Android 4.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
mUploadMessage = uploadMsg;
uploadImage();
}
// For Android 5.0+
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Log.d(TAG, "onShowFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
mUploadCallbackAboveL = filePathCallback;
uploadImage();
return true;
}
@Override
public void onPermissionRequest(PermissionRequest request) {
super.onPermissionRequest(request);
}
};
/**
* 拍照
*/
private void uploadImage() {
new ActionSheetDialog(MainActivity.this)
.builder()
.setCancelable(false)
.setCanceledOnTouchOutside(false)
.addSheetItem("拍照", ActionSheetDialog.SheetItemColor.Blue,
new ActionSheetDialog.OnSheetItemClickListener() {
@Override
public void onClick(int which) {
takePhoto();
}
})
.addSheetItem("从相册选择", ActionSheetDialog.SheetItemColor.Blue,
new ActionSheetDialog.OnSheetItemClickListener() {
@Override
public void onClick(int which) {
PhotoUtil.openPic(MainActivity.this, PHOTO_REQUEST);
}
})
.addSheetItem("取消", ActionSheetDialog.SheetItemColor.Red,
new ActionSheetDialog.OnSheetItemClickListener() {
@Override
public void onClick(int which) {
resetUploadImageData();
}
})
.show();
}
// 拍照功能权限验证
private void takePhoto(){
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.CAMERA,
new CheckPermissionWithRationaleAdapter("拍照需要使用相机权限,如果拒绝该权限该权限您将无法使用该功能,请点击授予权限",
new Runnable() {
@Override
public void run() {
// 每次重试之前需要先将数据初始化为null,否则为空,相机和相册功能无法再次启用
resetUploadImageData();
// 重试
takePhoto();
}
}){
@SuppressLint("MissingPermission")
@Override
public void onPermissionOk(Permission permission) {
File fileUri = new File(Environment.getExternalStorageDirectory().getPath() + "/" + SystemClock.currentThreadTimeMillis() + ".jpg");
imageUri = Uri.fromFile(fileUri);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
imageUri = FileProvider.getUriForFile(MainActivity.this, getPackageName() + ".fileprovider", fileUri);//通过FileProvider创建一个content类型的Uri
}
PhotoUtil.takePicture(MainActivity.this, imageUri, PHOTO_REQUEST);
}
});
}
// 相册功能
private void resetUploadImageData(){
if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(null);
mUploadMessage = null;
}
if (mUploadCallbackAboveL != null) {
mUploadCallbackAboveL.onReceiveValue(null);
mUploadCallbackAboveL = null;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PHOTO_REQUEST) {
if (null == mUploadMessage && null == mUploadCallbackAboveL) return;
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
if (mUploadCallbackAboveL != null) {
onActivityResultAboveL(requestCode, resultCode, data);
} else if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
if (requestCode != PHOTO_REQUEST || mUploadCallbackAboveL == null) {
return;
}
Uri[] results = null;
if (resultCode == Activity.RESULT_OK) {
showToast("正在上传,请稍后……");
if (data == null) {
results = new Uri[]{imageUri};
} else {
String dataString = data.getDataString();
ClipData clipData = data.getClipData();
if (clipData != null) {
results = new Uri[clipData.getItemCount()];
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
results[i] = item.getUri();
}
}
if (dataString != null)
results = new Uri[]{Uri.parse(dataString)};
}
} else {
showToast("没有选择图片!");
}
mUploadCallbackAboveL.onReceiveValue(results);
mUploadCallbackAboveL = null;
}
private void showToast(String message) {
Toast toast = Toast.makeText(this, null, Toast.LENGTH_LONG);
toast.setText(message);
toast.show();
}
private WebViewClient webViewClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.trim().startsWith("tel")) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
startActivity(i);
} else {
String port = url.substring(url.lastIndexOf(":") + 1, url.lastIndexOf("/"));//尝试要拦截的视频通讯url格式(808端口):【http://xxxx:808/?roomName】
if (port.equals("808")) {//特殊情况【若打开的链接是视频通讯地址格式则调用系统浏览器打开】
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
startActivity(i);
} else {//其它非特殊情况全部放行
view.loadUrl(url);
}
}
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
try {
onPageBegin(view, url, favicon);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
isLoadSuccess = false;
if (isLoadSuccess) {
layoutEmpty.setVisibility(View.GONE);
} else {
layoutEmpty.setVisibility(View.VISIBLE);
}
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
isLoadSuccess = false;
if (isLoadSuccess) {
layoutEmpty.setVisibility(View.GONE);
} else {
layoutEmpty.setVisibility(View.VISIBLE);
}
}
@Override
public void onPageFinished(WebView view, String url) {
try {
onPageEnd(view, url);
} catch (Exception e) {
e.printStackTrace();
}
}
};
protected void onPageBegin(WebView view, String url, Bitmap favicon) {
isLoadSuccess = true;
try {
if (progressBarStyle == ZhongZhanDriverConstant.WEB.PROGRESS_BAR_STYLE_PROGRESS) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
loadingDialog = new MaterialDialog.Builder(MainActivity.this).content(R.string.base_data_loading).contentGravity(GravityEnum.CENTER).cancelable(true).autoDismiss(false).progress(true, 0).progressIndeterminateStyle(false).show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected void onPageEnd(WebView view, String url) {
isCanReLoad = true;
if (isLoadSuccess) {
layoutEmpty.setVisibility(View.GONE);
} else {
layoutEmpty.setVisibility(View.VISIBLE);
return;
}
if (progressBarStyle == ZhongZhanDriverConstant.WEB.PROGRESS_BAR_STYLE_PROGRESS) {
progressBarFinish();
} else {
if (null != loadingDialog) {
loadingDialog.dismiss();
}
}
}
protected void onProgress(WebView view, int newProgress) {
if (progressBarStyle == ZhongZhanDriverConstant.WEB.PROGRESS_BAR_STYLE_PROGRESS) {
progressBar.setProgress(newProgress);
}
}
protected void progressBarFinish() {
try {
progressBar.setProgress(100);
progressBar.setVisibility(View.GONE);
} catch (Exception e) {
e.printStackTrace();
}
}
private final int exitIntervalTime = 1000 * 2;
private long exitLastTime = 0;
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (baseWebView.canGoBack()) {
baseWebView.goBack();// 返回前一个页面
return true;
} else if ((System.currentTimeMillis() - exitLastTime) > exitIntervalTime) {
Toast.makeText(this, "再按一次退出程序", Toast.LENGTH_SHORT).show();
exitLastTime = System.currentTimeMillis();
} else {
this.finish();
}
return true;
}
return super.onKeyDown(keyCode, event);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(LocationEvent event) {
try {
JSONObject result = event.getResult();
LogUtils.i(JSONObject.toJSONString(result));
baseWebView.loadUrl("javascript:locResult("+result+")");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}
在方法takePhoto()
中我们调用了SoulPermission.getInstance().checkAndRequestPermission
方法,这个时候首先系统会提示用户如果进行该操作需要授予某项权限,如果用户点击拒绝系统会再次提示用户授予该权限,如果用户点击禁止并不再提示则会引导用户前往应用详情界面开启该权限,这种方式看似没有问题,但是……
我们发现连续两次从设置页返回就无法再次选择图片了,其实这个就是我们在文章04.uni-app发布成H5后,uni.chooseImage方法在android WebView上无法使用结尾出提到的第二个坑,在这里想要解决这个问题只要能在返回页面的时候捕捉到这个事件,并且在该事件中执行MainActivity中的resetUploadImageData()
的方法就可以了。
应用详情页面的返回
上面的权限验证发放中我们调用了CheckPermissionWithRationaleAdapter
这个类,代码如下
public abstract class CheckPermissionWithRationaleAdapter implements CheckRequestPermissionListener {
private String rationaleMessage;
private Runnable retryRunnable;
/**
* @param rationaleMessage 当用户首次拒绝弹框时候,根据权限不同给用户不同的文案解释
* @param retryRunnable 用户点重新授权的runnable 即重新执行原方法
*/
public CheckPermissionWithRationaleAdapter(String rationaleMessage, Runnable retryRunnable) {
this.rationaleMessage = rationaleMessage;
this.retryRunnable = retryRunnable;
}
@Override
public void onPermissionDenied(Permission permission) {
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
//绿色框中的流程
//用户第一次拒绝了权限、并且没有勾选"不再提示"这个值为true,此时告诉用户为什么需要这个权限。
if (permission.shouldRationale()) {
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(rationaleMessage)
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用户确定以后,重新执行请求原始流程
retryRunnable.run();
}
}).create().show();
} else {
//此时请求权限会直接报未授予,需要用户手动去权限设置页,所以弹框引导用户跳转去设置页
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(permissionDesc + "异常,请前往设置->权限管理,打开" + permissionDesc + "。")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去设置页
SoulPermission.getInstance().goApplicationSettings();
}
}).create().show();
}
}
}
在上面的代码中用户点击拒绝并不再提示后跳往应用详情界面的代码SoulPermission.getInstance().goApplicationSettings();
我们点进去查看源码,发现该方法还有一个重载方法
该重载方法的参数是个Intent,源码如下
先不管Intent带回的数据是什么,我们先改下代码试一下效果
CheckPermissionWithRationaleAdapter优化
public abstract class CheckPermissionWithRationaleAdapter implements CheckRequestPermissionListener {
private String rationaleMessage;
private Runnable retryRunnable;
/**
* @param rationaleMessage 当用户首次拒绝弹框时候,根据权限不同给用户不同的文案解释
* @param retryRunnable 用户点重新授权的runnable 即重新执行原方法
*/
public CheckPermissionWithRationaleAdapter(String rationaleMessage, Runnable retryRunnable) {
this.rationaleMessage = rationaleMessage;
this.retryRunnable = retryRunnable;
}
@Override
public void onPermissionDenied(Permission permission) {
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
//绿色框中的流程
//用户第一次拒绝了权限、并且没有勾选"不再提示"这个值为true,此时告诉用户为什么需要这个权限。
if (permission.shouldRationale()) {
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(rationaleMessage)
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用户确定以后,重新执行请求原始流程
retryRunnable.run();
}
}).create().show();
} else {
//此时请求权限会直接报未授予,需要用户手动去权限设置页,所以弹框引导用户跳转去设置页
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(permissionDesc + "异常,请前往设置->权限管理,打开" + permissionDesc + "。")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去设置页
// SoulPermission.getInstance().goApplicationSettings();
SoulPermission.getInstance().goApplicationSettings(new GoAppDetailCallBack() {
@Override
public void onBackFromAppDetail(Intent data) {
ImageEvent imageEvent = new ImageEvent(data);
EventBus.getDefault().post(imageEvent);
}
});
}
}).create().show();
}
}
}
上面代码中当用户从应用详情页面返回的时候,我们利用EventBus直接将事件ImageEvent直接post了出去,然后在MainActivity中监听该事件即可
MainActivity优化
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(ImageEvent event) {
Intent intent = null;
try {
intent = event.getIntent();
if(null == intent){
resetUploadImageData();
}
} catch (Exception e) {
e.printStackTrace();
}
}
我们在该事件中通过判定Intent是否为null
来执行resetUploadImageData();
注:经过测试发现不论在应用详情页面是否允许权限页面监听到的intent都为null,不知道是不是我测试的问题;其实这里根本不用判定intent是否为空,我们主要是为了解决上面提到的第二个坑的问题,这里不涉及图片的选择,我们大可在监听到该事件时直接执行resetUploadImageData();
解决完上面的问题我们再来测试一下看看效果
上面可以看到每次从应用详情页面返回都可以再次点击,但是真正拍照上传却除了问题,我们看下控制台输出
意思大概是没有找到我们的照片
bug解决
这个问题我们按文章文章Android7.0及以上拍照获取照片无法使用file://,使用content://URI来修改MainActivity中对应的方法createTakePhotoFile()
的方法如下:
/**
* 解决W/ContentUriUtils: Cannot find content uri: content://getPackageName().fileprovider/camera_photos/**.jpg的错误
* @return
*/
@NonNull
private File createTakePhotoFile() {
File imagePath = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "zzwlnet_driver");
if (!imagePath.exists()) {
imagePath.mkdirs();
}
File file = new File(imagePath, SystemClock.currentThreadTimeMillis() + ".jpg");
return file;
}
再次启动App,发现程序正常运行,
忙活了一天也算历尽千辛总算搞出来了,奖励自己早睡O(∩_∩)O哈哈~