summaryrefslogtreecommitdiffstats
path: root/src/corelib/kernel/qpermissions_android.cpp
blob: 6c21ded72cddc0f27edbb97f2d0e391a2989b69d (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
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qpermissions.h"
#include "qpermissions_p.h"

#include <QtCore/qstringlist.h>
#include <QtCore/qfuture.h>
#include <QtCore/qhash.h>

#include "private/qandroidextras_p.h"

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

static QStringList nativeLocationPermission(const QLocationPermission &permission)
{
    QStringList nativeLocationPermissionList;
    const int sdkVersion = QtAndroidPrivate::androidSdkVersion();
    static QString backgroundLocation = u"android.permission.ACCESS_BACKGROUND_LOCATION"_s;
    static QString fineLocation = u"android.permission.ACCESS_FINE_LOCATION"_s;
    static QString coarseLocation = u"android.permission.ACCESS_COARSE_LOCATION"_s;

    // Since Android API 30, background location cannot be requested along
    // with fine or coarse location, but it should be requested separately after
    // the latter have been granted, see
    // https://developer.android.com/training/location/permissions
    if (sdkVersion < 30 || permission.availability() == QLocationPermission::WhenInUse) {
        if (permission.accuracy() == QLocationPermission::Approximate) {
            nativeLocationPermissionList << coarseLocation;
        } else {
            nativeLocationPermissionList << fineLocation;
            // Since Android API 31, if precise location is requested, it's advised
            // to request both fine and coarse location permissions, see
            // https://developer.android.com/training/location/permissions#approximate-request
            if (sdkVersion >= 31)
                nativeLocationPermissionList << coarseLocation;
        }
    }

    // NOTE: before Android API 29, background permission doesn't exist yet.

    // Keep the background permission in front to be able to use first()
    // on the list in checkPermission() because it takes single permission.
    if (sdkVersion >= 29 && permission.availability() == QLocationPermission::Always)
        nativeLocationPermissionList.prepend(backgroundLocation);

    return nativeLocationPermissionList;
}

static QStringList nativeBluetoothPermission(const QBluetoothPermission &permission)
{
    // See https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
    // for the details.

    // API Level < 31
    static QString bluetoothGeneral = u"android.permission.BLUETOOTH"_s;
    // API Level >= 31
    static QString bluetoothScan = u"android.permission.BLUETOOTH_SCAN"_s;
    static QString bluetoothAdvertise = u"android.permission.BLUETOOTH_ADVERTISE"_s;
    static QString bluetoothConnect = u"android.permission.BLUETOOTH_CONNECT"_s;
    // Fine location is currently required for ALL API levels, but that is not
    // strictly necessary for API Level >= 31. See QTBUG-112164.
    static QString fineLocation = u"android.permission.ACCESS_FINE_LOCATION"_s;

    if (QtAndroidPrivate::androidSdkVersion() < 31) {
        return {bluetoothGeneral, fineLocation};
    } else {
        const auto modes = permission.communicationModes();
        QStringList permissionList;
        if (modes & QBluetoothPermission::Advertise)
            permissionList << bluetoothAdvertise;
        if (modes & QBluetoothPermission::Access)
            permissionList << bluetoothScan << bluetoothConnect << fineLocation;
        return permissionList;
    }
}

static QStringList nativeStringsFromPermission(const QPermission &permission)
{
    const auto id = permission.type().id();
    if (id == qMetaTypeId<QLocationPermission>()) {
        return nativeLocationPermission(*permission.value<QLocationPermission>());
    } else if (id == qMetaTypeId<QCameraPermission>()) {
        return { u"android.permission.CAMERA"_s };
    } else if (id == qMetaTypeId<QMicrophonePermission>()) {
        return { u"android.permission.RECORD_AUDIO"_s };
    } else if (id == qMetaTypeId<QBluetoothPermission>()) {
        return nativeBluetoothPermission(*permission.value<QBluetoothPermission>());
    } else if (id == qMetaTypeId<QContactsPermission>()) {
        const auto readContactsString = u"android.permission.READ_CONTACTS"_s;
        switch (permission.value<QContactsPermission>()->accessMode()) {
        case QContactsPermission::AccessMode::ReadOnly:
            return { readContactsString };
        case QContactsPermission::AccessMode::ReadWrite:
            return { readContactsString, u"android.permission.WRITE_CONTACTS"_s };
        }
        Q_UNREACHABLE_RETURN({});
    } else if (id == qMetaTypeId<QCalendarPermission>()) {
        const auto readContactsString = u"android.permission.READ_CALENDAR"_s;
        switch (permission.value<QCalendarPermission>()->accessMode()) {
        case QCalendarPermission::AccessMode::ReadOnly:
            return { readContactsString };
        case QCalendarPermission::AccessMode::ReadWrite:
            return { readContactsString, u"android.permission.WRITE_CALENDAR"_s };
        }
        Q_UNREACHABLE_RETURN({});
    }

    return {};
}

static Qt::PermissionStatus
permissionStatusForAndroidResult(QtAndroidPrivate::PermissionResult result)
{
    switch (result) {
    case QtAndroidPrivate::PermissionResult::Authorized: return Qt::PermissionStatus::Granted;
    case QtAndroidPrivate::PermissionResult::Denied: return Qt::PermissionStatus::Denied;
    default: return Qt::PermissionStatus::Undetermined;
    }
}

using PermissionStatusHash = QHash<int, Qt::PermissionStatus>;
Q_GLOBAL_STATIC_WITH_ARGS(PermissionStatusHash, g_permissionStatusHash, ({
        { qMetaTypeId<QCameraPermission>(), Qt::PermissionStatus::Undetermined },
        { qMetaTypeId<QMicrophonePermission>(), Qt::PermissionStatus::Undetermined },
        { qMetaTypeId<QBluetoothPermission>(), Qt::PermissionStatus::Undetermined },
        { qMetaTypeId<QContactsPermission>(), Qt::PermissionStatus::Undetermined },
        { qMetaTypeId<QCalendarPermission>(), Qt::PermissionStatus::Undetermined },
        { qMetaTypeId<QLocationPermission>(), Qt::PermissionStatus::Undetermined }
}));

static Qt::PermissionStatus
getCombinedStatus(const QList<QtAndroidPrivate::PermissionResult> &androidResults)
{
    // Android returns only Denied or Granted
    for (const auto &result : androidResults) {
        const auto status = permissionStatusForAndroidResult(result);
        if (status == Qt::PermissionStatus::Denied)
            return status;
    }
    return Qt::PermissionStatus::Granted;
}

namespace QPermissions::Private
{
    Qt::PermissionStatus checkPermission(const QPermission &permission)
    {
        const auto nativePermissionList = nativeStringsFromPermission(permission);
        if (nativePermissionList.isEmpty())
            return Qt::PermissionStatus::Granted;

        QList<QtAndroidPrivate::PermissionResult> androidResults;
        androidResults.reserve(nativePermissionList.size());
        for (const auto &nativePermission : nativePermissionList)
            androidResults.push_back(QtAndroidPrivate::checkPermission(nativePermission).result());

        const auto status = getCombinedStatus(androidResults);
        const auto it = g_permissionStatusHash->constFind(permission.type().id());
        const bool foundStatus = (it != g_permissionStatusHash->constEnd());
        const bool itUndetermined = foundStatus && (*it) == Qt::PermissionStatus::Undetermined;
        if (status == Qt::PermissionStatus::Denied && itUndetermined)
            return Qt::PermissionStatus::Undetermined;
        return status;
    }

    void requestPermission(const QPermission &permission,
                           const QPermissions::Private::PermissionCallback &callback)
    {
        const auto nativePermissionList = nativeStringsFromPermission(permission);
        if (nativePermissionList.isEmpty()) {
            callback(Qt::PermissionStatus::Granted);
            return;
        }

        QtAndroidPrivate::requestPermissions(nativePermissionList).then(qApp,
            [callback, permission](QFuture<QtAndroidPrivate::PermissionResult> future) {
                const auto androidResults = future.isValid() ? future.results()
                                                             : QList{QtAndroidPrivate::Denied};
                const auto status = getCombinedStatus(androidResults);
                g_permissionStatusHash->insert(permission.type().id(), status);
                callback(status);
            }
        );
    }
}

QT_END_NAMESPACE