summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
blob: c1b38f61cbb411b5eceaac7050d9498be8e633b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.permissions;

import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;

import org.mozilla.gecko.util.ThreadUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Convenience class for checking and prompting for runtime permissions.
 *
 * Example:
 *
 *    Permissions.from(activity)
 *               .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
 *               .onUiThread()
 *               .andFallback(onPermissionDenied())
 *               .run(onPermissionGranted())
 *
 * This example will run the runnable returned by onPermissionGranted() if the WRITE_EXTERNAL_STORAGE permission is
 * already granted. Otherwise it will prompt the user and run the runnable returned by onPermissionGranted() or
 * onPermissionDenied() depending on whether the user accepted or not. If onUiThread() is specified then all callbacks
 * will be run on the UI thread.
 */
public class Permissions {
    private static final Queue<PermissionBlock> waiting = new LinkedList<>();
    private static final Queue<PermissionBlock> prompt = new LinkedList<>();

    private static PermissionsHelper permissionHelper = new PermissionsHelper();

    /**
     * Entry point for checking (and optionally prompting for) runtime permissions.
     *
     * Note: The provided context needs to be an Activity context in order to prompt. Use doNotPrompt()
     * for all other contexts.
     */
    public static PermissionBlock from(@NonNull Context context) {
        return new PermissionBlock(context, permissionHelper);
    }

    /**
     * This method will block until the specified permissions have been granted or denied by the user.
     * If needed the user will be prompted.
     *
     * @return true if all of the permissions have been granted. False if any of the permissions have been denied.
     */
    public static boolean waitFor(@NonNull Activity activity, String... permissions) {
        ThreadUtils.assertNotOnUiThread(); // We do not want to block the UI thread.

        // This task will block until all of the permissions have been granted
        final FutureTask<Boolean> blockingTask = new FutureTask<>(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return true;
            }
        });

        // This runnable will cancel the task if any of the permissions have been denied
        Runnable cancelBlockingTask = new Runnable() {
            @Override
            public void run() {
                blockingTask.cancel(true);
            }
        };

        Permissions.from(activity)
                .withPermissions(permissions)
                .andFallback(cancelBlockingTask)
                .run(blockingTask);

        try {
            return blockingTask.get();
        } catch (InterruptedException | ExecutionException | CancellationException e) {
            return false;
        }
    }

    /**
     * Determine whether you have been granted particular permissions.
     */
    public static boolean has(Context context, String... permissions) {
        return permissionHelper.hasPermissions(context, permissions);
    }

    /* package-private */ static void setPermissionHelper(PermissionsHelper permissionHelper) {
        Permissions.permissionHelper = permissionHelper;
    }

    /**
     * Callback for Activity.onRequestPermissionsResult(). All activities that prompt for permissions using this class
     * should implement onRequestPermissionsResult() and call this method.
     */
    public static synchronized void onRequestPermissionsResult(@NonNull Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
        processGrantResults(permissions, grantResults);

        processQueue(activity, permissions, grantResults);
    }

    /* package-private */ static synchronized void prompt(Activity activity, PermissionBlock block) {
        if (prompt.isEmpty()) {
            prompt.add(block);
            showPrompt(activity);
        } else {
            waiting.add(block);
        }
    }

    private static synchronized void processGrantResults(@NonNull String[] permissions, @NonNull int[] grantResults) {
        final HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults);

        while (!prompt.isEmpty()) {
            final PermissionBlock block = prompt.poll();

            if (allPermissionsGranted(block, grantedPermissions)) {
                block.onPermissionsGranted();
            } else {
                block.onPermissionsDenied();
            }
        }
    }

    private static synchronized void processQueue(Activity activity, String[] permissions, int[] grantResults) {
        final HashSet<String> deniedPermissions = collectDeniedPermissions(permissions, grantResults);

        while (!waiting.isEmpty()) {
            final PermissionBlock block = waiting.poll();

            if (block.hasPermissions(activity)) {
                block.onPermissionsGranted();
            } else {
                if (atLeastOnePermissionDenied(block, deniedPermissions)) {
                    // We just prompted the user and one of the permissions of this block has been denied:
                    // There's no reason to instantly prompt again; Just reject without prompting.
                    block.onPermissionsDenied();
                } else {
                    prompt.add(block);
                }
            }
        }

        if (!prompt.isEmpty()) {
            showPrompt(activity);
        }
    }

    private static synchronized void showPrompt(Activity activity) {
        HashSet<String> permissions = new HashSet<>();

        for (PermissionBlock block : prompt) {
            Collections.addAll(permissions, block.getPermissions());
        }

        permissionHelper.prompt(activity, permissions.toArray(new String[permissions.size()]));
    }

    private static HashSet<String> collectGrantedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
        return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_GRANTED);
    }

    private static HashSet<String> collectDeniedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
        return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_DENIED);
    }

    private static HashSet<String> filterPermissionsByResult(@NonNull String[] permissions, @NonNull int[] grantResults, int result) {
        HashSet<String> grantedPermissions = new HashSet<>(permissions.length);
        for (int i = 0; i < permissions.length; i++) {
            if (grantResults[i] == result) {
                grantedPermissions.add(permissions[i]);
            }
        }
        return grantedPermissions;
    }

    private static boolean allPermissionsGranted(PermissionBlock block, HashSet<String> grantedPermissions) {
        for (String permission : block.getPermissions()) {
            if (!grantedPermissions.contains(permission)) {
                return false;
            }
        }

        return true;
    }

    private static boolean atLeastOnePermissionDenied(PermissionBlock block, HashSet<String> deniedPermissions) {
        for (String permission : block.getPermissions()) {
            if (deniedPermissions.contains(permission)) {
                return true;
            }
        }

        return false;
    }
}