diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml
index 6c6d74cd3..0c100a139 100644
--- a/apps/toolbox/src/main-page.xml
+++ b/apps/toolbox/src/main-page.xml
@@ -19,6 +19,7 @@
+
diff --git a/apps/toolbox/src/pages/permissions.ts b/apps/toolbox/src/pages/permissions.ts
new file mode 100644
index 000000000..b43e26070
--- /dev/null
+++ b/apps/toolbox/src/pages/permissions.ts
@@ -0,0 +1,47 @@
+import { Observable, EventData, Page, Permissions, Trace } from '@nativescript/core';
+
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new PermissionsModel();
+ Trace.enable();
+ Trace.setCategories(Trace.categories.Permissions);
+}
+
+export class PermissionsModel extends Observable {
+ permissions = ['location', 'camera', 'microphone', 'photo', 'contacts', 'event', 'reminder', 'bluetooth', 'bluetoothScan', 'notification', 'backgroundRefresh', 'speechRecognition', 'mediaLibrary', 'motion', 'location', 'callPhone', 'readSms', 'receiveSms'].map((v) => {
+ return {
+ name: v,
+ checkPermission: this.checkPermission.bind(this),
+ requestPermission: this.requestPermission.bind(this),
+ };
+ });
+
+ constructor() {
+ super();
+ }
+
+ async checkPermission(args) {
+ const perm = args.object.bindingContext.name;
+ try {
+ console.log('checkPermission', perm);
+ const result = await Permissions.check(perm, { type: 'none' });
+ alert(JSON.stringify(result));
+ } catch (err) {
+ console.error(err);
+ alert(err);
+ }
+ }
+ async requestPermission(args) {
+ const perm = args.object.bindingContext.name;
+ try {
+ console.log('requestPermission', perm);
+ const result = await Permissions.request(perm, { type: 'none' });
+ alert(JSON.stringify(result));
+ } catch (err) {
+ console.error(err);
+ alert(err);
+ }
+ }
+}
diff --git a/apps/toolbox/src/pages/permissions.xml b/apps/toolbox/src/pages/permissions.xml
new file mode 100644
index 000000000..0e35299ba
--- /dev/null
+++ b/apps/toolbox/src/pages/permissions.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts
index 3c75a31ad..59f3e418b 100644
--- a/packages/core/index.d.ts
+++ b/packages/core/index.d.ts
@@ -98,6 +98,7 @@ export type { ImageAssetOptions } from './image-asset';
export { ImageSource } from './image-source';
export { ModuleNameResolver, _setResolver } from './module-name-resolver';
export type { ModuleListProvider, PlatformContext } from './module-name-resolver';
+export { Permissions, PermissionStatus } from './permissions';
export { isAndroid, isIOS, Screen, Device, platformNames } from './platform';
export type { IDevice } from './platform';
export { profile, enable as profilingEnable, disable as profilingDisable, time as profilingTime, uptime as profilingUptime, start as profilingStart, stop as profilingStop, isRunning as profilingIsRunning, dumpProfiles as profilingDumpProfiles, resetProfiles as profilingResetProfiles, startCPUProfile as profilingStartCPU, stopCPUProfile as profilingStopCPU } from './profiling';
diff --git a/packages/core/index.ts b/packages/core/index.ts
index bc57df100..afdcb945f 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -123,6 +123,9 @@ export type { ImageAssetOptions } from './image-asset';
export { ImageSource } from './image-source';
export { ModuleNameResolver, _setResolver } from './module-name-resolver';
export type { ModuleListProvider, PlatformContext } from './module-name-resolver';
+
+// Permissions
+export { Permissions, PermissionStatus } from './permissions';
export { isAndroid, isIOS, Screen, Device, platformNames } from './platform';
export type { IDevice } from './platform';
diff --git a/packages/core/permissions/common.ts b/packages/core/permissions/common.ts
new file mode 100644
index 000000000..b90ff42e1
--- /dev/null
+++ b/packages/core/permissions/common.ts
@@ -0,0 +1,8 @@
+export enum PermissionStatus {
+ authorized = 'authorized',
+ denied = 'denied',
+ limited = 'limited',
+ restricted = 'restricted',
+ undetermined = 'undetermined',
+ never_ask_again = 'never_ask_again',
+}
diff --git a/packages/core/permissions/index.android.ts b/packages/core/permissions/index.android.ts
new file mode 100644
index 000000000..52b2632f3
--- /dev/null
+++ b/packages/core/permissions/index.android.ts
@@ -0,0 +1,419 @@
+import { Trace } from '../trace';
+import { AndroidActivityRequestPermissionsEventData, AndroidApplication, android as androidApp } from '../application';
+import { getBoolean, setBoolean } from '../application-settings';
+import { PermissionStatus } from './common';
+import { PermissionCheckOptions, PermissionsType, PermissionRationale, PermissionRequestOptions } from '.';
+
+export * from './common';
+
+let ANDROID_SDK = -1;
+function getAndroidSDK() {
+ if (ANDROID_SDK === -1) {
+ ANDROID_SDK = android.os.Build.VERSION.SDK_INT;
+ }
+ return ANDROID_SDK;
+}
+
+const MARSHMALLOW = 23;
+const ANDROIDQ = 29;
+const ANDROIDS = 31;
+
+const NativePermissionsTypes: PermissionsType[] = ['location', 'camera', 'mediaLocation', 'microphone', 'contacts', 'event', 'storage', 'photo', 'callPhone', 'readSms', 'receiveSms', 'bluetoothScan', 'bluetoothConnect', 'bluetooth'];
+type NativePermissionsNames = typeof NativePermissionsTypes; // type Names = readonly ['Mike', 'Jeff', 'Ben']
+type NativePermissions = NativePermissionsNames[number];
+function getNativePermissions(permission: NativePermissions, options?) {
+ switch (permission) {
+ case 'location': {
+ const result = [android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION];
+ if (getAndroidSDK() >= ANDROIDQ) {
+ const type = typeof options === 'string' ? options : options && options.type;
+ if (type === 'always') {
+ result.push(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
+ }
+ }
+ return result;
+ }
+ case 'camera': {
+ return [android.Manifest.permission.CAMERA];
+ }
+ case 'mediaLocation': {
+ if (getAndroidSDK() >= ANDROIDQ) {
+ return [android.Manifest.permission.ACCESS_MEDIA_LOCATION];
+ }
+ break;
+ }
+ case 'microphone': {
+ return [android.Manifest.permission.RECORD_AUDIO];
+ }
+ case 'contacts': {
+ return [android.Manifest.permission.READ_CONTACTS];
+ }
+ case 'event': {
+ return [android.Manifest.permission.READ_CALENDAR];
+ }
+ case 'storage': {
+ return [android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE];
+ }
+ case 'photo': {
+ return [android.Manifest.permission.WRITE_EXTERNAL_STORAGE];
+ }
+ case 'callPhone': {
+ return [android.Manifest.permission.CALL_PHONE];
+ }
+ case 'readSms': {
+ return [android.Manifest.permission.READ_SMS];
+ }
+ case 'receiveSms': {
+ return [android.Manifest.permission.RECEIVE_SMS];
+ }
+ case 'bluetoothScan': {
+ if (getAndroidSDK() >= ANDROIDS) {
+ return [android.Manifest.permission.BLUETOOTH_SCAN];
+ }
+ break;
+ }
+ case 'bluetoothConnect': {
+ if (getAndroidSDK() >= ANDROIDS) {
+ return [android.Manifest.permission.BLUETOOTH_CONNECT];
+ }
+ break;
+ }
+ case 'bluetooth': {
+ if (getAndroidSDK() >= ANDROIDS) {
+ return [android.Manifest.permission.BLUETOOTH_ADVERTISE];
+ }
+ break;
+ }
+ }
+ return [];
+}
+
+const STORAGE_KEY = '@NSPermissions:didAskPermission:';
+
+const setDidAskOnce = (permission: string) => Promise.resolve().then(() => setBoolean(STORAGE_KEY + permission, true));
+
+const getDidAskOnce = (permission: string) => Promise.resolve(!!getBoolean(STORAGE_KEY + permission));
+
+namespace PermissionsAndroid {
+ /**
+ * A list of specified "dangerous" permissions that require prompting the user
+ */
+ // export const PERMISSIONS = {
+ // READ_CALENDAR: 'android.permission.READ_CALENDAR',
+ // WRITE_CALENDAR: 'android.permission.WRITE_CALENDAR',
+ // CAMERA: 'android.permission.CAMERA',
+ // READ_CONTACTS: 'android.permission.READ_CONTACTS',
+ // WRITE_CONTACTS: 'android.permission.WRITE_CONTACTS',
+ // GET_ACCOUNTS: 'android.permission.GET_ACCOUNTS',
+ // ACCESS_FINE_LOCATION: 'android.permission.ACCESS_FINE_LOCATION',
+ // ACCESS_COARSE_LOCATION: 'android.permission.ACCESS_COARSE_LOCATION',
+ // RECORD_AUDIO: 'android.permission.RECORD_AUDIO',
+ // READ_PHONE_STATE: 'android.permission.READ_PHONE_STATE',
+ // CALL_PHONE: 'android.permission.CALL_PHONE',
+ // READ_CALL_LOG: 'android.permission.READ_CALL_LOG',
+ // WRITE_CALL_LOG: 'android.permission.WRITE_CALL_LOG',
+ // ADD_VOICEMAIL: 'com.android.voicemail.permission.ADD_VOICEMAIL',
+ // USE_SIP: 'android.permission.USE_SIP',
+ // PROCESS_OUTGOING_CALLS: 'android.permission.PROCESS_OUTGOING_CALLS',
+ // BODY_SENSORS: 'android.permission.BODY_SENSORS',
+ // SEND_SMS: 'android.permission.SEND_SMS',
+ // RECEIVE_SMS: 'android.permission.RECEIVE_SMS',
+ // READ_SMS: 'android.permission.READ_SMS',
+ // RECEIVE_WAP_PUSH: 'android.permission.RECEIVE_WAP_PUSH',
+ // RECEIVE_MMS: 'android.permission.RECEIVE_MMS',
+ // READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE',
+ // WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE'
+ // };
+
+ export const RESULTS: { GRANTED: PermissionStatus; DENIED: PermissionStatus; NEVER_ASK_AGAIN: PermissionStatus } = {
+ GRANTED: PermissionStatus.authorized,
+ DENIED: PermissionStatus.denied,
+ NEVER_ASK_AGAIN: PermissionStatus.never_ask_again,
+ };
+
+ /**
+ * Returns a promise resolving to a boolean value as to whether the specified
+ * permissions has been granted
+ *
+ * See https://facebook.github.io/react-native/docs/permissionsandroid.html#check
+ */
+ export async function check(permission: string | string[]) {
+ const context: android.content.Context = androidApp.foregroundActivity || androidApp.startActivity;
+ let result = true;
+ const granted = android.content.pm.PackageManager.PERMISSION_GRANTED;
+ if (!Array.isArray(permission)) {
+ permission = [permission];
+ }
+ if (getAndroidSDK() < MARSHMALLOW) {
+ permission.forEach((p) => (result = result && context.checkPermission(p, android.os.Process.myPid(), android.os.Process.myUid()) === granted));
+ } else {
+ permission.forEach((p) => (result = result && context.checkSelfPermission(p) === granted));
+ }
+ return result;
+ }
+
+ /**
+ * Prompts the user to enable a permission and returns a promise resolving to a
+ * string value indicating whether the user allowed or denied the request
+ *
+ * See https://facebook.github.io/react-native/docs/permissionsandroid.html#request
+ */
+ export async function request(permission: string, rationale?: PermissionRationale): Promise {
+ // if (rationale) {
+ // const shouldShowRationale = await shouldShowRequestPermissionRationale(permission);
+
+ // if (shouldShowRationale) {
+ // return new Promise((resolve, reject) => {
+
+ // NativeModules.DialogManagerAndroid.showAlert(rationale, () => reject(new Error('Error showing rationale')), () => resolve(requestPermission(permission)));
+ // });
+ // }
+ // }
+ return requestPermission(permission);
+ }
+
+ /**
+ * Prompts the user to enable multiple permissions in the same dialog and
+ * returns an object with the permissions as keys and strings as values
+ * indicating whether the user allowed or denied the request
+ *
+ * See https://facebook.github.io/react-native/docs/permissionsandroid.html#requestmultiple
+ */
+ export function requestMultiple(permissions: string[]): Promise<{ [permission: string]: PermissionStatus }> {
+ return requestMultiplePermissions(permissions);
+ }
+}
+
+// PermissionsAndroid = new PermissionsAndroid();
+
+let mRequestCode = 0;
+function requestPermission(permission: string): Promise {
+ const activity: android.app.Activity = androidApp.foregroundActivity || androidApp.startActivity;
+ if (getAndroidSDK() < MARSHMALLOW) {
+ return Promise.resolve(activity.checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid()) === android.content.pm.PackageManager.PERMISSION_GRANTED ? PermissionsAndroid.RESULTS.GRANTED : PermissionsAndroid.RESULTS.DENIED);
+ }
+ if (activity.checkSelfPermission(permission) === android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ return Promise.resolve(PermissionsAndroid.RESULTS.GRANTED);
+ }
+
+ return new Promise((resolve, reject) => {
+ try {
+ const requestCode = mRequestCode++;
+ activity.requestPermissions([permission], requestCode);
+ androidApp.on(AndroidApplication.activityRequestPermissionsEvent, (args: AndroidActivityRequestPermissionsEventData) => {
+ if (args.requestCode === requestCode) {
+ if (args.grantResults.length > 0) {
+ if (args.grantResults.length > 0 && args.grantResults[0] === android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ resolve(PermissionsAndroid.RESULTS.GRANTED);
+ } else {
+ if (activity.shouldShowRequestPermissionRationale(permission)) {
+ resolve(PermissionsAndroid.RESULTS.DENIED);
+ } else {
+ resolve(PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN);
+ }
+ }
+ } else {
+ // it is possible that the permissions request interaction with the user is interrupted. In this case you will receive empty permissions and results arrays which should be treated as a cancellation.
+ reject();
+ }
+ }
+ });
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+async function requestMultiplePermissions(permissions: string[]): Promise<{ [permission: string]: PermissionStatus }> {
+ const grantedPermissions = {};
+ const permissionsToCheck = [];
+ let checkedPermissionsCount = 0;
+ if (Trace.isEnabled()) {
+ Trace.write(`requestMultiplePermissions ${permissions}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ const context: android.content.Context = androidApp.foregroundActivity || androidApp.startActivity;
+
+ for (let i = 0; i < permissions.length; i++) {
+ const perm = permissions[i];
+
+ if (getAndroidSDK() < MARSHMALLOW) {
+ grantedPermissions[perm] = context.checkPermission(perm, android.os.Process.myPid(), android.os.Process.myUid()) === android.content.pm.PackageManager.PERMISSION_GRANTED ? PermissionsAndroid.RESULTS.GRANTED : PermissionsAndroid.RESULTS.DENIED;
+ checkedPermissionsCount++;
+ } else if (context.checkSelfPermission(perm) === android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ grantedPermissions[perm] = PermissionsAndroid.RESULTS.GRANTED;
+ checkedPermissionsCount++;
+ } else {
+ permissionsToCheck.push(perm);
+ }
+ }
+ if (permissions.length === checkedPermissionsCount) {
+ return grantedPermissions;
+ }
+ const activity: android.app.Activity = androidApp.foregroundActivity || androidApp.startActivity;
+ return new Promise((resolve, reject) => {
+ try {
+ const requestCode = mRequestCode++;
+
+ activity.requestPermissions(permissionsToCheck, requestCode);
+ androidApp.on(AndroidApplication.activityRequestPermissionsEvent, (args: AndroidActivityRequestPermissionsEventData) => {
+ if (args.requestCode === requestCode) {
+ const results = args.grantResults;
+ for (let j = 0; j < permissionsToCheck.length; j++) {
+ const permission = permissionsToCheck[j];
+ if (results.length > j && results[j] === android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ grantedPermissions[permission] = PermissionsAndroid.RESULTS.GRANTED;
+ } else {
+ if (activity.shouldShowRequestPermissionRationale(permission)) {
+ grantedPermissions[permission] = PermissionsAndroid.RESULTS.DENIED;
+ } else {
+ grantedPermissions[permission] = PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN;
+ }
+ }
+ }
+ resolve(grantedPermissions);
+ }
+
+ // if (args.grantResults.length > 0 && args.grantResults[0] === android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ // resolve(PermissionStatus.GRANTED);
+ // } else {
+ // if (activity.shouldShowRequestPermissionRationale(permission)) {
+ // resolve(PermissionStatus.DENIED);
+ // } else {
+ // resolve(PermissionStatus.NEVER_ASK_AGAIN);
+ // }
+ // }
+ });
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+function shouldShowRequestPermissionRationale(permission: string | string[]) {
+ if (getAndroidSDK() < MARSHMALLOW) {
+ return Promise.resolve(false);
+ }
+ const activity: android.app.Activity = androidApp.foregroundActivity || androidApp.startActivity;
+ try {
+ if (Array.isArray(permission)) {
+ return Promise.resolve(permission.reduce((accu, p) => accu && activity.shouldShowRequestPermissionRationale(p), true));
+ }
+ return Promise.resolve(activity.shouldShowRequestPermissionRationale(permission));
+ } catch (e) {
+ return Promise.reject(e);
+ }
+}
+
+export class Permissions {
+ static canOpenSettings() {
+ return Promise.resolve(true);
+ }
+
+ static openSettings() {
+ const activity = androidApp.foregroundActivity || androidApp.startActivity;
+ return new Promise((resolve, reject) => {
+ const onActivityResultHandler = (data) => {
+ if (data.requestCode === 5140) {
+ androidApp.off(AndroidApplication.activityResultEvent, onActivityResultHandler);
+ resolve();
+ }
+ };
+ androidApp.on(AndroidApplication.activityResultEvent, onActivityResultHandler);
+ const intent = new android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(android.net.Uri.parse('package:' + activity.getPackageName()));
+ activity.startActivityForResult(intent, 5140);
+ });
+ }
+
+ static getTypes() {
+ return NativePermissionsTypes;
+ }
+
+ static async check(permission: PermissionsType, options?: PermissionCheckOptions): Promise<[PermissionStatus, boolean]> {
+ if (Trace.isEnabled()) {
+ Trace.write(`check ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ const perms: string | string[] = getNativePermissions(permission, options);
+ if (!perms) {
+ if (Trace.isEnabled()) {
+ Trace.write(`${permission} is not a valid permission type on Android version`, Trace.categories.Permissions, Trace.messageType.warn);
+ }
+ return [PermissionStatus.authorized, true];
+ }
+
+ const isAuthorized = await PermissionsAndroid.check(perms);
+ if (isAuthorized) {
+ if (getAndroidSDK() >= ANDROIDQ && permission === 'location') {
+ const type = typeof options === 'string' ? options : options && options.type;
+ if (type === 'always') {
+ const backAuthorized = await PermissionsAndroid.check(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
+ return [PermissionStatus.authorized, backAuthorized];
+ }
+ }
+ return [PermissionStatus.authorized, true];
+ }
+
+ return getDidAskOnce(permission).then((didAsk) => {
+ if (didAsk) {
+ return shouldShowRequestPermissionRationale(perms).then((shouldShow) => [shouldShow ? PermissionStatus.denied : PermissionStatus.restricted, true]);
+ }
+
+ return [PermissionStatus.undetermined, true];
+ });
+ }
+
+ static request(permission: PermissionsType | PermissionsType[] | string[], options?: PermissionRequestOptions): Promise<[PermissionStatus, boolean] | { [permission: string]: PermissionStatus }> {
+ if (Trace.isEnabled()) {
+ Trace.write(`request ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ let types: string[] = [];
+ if (Array.isArray(permission)) {
+ permission.forEach((s) => {
+ if (s.startsWith('android.permission.')) {
+ types.push(s);
+ } else {
+ types.push(...getNativePermissions(s as PermissionsType, options));
+ }
+ });
+ } else {
+ if (permission.startsWith('android.permission.')) {
+ types.push(permission);
+ } else {
+ types = getNativePermissions(permission, options);
+ }
+ }
+ if (types.length === 0) {
+ return Promise.resolve([PermissionStatus.authorized, true]);
+ }
+
+ const rationale = typeof options === 'string' ? undefined : options && options.rationale;
+ if (types.length > 1) {
+ return requestMultiplePermissions(types);
+ }
+ return PermissionsAndroid.request(types[0], rationale).then((result) => {
+ // PermissionsAndroid.request() to native module resolves to boolean
+ // rather than string if running on OS version prior to Android M
+ if (typeof result === 'boolean') {
+ return [result ? PermissionStatus.authorized : PermissionStatus.denied, true];
+ }
+
+ if (Array.isArray(permission)) {
+ return Promise.all(permission.map(setDidAskOnce)).then(() => [result as PermissionStatus, true]);
+ }
+ return setDidAskOnce(permission).then(() => [result as PermissionStatus, true]);
+ });
+ }
+
+ static checkMultiple(permissions: PermissionsType[]) {
+ if (Trace.isEnabled()) {
+ Trace.write(`checkMultiple ${permissions}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ return Promise.all(permissions.map((permission) => this.check(permission))).then((result) =>
+ result.reduce((acc, value, index) => {
+ const name = permissions[index];
+ acc[name] = value;
+ return acc;
+ }, {})
+ );
+ }
+}
diff --git a/packages/core/permissions/index.d.ts b/packages/core/permissions/index.d.ts
new file mode 100644
index 000000000..a66dc15fb
--- /dev/null
+++ b/packages/core/permissions/index.d.ts
@@ -0,0 +1,21 @@
+export * from './common';
+export type PermissionsType = 'location' | 'camera' | 'microphone' | 'photo' | 'contacts' | 'event' | 'reminder' | 'bluetooth' | 'bluetoothConnect' | 'bluetoothScan' | 'notification' | 'backgroundRefresh' | 'speechRecognition' | 'mediaLocation' | 'mediaLibrary' | 'motion' | 'storage' | 'callPhone' | 'readSms' | 'receiveSms';
+export interface PermissionRationale {
+ title: string;
+ message: string;
+ buttonPositive?: string;
+ buttonNegative?: string;
+ buttonNeutral?: string;
+}
+export type PermissionCheckOptions = string | { type: string };
+export type PermissionRequestOptions = string | { type: string; rationale?: PermissionRationale };
+export type PermissionResult = T extends any[] ? { [k: string]: Status } : [Status, boolean];
+
+export class Permissions {
+ static canOpenSettings(): Promise;
+ static openSettings(): Promise;
+ static getTypes(): Permissions[];
+ static check(permission: T, options?: CheckOptions): Promise>;
+ static request(permission: T, options?: RequestOptions): Promise>;
+ static checkMultiple(permissions: T): Promise>;
+}
diff --git a/packages/core/permissions/index.ios.ts b/packages/core/permissions/index.ios.ts
new file mode 100644
index 000000000..56abebf3d
--- /dev/null
+++ b/packages/core/permissions/index.ios.ts
@@ -0,0 +1,788 @@
+import { Device } from '../platform';
+import { Trace } from '../trace';
+import { PermissionStatus } from './common';
+import { PermissionCheckOptions, PermissionsType, PermissionRequestOptions } from '.';
+
+export * from './common';
+
+export namespace PermissionsIOS {
+ namespace NSPLocation {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ function getStatusFromCLAuthorizationStatus(lStatus: CLAuthorizationStatus, type?: string): [PermissionStatus, boolean] {
+ let always = false;
+ switch (lStatus) {
+ case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways:
+ always = true;
+ status = PermissionStatus.authorized;
+ break;
+ case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse:
+ status = PermissionStatus.authorized;
+ break;
+ case CLAuthorizationStatus.kCLAuthorizationStatusDenied:
+ status = PermissionStatus.denied;
+ break;
+ case CLAuthorizationStatus.kCLAuthorizationStatusRestricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ if (Trace.isEnabled()) {
+ Trace.write(`NSPLocation getStatusFromCLAuthorizationStatus ${status}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ return [status, always];
+ }
+ export function getStatusForType(type?: string): [PermissionStatus, boolean] {
+ const status2 = CLLocationManager.authorizationStatus();
+ return getStatusFromCLAuthorizationStatus(status2, type);
+ }
+ let locationManager: CLLocationManager;
+ let locationManagerDelegate: CLLocationManagerDelegateImpl;
+ export type SubCLLocationManagerDelegate = Partial;
+ @NativeClass
+ export class CLLocationManagerDelegateImpl extends NSObject implements CLLocationManagerDelegate {
+ public static ObjCProtocols = [CLLocationManagerDelegate];
+
+ private subDelegates: SubCLLocationManagerDelegate[];
+
+ public addSubDelegate(delegate: SubCLLocationManagerDelegate) {
+ if (!this.subDelegates) {
+ this.subDelegates = [];
+ }
+ const index = this.subDelegates.indexOf(delegate);
+ if (index === -1) {
+ this.subDelegates.push(delegate);
+ }
+ }
+
+ public removeSubDelegate(delegate: SubCLLocationManagerDelegate) {
+ const index = this.subDelegates.indexOf(delegate);
+ if (index !== -1) {
+ this.subDelegates.splice(index, 1);
+ }
+ }
+ static new(): CLLocationManagerDelegateImpl {
+ return super.new() as CLLocationManagerDelegateImpl;
+ }
+ public initDelegate() {
+ this.subDelegates = [];
+ return this;
+ }
+ locationManagerDidChangeAuthorizationStatus(manager: CLLocationManager, status: CLAuthorizationStatus) {
+ this.subDelegates &&
+ this.subDelegates.forEach((d) => {
+ if (d.locationManagerDidChangeAuthorizationStatus) {
+ d.locationManagerDidChangeAuthorizationStatus(manager, status);
+ }
+ });
+ }
+ // locationManagerDidFailWithError(manager: CLLocationManager, error: NSError) {
+ // this.subDelegates &&
+ // this.subDelegates.forEach(d => {
+ // if (d.locationManagerDidFailWithError) {
+ // d.locationManagerDidFailWithError(manager, error);
+ // }
+ // });
+ // }
+ }
+ export function request(type): Promise<[PermissionStatus, boolean]> {
+ const status = getStatusForType(type);
+ if (Trace.isEnabled()) {
+ Trace.write(`NSPLocation request ${type}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ if (status[0] === PermissionStatus.undetermined) {
+ return new Promise((resolve, reject) => {
+ if (!locationManager) {
+ locationManager = CLLocationManager.new();
+ }
+ if (!locationManagerDelegate) {
+ locationManagerDelegate = CLLocationManagerDelegateImpl.new().initDelegate();
+ locationManager.delegate = locationManagerDelegate;
+ }
+ const subD = {
+ locationManagerDidChangeAuthorizationStatus: (manager, status: CLAuthorizationStatus) => {
+ if (Trace.isEnabled()) {
+ Trace.write(`locationManagerDidChangeAuthorizationStatus ${status}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ if (status !== CLAuthorizationStatus.kCLAuthorizationStatusNotDetermined) {
+ if (locationManagerDelegate) {
+ locationManagerDelegate.removeSubDelegate(subD);
+ locationManagerDelegate = null;
+ }
+ if (locationManager) {
+ locationManager.delegate = null;
+ locationManager = null;
+ }
+ const rStatus = getStatusFromCLAuthorizationStatus(status, type);
+ resolve(rStatus);
+ // } else {
+ // reject('kCLAuthorizationStatusNotDetermined');
+ }
+ },
+ };
+ locationManagerDelegate.addSubDelegate(subD);
+ try {
+ if (Trace.isEnabled()) {
+ Trace.write(`NSPLocation requestAuthorization ${type}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ if (type === 'always') {
+ locationManager.requestAlwaysAuthorization();
+ } else {
+ locationManager.requestWhenInUseAuthorization();
+ }
+ } catch (e) {
+ reject(e);
+ if (locationManagerDelegate) {
+ locationManagerDelegate.removeSubDelegate(subD);
+ locationManagerDelegate = null;
+ }
+ if (locationManager) {
+ locationManager.delegate = null;
+ locationManager = null;
+ }
+ }
+ });
+ } else {
+ // if (CLLocationManager.authorizationStatus() === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse && type === 'always') {
+ // return Promise.resolve(PermissionStatus.denied);
+ // } else {
+ return Promise.resolve(status);
+ // }
+ }
+ }
+ }
+ namespace NSPBluetooth {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ const status2 = CBPeripheralManager.authorizationStatus();
+ switch (status2) {
+ case CBPeripheralManagerAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case CBPeripheralManagerAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case CBPeripheralManagerAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+ export type SubCBPeripheralManagerDelegate = Partial;
+ @NativeClass
+ export class CBPeripheralManagerDelegateImpl extends NSObject implements CBPeripheralManagerDelegate {
+ public static ObjCProtocols = [CBPeripheralManagerDelegate];
+
+ private subDelegates: SubCBPeripheralManagerDelegate[];
+
+ public addSubDelegate(delegate: SubCBPeripheralManagerDelegate) {
+ const index = this.subDelegates.indexOf(delegate);
+ if (index === -1) {
+ this.subDelegates.push(delegate);
+ }
+ }
+
+ public removeSubDelegate(delegate: SubCBPeripheralManagerDelegate) {
+ const index = this.subDelegates.indexOf(delegate);
+ if (index !== -1) {
+ this.subDelegates.splice(index, 1);
+ }
+ }
+ static new(): CBPeripheralManagerDelegateImpl {
+ return super.new() as CBPeripheralManagerDelegateImpl;
+ }
+ public initDelegate(): CBPeripheralManagerDelegateImpl {
+ this.subDelegates = [];
+ return this;
+ }
+ peripheralManagerDidUpdateState(peripheralManager) {
+ this.subDelegates.forEach((d) => {
+ if (d.peripheralManagerDidUpdateState) {
+ d.peripheralManagerDidUpdateState(peripheralManager);
+ }
+ });
+ }
+ }
+ let peripheralManager: CBPeripheralManager;
+ export function request(): Promise<[PermissionStatus, boolean]> {
+ const status = getStatus();
+ if (status[0] === PermissionStatus.undetermined) {
+ return new Promise((resolve, reject) => {
+ if (!peripheralManager) {
+ peripheralManager = CBPeripheralManager.new();
+ peripheralManager.delegate = CBPeripheralManagerDelegateImpl.new().initDelegate();
+ }
+ const subD = {
+ peripheralManagerDidUpdateState: (peripheralManager) => {
+ if (peripheralManager) {
+ peripheralManager.stopAdvertising();
+ (peripheralManager.delegate as CBPeripheralManagerDelegateImpl).removeSubDelegate(subD);
+ peripheralManager.delegate = null;
+ peripheralManager = null;
+ }
+ // for some reason, checking permission right away returns denied. need to wait a tiny bit
+ setTimeout(() => {
+ resolve(getStatus());
+ }, 100);
+ },
+ };
+ (peripheralManager.delegate as CBPeripheralManagerDelegateImpl).addSubDelegate(subD);
+ try {
+ peripheralManager.startAdvertising(null);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ } else {
+ return Promise.resolve(status);
+ }
+ }
+ }
+ namespace NSPAudioVideo {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ function typeFromString(value: string) {
+ if (value === 'audio') {
+ return AVMediaTypeAudio;
+ } else {
+ return AVMediaTypeVideo;
+ }
+ }
+ export function getStatus(type?: string): [PermissionStatus, boolean] {
+ const videoStatus = AVCaptureDevice.authorizationStatusForMediaType(typeFromString(type));
+ switch (videoStatus) {
+ case AVAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case AVAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case AVAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(type): Promise<[PermissionStatus, boolean]> {
+ return new Promise((resolve, reject) => {
+ AVCaptureDevice.requestAccessForMediaTypeCompletionHandler(typeFromString(type), (granted) => resolve(getStatus(type)));
+ });
+ }
+ }
+ namespace NSPSpeechRecognition {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ const speechStatus = SFSpeechRecognizer.authorizationStatus();
+ switch (speechStatus) {
+ case SFSpeechRecognizerAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case SFSpeechRecognizerAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case SFSpeechRecognizerAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(): Promise<[PermissionStatus, boolean]> {
+ return new Promise((resolve) => {
+ SFSpeechRecognizer.requestAuthorization(() => resolve(getStatus()));
+ });
+ }
+ }
+ namespace NSPPhoto {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ let photoStatus: PHAuthorizationStatus;
+ if (parseFloat(Device.osVersion) >= 14) {
+ photoStatus = PHPhotoLibrary.authorizationStatusForAccessLevel(PHAccessLevel.ReadWrite);
+ } else {
+ photoStatus = PHPhotoLibrary.authorizationStatus();
+ }
+ switch (photoStatus) {
+ case PHAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case PHAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case PHAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(): Promise<[PermissionStatus, boolean]> {
+ return new Promise((resolve) => {
+ PHPhotoLibrary.requestAuthorization(() => resolve(getStatus()));
+ });
+ }
+ }
+ namespace NSPMotion {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ if (status === PermissionStatus.undetermined) {
+ const cmStatus = CMMotionActivityManager.authorizationStatus as any as CMAuthorizationStatus;
+ switch (cmStatus) {
+ case CMAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case CMAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case CMAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ }
+ }
+ return [status, true];
+ }
+
+ export function request(): Promise<[PermissionStatus, boolean]> {
+ if (status === PermissionStatus.undetermined) {
+ return new Promise((resolve) => {
+ let activityManager = CMMotionActivityManager.new();
+ let motionActivityQueue = NSOperationQueue.new();
+ if (Trace.isEnabled()) {
+ Trace.write(`NSPMotion request ${status}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ activityManager.queryActivityStartingFromDateToDateToQueueWithHandler(NSDate.distantPast, new Date(), motionActivityQueue, (activities, error) => {
+ if (error) {
+ status = PermissionStatus.denied;
+ } else if (activities || !error) {
+ status = PermissionStatus.authorized;
+ }
+ if (Trace.isEnabled()) {
+ Trace.write(`NSPMotion got response ${status}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ resolve([status, true]);
+ activityManager = null;
+ motionActivityQueue = null;
+ });
+ });
+ } else {
+ return Promise.resolve([status, true]);
+ }
+ }
+ }
+ namespace NSPMediaLibrary {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ const mediaStatus = MPMediaLibrary.authorizationStatus();
+ switch (mediaStatus) {
+ case MPMediaLibraryAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case MPMediaLibraryAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case MPMediaLibraryAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(): Promise<[PermissionStatus, boolean]> {
+ return new Promise((resolve) => {
+ MPMediaLibrary.requestAuthorization(() => resolve(getStatus()));
+ });
+ }
+ }
+ namespace NSPNotification {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ const NSPDidAskForNotification = 'NSPDidAskForNotification';
+ export async function getStatus(): Promise<[PermissionStatus, boolean]> {
+ const didAskForPermission = NSUserDefaults.standardUserDefaults.boolForKey(NSPDidAskForNotification);
+ let isEnabled = false;
+ const osVersion = parseFloat(Device.osVersion);
+ if (osVersion >= 10) {
+ isEnabled = (await new Promise((resolve) => UNUserNotificationCenter.currentNotificationCenter().getNotificationSettingsWithCompletionHandler(resolve))) !== (UNAuthorizationOptionNone as any);
+ } else {
+ isEnabled = UIApplication.sharedApplication.currentUserNotificationSettings.types !== UIUserNotificationType.None;
+ }
+
+ if (isEnabled) {
+ status = PermissionStatus.authorized;
+ } else {
+ status = didAskForPermission ? PermissionStatus.denied : PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(types: UIUserNotificationType | UNAuthorizationOptions): Promise<[PermissionStatus, boolean]> {
+ const status = getStatus();
+
+ if (status[0] === PermissionStatus.undetermined) {
+ return new Promise((resolve, reject) => {
+ const observer = function () {
+ resolve(getStatus());
+ NSNotificationCenter.defaultCenter.removeObserver(observer);
+ };
+ NSNotificationCenter.defaultCenter.addObserverForNameObjectQueueUsingBlock(UIApplicationDidBecomeActiveNotification, null, null, observer);
+ const osVersion = parseFloat(Device.osVersion);
+ if (osVersion >= 10) {
+ UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptionsCompletionHandler(types as UNAuthorizationOptions, (p1: boolean, error: NSError) => {
+ if (error) {
+ reject(error);
+ } else {
+ UIApplication.sharedApplication.registerForRemoteNotifications();
+ NSUserDefaults.standardUserDefaults.setBoolForKey(true, NSPDidAskForNotification);
+ NSUserDefaults.standardUserDefaults.synchronize();
+ }
+ });
+ } else {
+ const settings = UIUserNotificationSettings.settingsForTypesCategories(types as UIUserNotificationType, null);
+ UIApplication.sharedApplication.registerUserNotificationSettings(settings);
+ UIApplication.sharedApplication.registerForRemoteNotifications();
+
+ NSUserDefaults.standardUserDefaults.setBoolForKey(true, NSPDidAskForNotification);
+ NSUserDefaults.standardUserDefaults.synchronize();
+ }
+ });
+ } else {
+ return Promise.resolve(status);
+ }
+ }
+ }
+ namespace NSPContacts {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ const contactStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts);
+ switch (contactStatus) {
+ case CNAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case CNAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case CNAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(): Promise<[PermissionStatus, boolean]> {
+ return new Promise((resolve) => {
+ const contactStore = CNContactStore.new();
+ contactStore.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, () => resolve(getStatus()));
+ });
+ }
+ }
+ namespace NSPBackgroundRefresh {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ export function getStatus(): [PermissionStatus, boolean] {
+ const refreshStatus = UIApplication.sharedApplication.backgroundRefreshStatus;
+ switch (refreshStatus) {
+ case UIBackgroundRefreshStatus.Available:
+ status = PermissionStatus.authorized;
+ break;
+ case UIBackgroundRefreshStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case UIBackgroundRefreshStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(): Promise {
+ return new Promise((resolve) => {
+ const contactStore = CNContactStore.new();
+ contactStore.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, () => resolve(getStatus()[0]));
+ });
+ }
+ }
+ namespace NSPEvent {
+ let status: PermissionStatus = PermissionStatus.undetermined;
+ function typeFromString(value: string) {
+ if (value === 'reminder') {
+ return EKEntityType.Reminder;
+ } else {
+ return EKEntityType.Event;
+ }
+ }
+ export function getStatus(type?: string): [PermissionStatus, boolean] {
+ const eventStatus = EKEventStore.authorizationStatusForEntityType(typeFromString(type));
+ switch (eventStatus) {
+ case EKAuthorizationStatus.Authorized:
+ status = PermissionStatus.authorized;
+ break;
+ case EKAuthorizationStatus.Denied:
+ status = PermissionStatus.denied;
+ break;
+ case EKAuthorizationStatus.Restricted:
+ status = PermissionStatus.restricted;
+ break;
+ default:
+ status = PermissionStatus.undetermined;
+ }
+ return [status, true];
+ }
+
+ export function request(type?: string): Promise<[PermissionStatus, boolean]> {
+ return new Promise((resolve) => {
+ const aStore = EKEventStore.new();
+ aStore.requestAccessToEntityTypeCompletion(typeFromString(type), () => resolve(getStatus(type)));
+ });
+ }
+ }
+
+ export enum NSType {
+ Location = 'location',
+ Camera = 'camera',
+ Microphone = 'microphone',
+ Photo = 'photo',
+ Contacts = 'contacts',
+ Event = 'event',
+ Reminder = 'reminder',
+ Bluetooth = 'bluetooth',
+ Notification = 'notification',
+ BackgroundRefresh = 'backgroundRefresh',
+ NSPTypeSpeechRecognition = 'speechRecognition',
+ MediaLibrary = 'mediaLibrary',
+ Motion = 'motion',
+ }
+
+ export function openSettings() {
+ return new Promise((resolve, reject) => {
+ const center = NSNotificationCenter.defaultCenter;
+ const observer = function (notif) {
+ resolve(true);
+ center.removeObserver(observer);
+ };
+ center.addObserverForNameObjectQueueUsingBlock(UIApplicationDidBecomeActiveNotification, null, null, observer);
+ UIApplication.sharedApplication.openURL(NSURL.URLWithString(UIApplicationOpenSettingsURLString));
+ });
+ }
+ export function canOpenSettings() {
+ return Promise.resolve(UIApplicationOpenSettingsURLString !== null);
+ }
+ export async function getPermissionStatus(type, json): Promise<[PermissionStatus, boolean]> {
+ let status: [PermissionStatus, boolean];
+ if (Trace.isEnabled()) {
+ Trace.write(`getPermissionStatus ${type}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+
+ switch (type) {
+ case NSType.Location: {
+ // NSString *locationPermissionType = [RCTConvert NSString:json];
+ status = NSPLocation.getStatusForType(json);
+ break;
+ }
+ case NSType.Camera:
+ status = NSPAudioVideo.getStatus('video');
+ break;
+ case NSType.Microphone:
+ status = NSPAudioVideo.getStatus('audio');
+ break;
+ case NSType.Photo:
+ status = NSPPhoto.getStatus();
+ break;
+ case NSType.Contacts:
+ status = NSPContacts.getStatus();
+ break;
+ case NSType.Event:
+ status = NSPEvent.getStatus('event');
+ break;
+ case NSType.Reminder:
+ status = NSPEvent.getStatus('reminder');
+ break;
+ case NSType.Bluetooth:
+ status = NSPBluetooth.getStatus();
+ break;
+ case NSType.Notification:
+ status = await NSPNotification.getStatus();
+ break;
+ case NSType.BackgroundRefresh:
+ status = NSPBackgroundRefresh.getStatus();
+ break;
+ case NSType.NSPTypeSpeechRecognition:
+ status = NSPSpeechRecognition.getStatus();
+ break;
+ case NSType.MediaLibrary:
+ status = NSPMediaLibrary.getStatus();
+ break;
+ case NSType.Motion:
+ status = NSPMotion.getStatus();
+ break;
+ default:
+ break;
+ }
+
+ return status;
+ }
+ export function requestPermission(type, json): Promise<[PermissionStatus, boolean]> {
+ if (Trace.isEnabled()) {
+ Trace.write(`requestPermission ${type}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ switch (type) {
+ case NSType.Location:
+ return NSPLocation.request(json);
+ case NSType.Camera:
+ return NSPAudioVideo.request('video');
+ case NSType.Microphone:
+ return NSPAudioVideo.request('audio');
+ case NSType.Photo:
+ return NSPPhoto.request();
+ case NSType.Contacts:
+ return NSPContacts.request();
+ case NSType.Event:
+ return NSPEvent.request('event');
+ case NSType.Reminder:
+ return NSPEvent.request('reminder');
+ case NSType.Bluetooth:
+ return NSPBluetooth.request();
+ case NSType.Notification:
+ let types: UIUserNotificationType;
+ const typeStrings: string[] = json;
+ const osVersion = parseFloat(Device.osVersion);
+ if (osVersion >= 10) {
+ if (typeStrings.indexOf('alert') !== -1) {
+ types = types | UNAuthorizationOptions.Alert;
+ }
+ if (typeStrings.indexOf('badge') !== -1) {
+ types = types | UNAuthorizationOptions.Badge;
+ }
+ if (typeStrings.indexOf('sound') !== -1) {
+ types = types | UNAuthorizationOptions.Sound;
+ }
+ if (typeStrings.indexOf('providesAppNotificationSettings') !== -1 && parseFloat(Device.osVersion) >= 12) {
+ types = types | UNAuthorizationOptions.ProvidesAppNotificationSettings;
+ }
+ } else {
+ if (typeStrings.indexOf('alert') !== -1) {
+ types = types | UIUserNotificationType.Alert;
+ }
+ if (typeStrings.indexOf('badge') !== -1) {
+ types = types | UIUserNotificationType.Badge;
+ }
+ if (typeStrings.indexOf('sound') !== -1) {
+ types = types | UIUserNotificationType.Sound;
+ }
+ }
+
+ return NSPNotification.request(types);
+ case NSType.NSPTypeSpeechRecognition:
+ return NSPSpeechRecognition.request();
+ case NSType.MediaLibrary:
+ return NSPMediaLibrary.request();
+ case NSType.Motion:
+ return NSPMotion.request();
+ default:
+ return Promise.reject('unknown');
+ }
+ }
+}
+
+const DEFAULTS = {
+ location: 'whenInUse',
+ notification: ['alert', 'badge', 'sound'],
+};
+
+type IOSPermissionTypes = `${PermissionsIOS.NSType}`;
+const permissionTypes = Object.values(PermissionsIOS.NSType) as IOSPermissionTypes[];
+type SingleResult = [PermissionStatus, boolean];
+interface MultipleResult {
+ [k: string]: PermissionStatus;
+}
+type Result = T extends any[] ? MultipleResult : SingleResult;
+
+export class Permissions {
+ static canOpenSettings() {
+ return PermissionsIOS.canOpenSettings();
+ }
+ static openSettings() {
+ return PermissionsIOS.openSettings();
+ }
+ static getTypes() {
+ return permissionTypes;
+ }
+ static check(permission: IOSPermissionTypes, options?: PermissionCheckOptions): Promise {
+ if (Trace.isEnabled()) {
+ Trace.write(`check ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ if (permissionTypes.indexOf(permission) === -1) {
+ if (Trace.isEnabled()) {
+ Trace.write(`'${permission}' is not a valid permission type on iOS`, Trace.categories.Permissions, Trace.messageType.warn);
+ }
+
+ return Promise.resolve([PermissionStatus.authorized, true]);
+ }
+
+ let type: PermissionCheckOptions;
+
+ if (typeof options === 'string') {
+ type = options;
+ } else if (options && options.type) {
+ type = options.type;
+ }
+
+ return PermissionsIOS.getPermissionStatus(permission, type || DEFAULTS[permission]);
+ }
+ static async request(permission: T, options?: PermissionRequestOptions): Promise> {
+ if (Trace.isEnabled()) {
+ Trace.write(`request ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
+ }
+ if (Array.isArray(permission)) {
+ const grantedPermissions: Result = {};
+ for (let index = 0; index < permission.length; index++) {
+ const res = await Permissions.request(permission[index], options);
+ grantedPermissions[permission[index]] = res[0];
+ }
+ return Promise.resolve(grantedPermissions as Result);
+ }
+ if (permissionTypes.indexOf(permission) === -1) {
+ if (Trace.isEnabled()) {
+ Trace.write(`'${permission}' is not a valid permission type on iOS`, Trace.categories.Permissions, Trace.messageType.warn);
+ }
+
+ return Promise.resolve([PermissionStatus.authorized, true] as Result);
+ }
+
+ //@ts-ignore
+ if (permission === 'backgroundRefresh') {
+ throw new Error('You cannot request backgroundRefresh');
+ }
+
+ let type: PermissionRequestOptions;
+
+ if (typeof options === 'string') {
+ type = options;
+ } else if (options && options.type) {
+ type = options.type;
+ }
+
+ //@ts-ignore
+ return PermissionsIOS.requestPermission(permission, type || DEFAULTS[permission]);
+ }
+ static checkMultiple(permissions: PermissionsType[]) {
+ return Promise.all(permissions.map((permission) => Permissions.check(permission))).then((result) =>
+ result.reduce((acc, value, index) => {
+ const name = permissions[index];
+ acc[name] = value;
+ return acc;
+ }, {})
+ );
+ }
+}
diff --git a/packages/core/references.d.ts b/packages/core/references.d.ts
index 3f788d624..5076c1a91 100644
--- a/packages/core/references.d.ts
+++ b/packages/core/references.d.ts
@@ -2,7 +2,7 @@
///
///
///
-///
+///
///
///
///
diff --git a/packages/core/trace/index.ts b/packages/core/trace/index.ts
index 1a8a22673..6e4cc96e7 100644
--- a/packages/core/trace/index.ts
+++ b/packages/core/trace/index.ts
@@ -201,9 +201,10 @@ export namespace Trace {
export const Transition = 'Transition';
export const Livesync = 'Livesync';
export const ModuleNameResolver = 'ModuleNameResolver';
+ export const Permissions = 'Permissions';
export const separator = ',';
- export const All: string = [VisualTreeEvents, Layout, Style, ViewHierarchy, NativeLifecycle, Debug, Navigation, Test, Binding, Error, Animation, Transition, Livesync, ModuleNameResolver].join(separator);
+ export const All: string = [VisualTreeEvents, Layout, Style, ViewHierarchy, NativeLifecycle, Debug, Navigation, Test, Binding, Error, Animation, Transition, Livesync, ModuleNameResolver, Permissions].join(separator);
export function concat(...args: any): string {
let result: string;
diff --git a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml
index 250a672d2..5f0452a03 100644
--- a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml
+++ b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml
@@ -1,14 +1,7 @@
-
+
-
+
@@ -16,22 +9,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/tools/assets/App_Resources/iOS/Info.plist b/tools/assets/App_Resources/iOS/Info.plist
index 3c0d86fce..f0b6fc33c 100644
--- a/tools/assets/App_Resources/iOS/Info.plist
+++ b/tools/assets/App_Resources/iOS/Info.plist
@@ -26,6 +26,16 @@
LaunchScreen
UIRequiresFullScreen
+ NSCameraUsageDescription
+ Can we use your camera?
+ NSContactsUsageDescription
+ Can we access your contacts?
+ NSPhotoLibraryUsageDescription
+ Can we use your photo library?
+ NSPhotoLibraryAddUsageDescription
+ Can we use your photo library?
+ NSMicrophoneUsageDescription
+ Can we use your microphone?
UIRequiredDeviceCapabilities
armv7