可以在 Android Marshmallow (API 23) 的运行时权限模型中同步请求权限吗?
Posted
技术标签:
【中文标题】可以在 Android Marshmallow (API 23) 的运行时权限模型中同步请求权限吗?【英文标题】:Can you request permissions synchronously in Android Marshmallow (API 23)'s runtime permissions model? 【发布时间】:2015-11-15 01:02:13 【问题描述】:假设你有这样的方法:
public boolean saveFile (Url url, String content)
// save the file, this can be done a lot of different ways, but
// basically the point is...
return save_was_successful;
在整个应用程序中,如果您想将文件保存到外部存储,您可以执行类似...
if (saveFile(external_storage_url, "this is a test"))
// yay, success!
else // notify the user something was wrong or handle the error
这是一个简化的例子,所以不要谈论我关于阻止 UI、正确处理异常等的案例。如果你不喜欢文件保存,你可以想象一个 getContact()
或 getPhoneState()
或其他.关键是它是一个需要权限的操作,它返回一些值并在整个应用程序中使用。
在 android android.permission.WRITE_EXTERNAL_STORAGE 或其他权限,一切都会好起来的。
但在新的 Marshmallow (API 23) runtime permission model 中,在将文件保存到外部存储之前,您应该 (1) 检查是否已授予权限。 If not,可能是(2)show a rationale for the request(如果是system thinks it's a good idea)祝酒或其他任何东西和(3)通过对话框要求用户授予权限,然后基本上坐下来等待回调......
(因此您的应用程序会闲置,等待...)
(4) 当用户最终响应对话框时,onRequestPermissionsResult() 方法触发,你的代码现在 (5) 必须筛选出 permission 请求它们实际上是 responding to,无论用户是否说是或否(据我所知,没有办法处理“不”与“不,不要再问”),(6)首先弄清楚他们试图完成的事情,这促使整个要求-permissions 过程,以便程序最终 (7) 继续执行该操作。
要了解用户在步骤 (6) 中尝试执行的操作,需要事先传递一个特殊的 code(“权限请求响应”),在文档中将其描述为 permission request 类型的标识符(相机/联系人/等),但在我看来更像是“具体来说,当你意识到你需要请求权限时你正在尝试做的事情”代码,因为相同的权限/组可以用于多个代码中的目的,因此您需要在获得权限后使用此代码将执行返回到适当的位置。
我可能完全误解了这应该是如何工作的——所以如果我离题了,请告诉我——但更大的一点是我真的不知道该怎么做由于异步“等待用户响应”部分,请考虑使用前面描述的saveFile()
方法执行上述所有操作。我考虑过的想法很老套,而且肯定是错误的。
今天的Android Developer Podcast 暗示可能会有一个同步的解决方案指日可待,甚至有人谈到了 Android Studio 中一种神奇的、一步 alt-enter 类型的“添加权限请求”工具。尽管如此,如何将运行时权限过程推入saveFile()
或其他什么——我在想一些类似的事情:
public boolean saveFile(Url url, String content)
// this next line will check for the permission, ask the user
// for permission if required, maybe even handle the rationale
// situation
if (!checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
R.string.permission_storage_rationale))
return false; // or throw an exception or whatever
else
// try to save the file
return save_was_successful;
因此,如果用户没有并且拒绝授予权限,上述checkPermission()
将失败。也许可以使用checkPermission()
周围的循环来尝试最多询问 3 次或其他内容,或者如果该方法处理了一个理智的不要烦人的策略,那就更好了。
这样的事情可能吗?可取的?任何这样的解决方案会阻止 UI 线程吗?从播客看来,谷歌可能有一个类似的解决方案即将到来,但我想知道是否有什么东西——一个便利类、一个模式、什么东西——并不涉及每个人都必须重构所有需要权限的操作,我必须假设这可能会变得非常混乱。
很抱歉这个冗长的问题,但我想尽可能完整。我会把我的答案从广播中删除。谢谢!
更新:这是上述播客的文字记录。
收听41:20。在本次讨论中:
粗略的成绩单:
Tor Norbye(工具团队):“所以对于开发人员来说,这似乎不应该做很多工作。但我理解部分问题是这些不是同步调用,对吧?所以你必须做的——实际上改变你的 Activity 的编写方式,让它有一个回调——所以它真的有点像一个状态机……对于这个状态,你——"
Poiesz(产品经理):“啊-我想-有一个-可能有同步响应的选项--”
Norbye:“哦。那会让事情发生——”
Poiesz:“我可以在内部与人交谈。我记得有一次关于同步的讨论——但我们可以找到答案。”
Norbye:“是的。事实上,我们可能应该把它变成工具。在那里你有一个简单的重构......”
然后他谈到在工具中使用注释来确定哪些 API 需要权限..(到目前为止,这在 IMO 中还没有那么好用),以及他希望有朝一日这些工具在发现未经检查的“危险”方法调用:
Norbye:“...如果你也在 M不是,我们会说“你可能需要做一些事情来请求这里的许可”。不过,我想要的是有一个快速修复,你可以去“CHING!”它插入了所有正确的东西来询问,但是当我看到事情的时候,这需要重组很多东西——添加接口和回调,改变流程,而我们做不到。但如果有一个简单的同步模式作为临时的东西或永久的东西,那会[很棒]。"
【问题讨论】:
我认为 UI 线程阻塞不会有问题。甚至在 Marshmallow 之前,我们就做了很多询问对话框并调用不同的代码取决于用户的决定,所有这些都是异步的。 您认为同步方法如何工作?如果不能只单击 UI 线程。现在的事情实际上非常简单,只需几行代码。我想一个同步选项实际上更复杂。结果只是更多的代码聚集在一个地方,而不是现在处理事情的好方法。 归根结底就是为您的应用程序提供一个干净整洁的设计和架构,我发现运行时权限可以很好地集成到整个系统中,即使我希望关键方法不会成为其中的一部分Activity
类。但是我可以想象出一些很好的理由来说明为什么会出现这种情况。
顺便说一句:Android Studio 中有快速修复插入运行时权限检查。而且我从来不需要重组任何东西来整合它们。
如果您足够幸运已经拥有需要异步权限的代码,事情就很简单了。但是我给出了一些示例,其中可能没有考虑到应用程序的设计,例如返回成功/失败的方法,该方法可能在整个应用程序的无数上下文中重复调用。它必须完全重构。传递大量“权限请求响应”代码,即。每种类型的权限检查的上下文,通过 result 方法,然后始终正确处理每个场景(每个实例、每个权限、每个结果)可以轻松地将代码快速变成意大利面条。
【参考方案1】:
至于 Marshmallow,我的理解是你不能。
我必须在我的应用中解决同样的问题。我是这样做的:
-
重构:将依赖于某种权限的每一段代码移动到自己的方法中。
更多重构:确定每个方法的触发器(例如启动活动、点击控件等),如果它们具有相同的触发器,则将它们组合在一起。 (如果两种方法最终具有相同的触发器并且需要相同的权限集,请考虑合并它们。)
更多重构:找出之前调用过的新方法依赖于什么,并确保何时调用这些方法是灵活的:要么将其移入方法本身,要么至少确保如果您的方法之前没有被调用过,那么无论您做什么都不会引发异常,并且一旦调用该方法,它就会开始按预期运行。
请求代码:对于这些方法中的每一个(或其组),定义一个整数常量以用作请求代码。
检查和请求权限:将这些方法/方法组中的每一个包装到以下代码中:
.
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SOME_PERMISSION) == PackageManager.PERMISSION_GRANTED)
doStuffThatRequiresPermission();
else
ActivityCompat.requestPermissions(this, new String[]Manifest.permission.SOME_PERMISSION, Const.PERM_REQUEST_DO_STUFF_THAT_REQUIRES_PERMISSION);
-
处理响应:在每个请求权限的
Activity
中为onRequestPermissionsResult()
编写一个实现。检查请求的权限是否被授予,并使用请求代码来确定需要调用的方法。
请注意,API 需要 Activity
来处理运行时权限请求。如果您有非交互式组件(例如 Service
),请查看 How to request permissions from a service in Android Marshmallow 以获取有关如何解决此问题的建议。基本上,最简单的方法是显示一个通知,然后会弹出一个Activity
,它只会显示运行时权限对话框。 Here 是我在我的应用程序中解决这个问题的方法。
【讨论】:
我调用requestPermissions()
一次,它们对该进程中的所有活动以及我的后台服务生效,该服务在一个完全独立的进程中运行,该进程以授予 sdcard-write 权限后的意图(到启动它的“前台”Activity)。
确实,这可能行得通:当您的应用首次启动时,请求它所需要的所有权限。这可能适用于一个或两个权限,但对于第一次使用的用户来说,如果他们必须在第一次接触您的应用之前确认大约五个权限,则可能会变得乏味。
据我所知,唯一需要这种额外安全级别的权限是 sdcard 写入权限和摄像头(尽管我仍然可以在不先通过 requestPermissions 对话框提示用户的情况下启动摄像头)。跨度>
据我所知,这会影响所有权限,尽管请求是按类别进行的。联系人和位置也肯定需要被请求。警告:如果应用程序针对较低的 API 级别,旧的行为仍然存在,在安装时授予所有权限。
我可以看到你的方法是适用的,形式总是好的。我想对于我的特定场景,我需要在启动时立即写入配置文件。但是,是的,如果可能需要很多权限,那么从 UX 的角度来看,等到真正需要它们可能是一个好主意(而不是用一堆弹出窗口来烦人的用户......没有人有时间)。但是,如果有一种方法可以在单个对话框中请求所有权限(例如从商店安装时),那么我想这不会那么糟糕。【参考方案2】:
简短回答:不,今天没有任何同步操作。您必须在完成操作之前检查您是否拥有正确的权限,或者作为最后一个选项,您可以为安全异常放置一个 try/catch 块。在 catch 块中,您可以通知用户由于权限问题导致操作失败。 此外,还有一点:当权限被撤销时,应用程序不会从主活动中重新启动,因此即使在 onResume() 中也必须检查权限。
【讨论】:
【参考方案3】:所以我不想专门回答我自己关于我的示例中使用的android.permission.WRITE_EXTERNAL_STORAGE
权限的问题,但到底是什么。
对于读取和/或写入文件,实际上有一种方法可以完全避免请求然后检查权限,从而绕过我上面描述的整个流程。这样我举例的saveFile (Url url, String content)
方法就可以继续同步工作了。
我相信该解决方案适用于 API 19+,通过让 DocumentsProvider
充当“中间人”,基本上代表您的应用询问用户“请选择特定的要写入的文件”(即“文件选择器”),然后一旦用户选择了一个文件(或键入新的文件名),应用程序现在神奇地被授予对该 Uri 执行此操作的权限,因为用户已经特别授予它。
无需“官方”WRITE_EXTERNAL_STORAGE
许可。
这种借用权限的方式是Storage Access Framework 的一部分,Ian Lake 在 Big Android BBQ 上对此进行了讨论。 Here's a video 称为 忘记存储权限:共享和协作的替代方案,它涵盖了基础知识以及您如何专门使用它来完全绕过 WRITE_EXTERNAL_STORAGE
权限要求。
这并不能完全解决所有情况下的同步/异步权限问题,但适用于任何类型的外部文档,甚至是由提供商(例如 gDrive、Box.net、Dropbox 等)提供的文档。这可能是一个值得一试的解决方案。
【讨论】:
【参考方案4】:这是我解决“同步性”问题的方法,无需显式阻止(和忙等待),也无需单独的“引导加载程序”活动。我重构了闪屏Activity如下:
更新:更完整的例子可以找到here.
注意:因为requestPermissions()
API 调用startActivityForResult()
public final void requestPermissions(@NonNull String[] permissions, int requestCode)
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
主视图创建逻辑从OnCreate()
移至OnCreate2()
,OnCreate()
现在处理权限检查。如果需要调用RequestPermissions()
,则关联的OnRequestPermissionsResult()
会重新启动此活动(转发原始包的副本)。
[Activity(Label = "MarshmellowActivated",
MainLauncher = true,
Theme = "@style/Theme.Transparent",
Icon = "@drawable/icon"
////////////////////////////////////////////////////////////////
// THIS PREVENTS OnRequestPermissionsResult() from being called
//NoHistory = true
)]
public class MarshmellowActivated : Activity
private const int ANDROID_PERMISSION_REQUEST_CODE__SDCARD = 112;
private Bundle _savedInstanceState;
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode)
case ANDROID_PERMISSION_REQUEST_CODE__SDCARD:
if (grantResults.Length > 0 && grantResults[0] == Permission.Granted)
Intent restartThisActivityIntent = new Intent(this, this.GetType());
if (_savedInstanceState != null)
// *ref1: Forward bundle from one intent to another
restartThisActivityIntent.PutExtras(_savedInstanceState);
StartActivity(restartThisActivityIntent);
else
throw new Exception("SD Card Write Access: Denied.");
break;
protected override void OnCreate(Bundle savedInstanceState)
base.OnCreate(savedInstanceState);
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// Android v6 requires explicit permission granting from user at runtime for extra sweet security goodness
Permission extStoragePerm = ApplicationContext.CheckSelfPermission(Android.Manifest.Permission.WriteExternalStorage);
//if(extStoragePerm == Permission.Denied)
if (extStoragePerm != Permission.Granted)
_savedInstanceState = savedInstanceState;
// **calls startActivityForResult()**
RequestPermissions(new[] Android.Manifest.Permission.WriteExternalStorage , ANDROID_PERMISSION_REQUEST_CODE__SDCARD);
else
OnCreate2(savedInstanceState);
private void OnCreate2(Bundle savedInstanceState)
//...
ref1: Forward bundle from one intent to another
注意:这可以被重构来处理更多的权限。它目前只处理 sdcard 写权限,应该足够清晰地传达相关逻辑。
【讨论】:
【参考方案5】:您可以像这样添加阻塞辅助方法:
@TargetApi(23)
public static void checkForPermissionsMAndAboveBlocking(Activity act)
Log.i(Prefs.TAG, "checkForPermissions() called");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
// Here, thisActivity is the current activity
if (act.checkSelfPermission(
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED)
// No explanation needed, we can request the permission.
act.requestPermissions(
new String[]
Manifest.permission.WRITE_EXTERNAL_STORAGE
,
0);
while (true)
if (act.checkSelfPermission(
Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED)
Log.i(Prefs.TAG, "Got permissions, exiting block loop");
break;
Log.i(Prefs.TAG, "Sleeping, waiting for permissions");
try Thread.sleep(1000); catch (InterruptedException e) e.printStackTrace();
// permission already granted
else
Log.i(Prefs.TAG, "permission already granted");
else
Log.i(Prefs.TAG, "Below M, permissions not via code");
【讨论】:
太疯狂了,你知道阻塞 UI 线程是个糟糕的主意,而且阻塞线程的方法比使用 Thread.sleep 命令的无限循环要好得多,对吧? 如果你这样做了,你怎么期望任何东西都能正常工作,这仍然很糟糕?你为什么要这样做呢?如果将主线程装箱并削弱您的应用程序性能和用户体验的好处呢?你不需要做任何这些。如果您需要在一开始就请求许可,那么是什么阻止您这样做?没有。所有这一切都是用户甚至无法与权限对话框正确交互,因为 UI 线程无缘无故地不断休眠。 为什么需要立即采取行动?因为它必须读取或写入数据到 SD 卡?你没有理由不能在onRequestPermissionsResult()
中而不是立即开始这样做。事实上,大多数时候您可以立即开始 - 如果selfPermissionCheck()
返回已授予权限。您只需要添加 if 然后在 onRequestPermissionsResult()
中启动该过程。
我找不到任何迹象表明他们已经或曾经计划添加该内容。
那么一年多前,某位工程师在播客上所说的是谷歌承认需要添加这一点吗?当然,他只是将此作为一种解决方法,直到可以将快速修复添加到 Android Studio 中。 (它现在有)。在代码中做任何你想做的事情,我可以给你建议:解决你遇到的任何问题。你在做什么是坏的。你不能仅仅阻塞主线程。寻找其他解决方案。以上是关于可以在 Android Marshmallow (API 23) 的运行时权限模型中同步请求权限吗?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Android Marshmallow 平台上高效开发系统应用?
仅要求 Android Marshmallow 的运行时权限?
在 Genymotion 中测试打盹功能(Android 6.0 Marshmallow)
如何在 Android 6.0 Marshmallow 中访问相机?