可以在 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 中访问相机?

如何在打盹模式下移动设备(Android Preview M / Marshmallow)?

如何在 Android Marshmallow (API>=23) 中获取 Chrome 历史记录和书签?