Browse Source

added android support 🎉🎉

Yonah Forst 9 years ago
parent
commit
eb87369148

+ 90 - 10
README.md

@@ -2,16 +2,16 @@
 Request user permissions from React Native (iOS only - android coming soon)
 
 The current supported permissions are:
-- Push Notifications
 - Location
 - Camera
 - Microhone
 - Photos
 - Contacts
 - Events
-- Reminders
-- Bluetooth (Peripheral role. Don't use for Central only)
-- Background Refresh
+- Reminders *(iOS only)*
+- Bluetooth *(iOS only)*
+- Push Notifications *(iOS only)*
+- Background Refresh *(iOS only)*
 
 ##General Usage
 ```js
@@ -72,10 +72,32 @@ const Permissions = require('react-native-permissions');
 
 ##API
 
-_Permission statuses_ - `authorized`, `denied`, `restricted`, or `undetermined`
+###Permission statuses
+Promises resolve into one of these statuses
 
-_Permission types_ - `location`, `camera`, `microphone`, `photo`, `contacts`, `event`, `reminder`, `bluetooth`, `notification`, or `backgroundRefresh`
+| Return value | Notes|
+|---|---|
+|`authorized`| user has authorized this permission |
+|`denied`| user has denied permissions at least once. On iOS this means that the user will not be prompted again. Android users can be promted multiple times until they select 'Never ask me again'|
+|`restricted`| iOS only|
+|`undetermined`| user has not yet been prompted with a permission dialog |
 
+###Supported permission types
+
+| Name | iOS | Android |
+|---|---|---|
+|`location`| ✔️ | ✔ |
+|`camera`| ✔️ | ✔ |
+|`microphone`| ✔️ | ✔ |
+|`photo`| ✔️ | ✔ |
+|`contacts`| ✔️ | ✔ |
+|`event`| ✔️ | ✔ |
+|`bluetooth`| ✔️ | ❌ |
+|`reminder`| ✔️ | ❌ |
+|`notification`| ✔️ | ❌ |
+|`backgroundRefresh`| ✔️ | ❌ |
+
+###Methods
 | Method Name | Arguments | Notes
 |---|---|---|
 | `getPermissionStatus` | `type` | - Returns a promise with the permission status. Note: for type `location`, iOS `AuthorizedAlways` and `AuthorizedWhenInUse` both return `authorized` |
@@ -85,9 +107,8 @@ _Permission types_ - `location`, `camera`, `microphone`, `photo`, `contacts`, `e
 | `openSettings` | *none* | - Switches the user to the settings page of your app (iOS 8.0 and later)  |
 | `canOpenSettings` | *none* | - Returns a boolean indicating if the device supports switching to the settings page |
 
-Note: Permission type `bluetooth` represents the status of the `CBPeripheralManager`. Don't use this if you're only using `CBCentralManager`
-
-###Special cases
+###iOS Notes
+Permission type `bluetooth` represents the status of the `CBPeripheralManager`. Don't use this if only need `CBCentralManager`
 
 `requestPermission` also accepts a second parameter for types `location` and `notification`.
 - `location`: the second parameter is a string, either `always` or `whenInUse`(default).
@@ -105,14 +126,73 @@ Note: Permission type `bluetooth` represents the status of the `CBPeripheralMana
       })
 ```
 
+###Android Notes
+All required permissions also need to be included in the Manifest before they can be requested. Otherwise `requestPermission` will immediately return `denied`.
+
+Permissions are automatically accepted for targetSdkVersion < 23 but you can still use `getPermissionStatus` to check if the user has disabled them from Settings.
+
+Here's a map of types to Android system permissions names:  
+`location` -> `android.permission.ACCESS_FINE_LOCATION`  
+`camera` -> `android.permission.CAMERA`  
+`microphone` -> `android.permission.RECORD_AUDIO`  
+`photo` -> `android.permission.READ_EXTERNAL_STORAGE`  
+`contacts` -> `android.permission.READ_CONTACTS`  
+`event` -> `android.permission.READ_CALENDAR`  
+
+You can request write access to any of these types by also including the appropriate write permission in the Manifest. Read more here: https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous
 
 ##Setup
 
 ````
 npm install --save react-native-permissions
+rnpm link
 ````
 
-##iOS
+###Or manualy linking   
+
+####iOS
 * Run open node_modules/react-native-permissions
 * Drag ReactNativePermissions.xcodeproj into the Libraries group of your app's Xcode project
 * Add libReactNativePermissions.a to `Build Phases -> Link Binary With Libraries.
+
+####Android
+#####Step 1 - Update Gradle Settings
+
+```
+// file: android/settings.gradle
+...
+
+include ':react-native-permissions'
+project(':react-native-permissions').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-permissions/android')
+```
+#####Step 2 - Update Gradle Build
+
+```
+// file: android/app/build.gradle
+...
+
+dependencies {
+    ...
+    compile project(':react-native-permissions')
+}
+```
+#####Step 3 - Register React Package
+```
+...
+import com.joshblour.reactnativepermissions.ReactNativePermissionsPackage; // <--- import
+
+public class MainActivity extends ReactActivity {
+
+    ...
+
+    @Override
+    protected List<ReactPackage> getPackages() {
+        return Arrays.<ReactPackage>asList(
+            new MainReactPackage(),
+            new ReactNativePermissionsPackage() // <------ add the package
+        );
+    }
+
+    ...
+}
+```

+ 0 - 3
ReactNativePermissions.android.js

@@ -1,3 +0,0 @@
-'use strict';
-
-module.exports = {};

+ 41 - 41
ReactNativePermissions.ios.js → ReactNativePermissions.js

@@ -1,20 +1,31 @@
 'use strict';
 
-var React = require('react-native');
-var RNPermissions = React.NativeModules.ReactNativePermissions;
+var ReactNative = require('react-native')
+var Platform = ReactNative.Platform
+var RNPermissions = ReactNative.NativeModules.ReactNativePermissions;
 
-const RNPTypes = [
-	'location',
-	'camera',
-	'microphone',
-	'photo',
-	'contacts',
-	'event',
-	'reminder',
-	'bluetooth',
-	'notification',
-	'backgroundRefresh', 
-]
+const RNPTypes = {
+	ios: [
+		'location',
+		'camera',
+		'microphone',
+		'photo',
+		'contacts',
+		'event',
+		'reminder',
+		'bluetooth',
+		'notification',
+		'backgroundRefresh', 
+	],
+	android: [
+		'location',
+		'camera',
+		'microphone',
+		'contacts',
+		'event',
+		'photos',
+	]
+}
 
 class ReactNativePermissions {
 	constructor() {
@@ -24,7 +35,7 @@ class ReactNativePermissions {
 		this.StatusAuthorized = 'authorized'
 		this.StatusRestricted = 'restricted'
 
-		RNPTypes.forEach(type => {
+		this.getPermissionTypes().forEach(type => {
 			let methodName = `${type}PermissionStatus`
 			this[methodName] = p => {
 				console.warn(`ReactNativePermissions: ${methodName} is depricated. Use getPermissionStatus('${type}') instead.`)
@@ -42,42 +53,31 @@ class ReactNativePermissions {
 	}
 
 	getPermissionTypes() {
-		return RNPTypes;
+		return RNPTypes[Platform.OS];
 	}
 
 	getPermissionStatus(permission) {
-		if (RNPTypes.includes(permission)) {
+		if (this.getPermissionTypes().includes(permission)) {
 			return RNPermissions.getPermissionStatus(permission)
 		} else {
-			return Promise.reject(`ReactNativePermissions: ${permission} is not a valid permission type`)
+			return Promise.reject(`ReactNativePermissions: ${permission} is not a valid permission type on ${Platform.OS}`)
 		}
 	}
 
 	requestPermission(permission, type) {
-		switch (permission) {
-			case "location":
-				return RNPermissions.requestLocation(type || 'always')
-			case "camera":
-				return RNPermissions.requestCamera();
-			case "microphone":
-				return RNPermissions.requestMicrophone();
-			case "photo":
-				return RNPermissions.requestPhoto();
-			case "contacts":
-				return RNPermissions.requestContacts();
-			case "event":
-				return RNPermissions.requestEvent();
-			case "reminder":
-				return RNPermissions.requestReminder();
-			case "bluetooth":
-				return RNPermissions.requestBluetooth();
-			case "notification":
-				return RNPermissions.requestNotification(type || ['alert', 'badge', 'sound'])
-			case "backgroundRefresh":
-				return Promise.reject('You cannot request backgroundRefresh')
-			default:
-				return Promise.reject('invalid type: ' + type)
+		let options; 
+
+		if (!this.getPermissionTypes().includes(permission)) {
+			return Promise.reject(`ReactNativePermissions: ${permission} is not a valid permission type on ${Platform.OS}`)
+		} else if (permission == 'backgroundRefresh'){
+			return Promise.reject('You cannot request backgroundRefresh')
+		} else if (permission == 'location') {
+			options = type || 'always'
+		} else if (permission == 'notification') {
+			options = type || ['alert', 'badge', 'sound']
 		}
+
+		return RNPermissions.requestPermission(permission, options)
 	}
 
 	//recursive funciton to chain a promises for a list of permissions

+ 38 - 35
ReactNativePermissions.m

@@ -109,17 +109,52 @@ RCT_REMAP_METHOD(getPermissionStatus, getPermissionStatus:(RNPType)type resolve:
     resolve(status);
 }
 
-RCT_REMAP_METHOD(requestLocation, requestLocation:(NSString *)type resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+RCT_REMAP_METHOD(requestPermission, permissionType:(RNPType)type json:(id)json resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+{
+    NSString *status;
+    
+    switch (type) {
+        case RNPTypeLocation:
+            return [self requestLocation:json resolve:resolve];
+        case RNPTypeCamera:
+            return [RNPAudioVideo request:@"video" completionHandler:resolve];
+        case RNPTypeMicrophone:
+            return [RNPAudioVideo request:@"audio" completionHandler:resolve];
+        case RNPTypePhoto:
+            return [RNPPhoto request:resolve];
+        case RNPTypeContacts:
+            return [RNPContacts request:resolve];
+        case RNPTypeEvent:
+            return [RNPEvent request:@"event" completionHandler:resolve];
+        case RNPTypeReminder:
+            return [RNPEvent request:@"reminder" completionHandler:resolve];
+        case RNPTypeBluetooth:
+            return [self requestBluetooth:resolve];
+        case RNPTypeNotification:
+            return [self requestNotification:json resolve:resolve];
+        default:
+            break;
+    }
+    
+
+}
+
+
+- (void) requestLocation:(id)json resolve:(RCTPromiseResolveBlock)resolve
 {
     if (self.locationMgr == nil) {
         self.locationMgr = [[RNPLocation alloc] init];
     }
     
+    NSString *type = [RCTConvert NSString:json];
+    
     [self.locationMgr request:type completionHandler:resolve];
 }
 
-RCT_REMAP_METHOD(requestNotification, requestNotification:(NSArray *)typeStrings resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+- (void) requestNotification:(id)json resolve:(RCTPromiseResolveBlock)resolve
 {
+    NSArray *typeStrings = [RCTConvert NSArray:json];
+    
     UIUserNotificationType types;
     if ([typeStrings containsObject:@"alert"])
         types = types | UIUserNotificationTypeAlert;
@@ -140,7 +175,7 @@ RCT_REMAP_METHOD(requestNotification, requestNotification:(NSArray *)typeStrings
 }
 
 
-RCT_REMAP_METHOD(requestBluetooth, requestBluetooth:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+- (void) requestBluetooth:(RCTPromiseResolveBlock)resolve
 {
     if (self.bluetoothMgr == nil) {
         self.bluetoothMgr = [[RNPBluetooth alloc] init];
@@ -149,38 +184,6 @@ RCT_REMAP_METHOD(requestBluetooth, requestBluetooth:(RCTPromiseResolveBlock)reso
     [self.bluetoothMgr request:resolve];
 }
 
-RCT_REMAP_METHOD(requestCamera, requestCamera:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
-{
-    [RNPAudioVideo request:@"video" completionHandler:resolve];
-}
-
-RCT_REMAP_METHOD(requestMicrophone, requestMicrophone:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
-{
-    [RNPAudioVideo request:@"audio" completionHandler:resolve];
-}
-
-RCT_REMAP_METHOD(requestEvent, requestEvents:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
-{
-    [RNPEvent request:@"event" completionHandler:resolve];
-}
-
-RCT_REMAP_METHOD(requestReminder, requestReminders:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
-{
-    [RNPEvent request:@"reminder" completionHandler:resolve];
-}
-
-RCT_REMAP_METHOD(requestPhoto, requestPhoto:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
-{
-    [RNPPhoto request:resolve];
-}
-
-RCT_REMAP_METHOD(requestContacts, requestContacts:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
-{
-    [RNPContacts request:resolve];
-}
-
-
-
 
 
 

+ 34 - 0
android/build.gradle

@@ -0,0 +1,34 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:2.1.+'
+    }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
+
+    defaultConfig {
+        minSdkVersion 18
+        targetSdkVersion 23
+        versionCode 1
+        versionName "1.0"
+    }
+    lintOptions {
+        abortOnError false
+    }
+}
+
+repositories {
+    jcenter()
+}
+
+dependencies {
+    compile 'com.facebook.react:react-native:+'
+}

+ 3 - 0
android/src/main/AndroidManifest.xml

@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.joshblour.reactnativepermissions">
+</manifest>

+ 128 - 0
android/src/main/java/com/joshblour/reactnativepermissions/ReactNativePermissionsModule.java

@@ -0,0 +1,128 @@
+package com.joshblour.reactnativepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Settings;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.PermissionChecker;
+
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.modules.permissions.PermissionsModule;
+
+
+public class ReactNativePermissionsModule extends ReactContextBaseJavaModule {
+  private final ReactApplicationContext reactContext;
+  private final PermissionsModule mPermissionsModule;
+
+  public enum RNType {
+    LOCATION,
+    CAMERA,
+    MICROPHONE,
+    CONTACTS,
+    EVENT,
+    PHOTOS;
+  }
+
+  public ReactNativePermissionsModule(ReactApplicationContext reactContext) {
+    super(reactContext);
+    this.reactContext = reactContext;
+    mPermissionsModule = new PermissionsModule(this.reactContext);
+  }
+
+  @Override
+  public String getName() {
+    return "ReactNativePermissions";
+  }
+
+  @ReactMethod
+  public void getPermissionStatus(String permissionString, Promise promise) {
+    String permission = permissionForString(permissionString);
+
+    // check if permission is valid
+    if (permission == null) {
+      promise.reject("unknown-permission", "ReactNativePermissions: unknown permission type - " + permissionString);
+      return;
+    }
+
+    int result = PermissionChecker.checkSelfPermission(this.reactContext, permission);
+    switch (result) {
+      case PermissionChecker.PERMISSION_DENIED:
+        // PermissionDenied could also mean that we've never asked for permission yet.
+        // Use shouldShowRequestPermissionRationale to determined which on it is.
+        if (getCurrentActivity() != null) {
+          boolean deniedOnce = ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), permission);
+          promise.resolve(deniedOnce ? "denied" : "undetermined");
+        } else {
+          promise.resolve("denied");
+        }
+        break;
+      case PermissionChecker.PERMISSION_DENIED_APP_OP:
+        promise.resolve("denied");
+        break;
+      case PermissionChecker.PERMISSION_GRANTED:
+        promise.resolve("authorized");
+        break;
+      default:
+        promise.resolve("undetermined");
+        break;
+    }
+  }
+
+  @ReactMethod
+  public void requestPermission(final String permissionString, String nullForiOSCompat, final Promise promise) {
+    String permission = permissionForString(permissionString);
+    mPermissionsModule.requestPermission(permission, new Callback() {
+      @Override
+      public void invoke(Object... args) {
+        getPermissionStatus(permissionString, promise);
+//        promise.resolve((boolean)args[1] ? "authorized" : "denied");
+      }
+    }, null);
+  }
+
+
+  @ReactMethod
+  public void canOpenSettings(Promise promise) {
+    promise.resolve(true);
+  }
+
+  @ReactMethod
+  public void openSettings() {
+    final Intent i = new Intent();
+    i.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+    i.addCategory(Intent.CATEGORY_DEFAULT);
+    i.setData(Uri.parse("package:" + this.reactContext.getPackageName()));
+    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+    i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+    this.reactContext.startActivity(i);
+  }
+
+  private String permissionForString(String permission) {
+    switch (RNType.valueOf(permission.toUpperCase())) {
+      case LOCATION:
+        return Manifest.permission.ACCESS_FINE_LOCATION;
+      case CAMERA:
+        return Manifest.permission.CAMERA;
+      case MICROPHONE:
+        return Manifest.permission.RECORD_AUDIO;
+      case CONTACTS:
+        return Manifest.permission.READ_CONTACTS;
+      case EVENT:
+        return Manifest.permission.READ_CALENDAR;
+      case PHOTOS:
+        return Manifest.permission.READ_EXTERNAL_STORAGE;
+      default:
+        return null;
+    }
+  }
+
+}

+ 28 - 0
android/src/main/java/com/joshblour/reactnativepermissions/ReactNativePermissionsPackage.java

@@ -0,0 +1,28 @@
+package com.joshblour.reactnativepermissions;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+import com.facebook.react.bridge.JavaScriptModule;
+
+public class ReactNativePermissionsPackage implements ReactPackage {
+    @Override
+    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
+      return Arrays.<NativeModule>asList(new ReactNativePermissionsModule(reactContext));
+    }
+
+    @Override
+    public List<Class<? extends JavaScriptModule>> createJSModules() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
+      return Collections.emptyList();
+    }
+}