feat(all): fix various issues and big maintenance update of the library (#404) (#411) BREAKING

Fixes
fix(ci): fixed android release build as a CI test step

fix(android): Fix MediaStore issues and support scopped storage #322 #244

fix(android): Fix saveImageToCameraRoll #248 #254

fix(android): remove JCenter #340 #401

fix(ios): Fix performance issues #276

fix(ios): add method to get file Path from internal PH ID #135

fix(react-native): Bump used dependencies #393 ...

Fixes #322
Fixes #244
Fixes #248
Fixes #254
Fixes #340
Fixes #401
Fixes #276
Fixes #135
Fixes #393

new Features

feat(ios): now it's possible to listen to library selection changes
`useEffect(() => {
let subscription: EmitterSubscription;
if (isAboveIOS14) {
subscription = cameraRollEventEmitter.addListener('onLibrarySelectionChange', (_event) => {
getUnloadedPictures();
});
}

return () => {
if (isAboveIOS14 && subscription) {
subscription.remove();
}
};`

feat(ios): possibility to open native modal to update selection when authorization is limited
iosRefreshGallerySelection()
feat(ios): Convert HEIC images on demand to upload to server
BREAKING CHANGE: root import changed!
```
import { CameraRoll } from @react-native-community/cameraroll
instead of
import CameraRoll from @react-native-community/cameraroll
```

Co-authored-by: idrissakhi <85105624+idrissakhi@users.noreply.github.com>
This commit is contained in:
Bartol Karuza
2022-08-23 08:13:10 +00:00
co-authored by idrissakhi
parent 3f0aed96db
commit c230dd0074
41 changed files with 6951 additions and 5805 deletions
+21 -31
View File
@@ -1,7 +1,7 @@
version: 2.1
orbs:
rn: react-native-community/react-native@4.0.4
rn: react-native-community/react-native@7.1.1
jobs:
checkout_code:
@@ -10,8 +10,10 @@ jobs:
- checkout
- persist_to_workspace:
root: .
paths: .
analyze:
paths:
- .
analyse_js:
executor: rn/linux_js
steps:
- attach_workspace:
@@ -20,9 +22,6 @@ jobs:
- run:
name: Eslint
command: yarn run validate:eslint
- run:
name: Flow
command: yarn run validate:flow
- run:
name: TypeScript
command: yarn run validate:typescript
@@ -49,39 +48,30 @@ workflows:
test:
jobs:
- checkout_code
- analyze:
- analyse_js:
requires:
- checkout_code
- rn/android_build:
name: android_debug_build
project_path: "example/android"
build_type: debug
requires:
- analyze
- rn/android_build:
name: android_release_build
project_path: "example/android"
build_type: release
name: build_android_release
project_path: android
store_artifacts: false
persist_to_workspace: false
requires:
- analyze
# - rn/android_test:
# logcat_grep: "com.camerarollexample"
# detox_configuration: "android.emu.release"
- analyse_js
# - rn/ios_build:
# build_configuration: Debug
# device: iPhone 11
# name: build_ios_debug
# project_path: example/ios/CameraRollExample.xcworkspace
# project_type: workspace
# requires:
# - android_release_build
# - rn/ios_build_and_test:
# project_path: "example/ios/CameraRollExample.xcodeproj"
# derived_data_path: "example/ios/build"
# device: "iPhone X"
# build_configuration: "Release"
# scheme: "CameraRollExample"
# detox_configuration: "ios.sim.release"
# requires:
# - analyze
# - analyse_js
# scheme: CameraRollExample
- publish:
requires:
- android_debug_build
- android_release_build
- build_android_release
# - build_ios_debug
filters:
branches:
only: master
+92 -4
View File
@@ -1,5 +1,93 @@
module.exports = {
root: true,
extends: '@react-native-community',
};
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
ignorePatterns: ['scripts', 'lib', 'docs', 'example', 'app.plugin.js'],
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended', '@react-native-community'],
rules: {
// eslint
semi: 'off',
curly: ['warn', 'multi-or-nest', 'consistent'],
'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs'],
'no-async-promise-executor': 'warn',
'require-await': 'warn',
'no-return-await': 'warn',
'no-await-in-loop': 'warn',
'comma-dangle': 'off', // prettier already detects this
'no-restricted-syntax': [
'error',
{
selector: 'TSEnumDeclaration',
message:
"Enums have various disadvantages, use TypeScript's union types instead.",
},
],
// prettier
'prettier/prettier': ['warn'],
// typescript
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: false,
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
},
],
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unsafe-assignment': 'error',
// react plugin
'react/no-unescaped-entities': 'off',
// react native plugin
'react-native/no-unused-styles': 'warn',
'react-native/split-platform-components': 'off',
'react-native/no-inline-styles': 'warn',
'react-native/no-color-literals': 'off',
'react-native/no-raw-text': 'off',
'react-native/no-single-element-style-arrays': 'warn',
'@typescript-eslint/strict-boolean-expressions': [
'error',
{
allowString: false,
allowNullableObject: false,
allowNumber: false,
allowNullableBoolean: true,
},
],
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
// react hooks
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks:
'(useDerivedValue|useAnimatedStyle|useAnimatedProps|useWorkletCallback|useFrameProcessor)',
},
],
},
env: {
node: true,
},
globals: {
_log: 'readonly',
},
};
+3
View File
@@ -59,3 +59,6 @@ example/android-bundle.js
example/ios-bundle.js
index.android.bundle
index.ios.bundle
# Bob generated files
lib/
+1 -3
View File
@@ -3,7 +3,5 @@
"trailingComma": "all",
"bracketSpacing": false,
"jsxBracketSameLine": true,
"overrides": [
{"files": ["*.js"], "options": {"parser": "flow", "requirePragma": true}}
]
"overrides": [{ "files": ["*.js"], "options": { "parser": "flow", "requirePragma": true } }]
}
+152 -2
View File
@@ -56,7 +56,7 @@ import { CameraRoll } from "react-native";
to:
```javascript
import CameraRoll from "@react-native-community/cameraroll";
import { CameraRoll } from "@react-native-community/cameraroll";
```
## Usage
@@ -93,7 +93,7 @@ Then you have to explicitly ask for the permission
```javascript
import { PermissionsAndroid, Platform } from "react-native";
import CameraRoll from "@react-native-community/cameraroll";
import { CameraRoll } from "@react-native-community/cameraroll";
async function hasAndroidPermission() {
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
@@ -284,6 +284,138 @@ render() {
}
```
Loading images with listeners and refetchs:
```javascript
import { PhotoGallery, cameraRollEventEmitter } from 'react-native-photo-gallery-api';
import { useCallback, useEffect, useState } from 'react';
import { AppState, EmitterSubscription } from 'react-native';
interface GalleryOptions {
pageSize: number;
mimeTypeFilter?: Array<string>;
}
interface GalleryLogic {
photos?: ImageDTO[];
loadNextPagePictures: () => void;
isLoading: boolean;
isLoadingNextPage: boolean;
isReloading: boolean;
hasNextPage: boolean;
}
const supportedMimeTypesByTheBackEnd = [
'image/jpeg',
'image/png',
'image/heif',
'image/heic',
'image/heif-sequence',
'image/heic-sequence',
];
export const useGallery = ({
pageSize = 30,
mimeTypeFilter = supportedMimeTypesByTheBackEnd,
}: GalleryOptions): GalleryLogic => {
const [isLoading, setIsLoading] = useState(false);
const [isReloading, setIsReloading] = useState(false);
const [isLoadingNextPage, setIsLoadingNextPage] = useState(false);
const [hasNextPage, setHasNextPage] = useState(false);
const [nextCursor, setNextCursor] = useState<string>();
const [photos, setPhotos] = useState<ImageDTO[]>();
const loadNextPagePictures = useCallback(async () => {
try {
nextCursor ? setIsLoadingNextPage(true) : setIsLoading(true);
const { edges, page_info } = await PhotoGallery.getPhotos({
first: pageSize,
after: nextCursor,
assetType: 'Photos',
mimeTypes: mimeTypeFilter,
...(isAndroid && { include: ['fileSize', 'filename'] }),
});
const photos = convertCameraRollPicturesToImageDtoType(edges);
setPhotos((prev) => [...(prev ?? []), ...photos]);
setNextCursor(page_info.end_cursor);
setHasNextPage(page_info.has_next_page);
} catch (error) {
console.error('useGallery getPhotos error:', error);
} finally {
setIsLoading(false);
setIsLoadingNextPage(false);
}
}, [mimeTypeFilter, nextCursor, pageSize]);
const getUnloadedPictures = useCallback(async () => {
try {
setIsReloading(true);
const { edges, page_info } = await PhotoGallery.getPhotos({
first: !photos || photos.length < pageSize ? pageSize : photos.length,
assetType: 'Photos',
mimeTypes: mimeTypeFilter,
// Include fileSize only for android since it's causing performance issues on IOS.
...(isAndroid && { include: ['fileSize', 'filename'] }),
});
const newPhotos = convertCameraRollPicturesToImageDtoType(edges);
setPhotos(newPhotos);
setNextCursor(page_info.end_cursor);
setHasNextPage(page_info.has_next_page);
} catch (error) {
console.error('useGallery getNewPhotos error:', error);
} finally {
setIsReloading(false);
}
}, [mimeTypeFilter, pageSize, photos]);
useEffect(() => {
if (!photos) {
loadNextPagePictures();
}
}, [loadNextPagePictures, photos]);
useEffect(() => {
const subscription = AppState.addEventListener('change', async (nextAppState) => {
if (nextAppState === 'active') {
getUnloadedPictures();
}
});
return () => {
subscription.remove();
};
}, [getUnloadedPictures]);
useEffect(() => {
let subscription: EmitterSubscription;
if (isAboveIOS14) {
subscription = cameraRollEventEmitter.addListener('onLibrarySelectionChange', (_event) => {
getUnloadedPictures();
});
}
return () => {
if (isAboveIOS14 && subscription) {
subscription.remove();
}
};
}, [getUnloadedPictures]);
return {
photos,
loadNextPagePictures,
isLoading,
isLoadingNextPage,
isReloading,
hasNextPage,
};
};
```
---
### `deletePhotos()`
@@ -307,6 +439,24 @@ Returns a Promise which will resolve when the deletion request is completed, or
| uri | string | Yes | See above. |
### `iosGetImageDataById()`
```javascript
CameraRoll.iosGetImageDataById(internalID, true);
```
**Parameters:**
| Name | Type | Required | Description |
| ------------ | ----------------------- | ---------- | ---------------------------------------------------- |
| internalID | string | Yes | Ios internal ID 'PH://xxxx'. |
| convertHeic | boolean | False | Whether to convert or not to JPEG image. |
### Known issues
#### IOS
If you try to save media into specific album without asking for read and write permission then saving will not work, workaround is to not precice album name for IOS if you don't want to request full permission (Only ios >= 14).
[circle-ci-badge]:https://img.shields.io/circleci/project/github/react-native-cameraroll/react-native-cameraroll/master.svg?style=flat-square
[circle-ci]:https://circleci.com/gh/react-native-cameraroll/workflows/react-native-cameraroll/tree/master
[supported-os-badge]:https://img.shields.io/badge/platforms-android%20|%20ios-lightgrey.svg?style=flat-square
+44 -11
View File
@@ -1,15 +1,36 @@
buildscript {
repositories {
google()
jcenter()
}
// The Android Gradle plugin is only required when opening the android folder stand-alone.
// This avoids unnecessary downloads and potential conflicts when the library is included as a
// module dependency in an application project.
if (project == rootProject) {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
dependencies {
classpath("com.android.tools.build:gradle:4.2.2")
}
}
}
task downloadDependencies() {
description 'Download all dependencies to the Gradle cache'
doLast {
configurations.findAll().each { config ->
if (config.name.contains("minReactNative") && config.canBeResolved) {
print config.name
print '\n'
config.files
}
}
}
}
def getExtOrInitialValue(name, initialValue) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : initialValue
}
def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeCameraRoll_' + name]
}
@@ -22,21 +43,33 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
buildToolsVersion getExtOrDefault('buildToolsVersion')
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion getExtOrIntegerDefault('minSdkVersion')
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
}
lintOptions{
abortOnError false
}
}
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
google()
jcenter()
mavenLocal()
mavenCentral()
}
dependencies {
//noinspection GradleDynamicVersion
api 'com.facebook.react:react-native:+'
}
implementation 'com.facebook.react:react-native:+'
}
+7 -4
View File
@@ -1,4 +1,7 @@
ReactNativeCameraRoll_compileSdkVersion=28
ReactNativeCameraRoll_buildToolsVersion=28.0.3
ReactNativeCameraRoll_targetSdkVersion=27
ReactNativeCameraRoll_minSdkVersion=16
ReactNativeCameraRoll_compileSdkVersion=31
ReactNativeCameraRoll_buildToolsVersion=29.0.2
ReactNativeCameraRoll_targetSdkVersion=31
ReactNativeCameraRoll_minSdkVersion=23
android.useAndroidX=true
android.enableJetifier=true
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+103
View File
@@ -0,0 +1,103 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -9,6 +9,7 @@ package com.reactnativecommunity.cameraroll;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
@@ -18,7 +19,9 @@ import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.text.TextUtils;
@@ -46,7 +49,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -82,20 +85,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
private static final String INCLUDE_PLAYABLE_DURATION = "playableDuration";
private static final String[] PROJECTION = {
Images.Media._ID,
Images.Media.MIME_TYPE,
Images.Media.BUCKET_DISPLAY_NAME,
Images.Media.DATE_TAKEN,
MediaStore.MediaColumns.DATE_ADDED,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.DATA
Images.Media._ID,
Images.Media.MIME_TYPE,
Images.Media.BUCKET_DISPLAY_NAME,
Images.Media.DATE_TAKEN,
MediaStore.MediaColumns.DATE_ADDED,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.DATA
};
private static final String SELECTION_BUCKET = Images.Media.BUCKET_DISPLAY_NAME + " = ?";
private static final String SELECTION_DATE_TAKEN = Images.Media.DATE_TAKEN + " < ?";
public CameraRollModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -111,13 +113,13 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
* from wherever it may be to the external storage pictures directory, so that it can be scanned
* by the MediaScanner.
*
* @param uri the file:// URI of the image to save
* @param uri the file:// URI of the image to save
* @param promise to be resolved or rejected
*/
@ReactMethod
public void saveToCameraRoll(String uri, ReadableMap options, Promise promise) {
new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), options, promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static class SaveToCameraRoll extends GuardedAsyncTask<Void, Void> {
@@ -138,82 +140,107 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
@Override
protected void doInBackgroundGuarded(Void... params) {
File source = new File(mUri.getPath());
FileChannel input = null, output = null;
try {
boolean isAlbumPresent = !"".equals(mOptions.getString("album"));
final File environment;
// Media is not saved into an album when using Environment.DIRECTORY_DCIM.
if (isAlbumPresent) {
if ("video".equals(mOptions.getString("type"))) {
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
} else {
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
}
} else {
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
}
FileInputStream input = null;
OutputStream output = null;
File exportDir;
if (isAlbumPresent) {
exportDir = new File(environment, mOptions.getString("album"));
if (!exportDir.exists() && !exportDir.mkdirs()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Album Directory not created. Did you request WRITE_EXTERNAL_STORAGE?");
String mimeType = Utils.getMimeType(mUri.getPath());
try {
String album = mOptions.getString("album");
boolean isAlbumPresent = !TextUtils.isEmpty(album);
// Android Q and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues mediaDetails = new ContentValues();
if (isAlbumPresent) {
String relativePath = Environment.DIRECTORY_DCIM + File.separator + album;
mediaDetails.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath);
}
mediaDetails.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
mediaDetails.put(Images.Media.DISPLAY_NAME, source.getName());
mediaDetails.put(Images.Media.IS_PENDING, 1);
ContentResolver resolver = mContext.getContentResolver();
Uri mediaContentUri = resolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaDetails);
output = resolver.openOutputStream(mediaContentUri);
input = new FileInputStream(source);
FileUtils.copy(input, output);
mediaDetails.clear();
mediaDetails.put(Images.Media.IS_PENDING, 0);
resolver.update(mediaContentUri, mediaDetails, null, null);
mPromise.resolve(mediaContentUri.toString());
} else {
final File environment;
// Media is not saved into an album when using Environment.DIRECTORY_DCIM.
if (isAlbumPresent) {
if ("video".equals(mOptions.getString("type"))) {
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
} else {
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
}
} else {
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
}
File exportDir;
if (isAlbumPresent) {
exportDir = new File(environment, album);
if (!exportDir.exists() && !exportDir.mkdirs()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Album Directory not created. Did you request WRITE_EXTERNAL_STORAGE?");
return;
}
} else {
exportDir = environment;
}
if (!exportDir.isDirectory()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
return;
}
} else {
exportDir = environment;
}
if (!exportDir.isDirectory()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
return;
}
File dest = new File(exportDir, source.getName());
int n = 0;
String fullSourceName = source.getName();
String sourceName, sourceExt;
if (fullSourceName.indexOf('.') >= 0) {
sourceName = fullSourceName.substring(0, fullSourceName.lastIndexOf('.'));
sourceExt = fullSourceName.substring(fullSourceName.lastIndexOf('.'));
} else {
sourceName = fullSourceName;
sourceExt = "";
}
while (!dest.createNewFile()) {
dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
}
input = new FileInputStream(source).getChannel();
output = new FileOutputStream(dest).getChannel();
output.transferFrom(input, 0, input.size());
input.close();
output.close();
File dest = new File(exportDir, source.getName());
int n = 0;
String fullSourceName = source.getName();
String sourceName, sourceExt;
if (fullSourceName.indexOf('.') >= 0) {
sourceName = fullSourceName.substring(0, fullSourceName.lastIndexOf('.'));
sourceExt = fullSourceName.substring(fullSourceName.lastIndexOf('.'));
} else {
sourceName = fullSourceName;
sourceExt = "";
}
while (!dest.createNewFile()) {
dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
}
input = new FileInputStream(source);
output = new FileOutputStream(dest);
((FileOutputStream) output).getChannel()
.transferFrom(input.getChannel(), 0, input.getChannel().size());
input.close();
output.close();
MediaScannerConnection.scanFile(
mContext,
new String[]{dest.getAbsolutePath()},
null,
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
if (uri != null) {
mPromise.resolve(uri.toString());
} else {
mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery");
}
}
});
MediaScannerConnection.scanFile(
mContext,
new String[]{dest.getAbsolutePath()},
null,
(path, uri) -> {
if (uri != null) {
mPromise.resolve(uri.toString());
} else {
mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery");
}
});
}
} catch (IOException e) {
mPromise.reject(e);
} finally {
if (input != null && input.isOpen()) {
if (input != null) {
try {
input.close();
} catch (IOException e) {
FLog.e(ReactConstants.TAG, "Could not close input channel", e);
}
}
if (output != null && output.isOpen()) {
if (output != null) {
try {
output.close();
} catch (IOException e) {
@@ -227,25 +254,25 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
/**
* Get photos from {@link MediaStore.Images}, most recent first.
*
* @param params a map containing the following keys:
* <ul>
* <li>first (mandatory): a number representing the number of photos to fetch</li>
* <li>
* after (optional): a cursor that matches page_info[end_cursor] returned by a
* previous call to {@link #getPhotos}
* </li>
* <li>groupName (optional): an album name</li>
* <li>
* mimeType (optional): restrict returned images to a specific mimetype (e.g.
* image/jpeg)
* </li>
* <li>
* assetType (optional): chooses between either photos or videos from the camera roll.
* Valid values are "Photos" or "Videos". Defaults to photos.
* </li>
* </ul>
* @param params a map containing the following keys:
* <ul>
* <li>first (mandatory): a number representing the number of photos to fetch</li>
* <li>
* after (optional): a cursor that matches page_info[end_cursor] returned by a
* previous call to {@link #getPhotos}
* </li>
* <li>groupName (optional): an album name</li>
* <li>
* mimeType (optional): restrict returned images to a specific mimetype (e.g.
* image/jpeg)
* </li>
* <li>
* assetType (optional): chooses between either photos or videos from the camera roll.
* Valid values are "Photos" or "Videos". Defaults to photos.
* </li>
* </ul>
* @param promise the Promise to be resolved when the photos are loaded; for a format of the
* parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js
* parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js
*/
@ReactMethod
public void getPhotos(final ReadableMap params, final Promise promise) {
@@ -256,30 +283,33 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
long fromTime = params.hasKey("fromTime") ? (long) params.getDouble("fromTime") : 0;
long toTime = params.hasKey("toTime") ? (long) params.getDouble("toTime") : 0;
ReadableArray mimeTypes = params.hasKey("mimeTypes")
? params.getArray("mimeTypes")
: null;
? params.getArray("mimeTypes")
: null;
ReadableArray include = params.hasKey("include") ? params.getArray("include") : null;
new GetMediaTask(
getReactApplicationContext(),
first,
after,
groupName,
mimeTypes,
assetType,
fromTime,
toTime,
include,
promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
getReactApplicationContext(),
first,
after,
groupName,
mimeTypes,
assetType,
fromTime,
toTime,
include,
promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static class GetMediaTask extends GuardedAsyncTask<Void, Void> {
private final Context mContext;
private final int mFirst;
private final @Nullable String mAfter;
private final @Nullable String mGroupName;
private final @Nullable ReadableArray mMimeTypes;
private final @Nullable
String mAfter;
private final @Nullable
String mGroupName;
private final @Nullable
ReadableArray mMimeTypes;
private final Promise mPromise;
private final String mAssetType;
private final long mFromTime;
@@ -287,16 +317,16 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
private final Set<String> mInclude;
private GetMediaTask(
ReactContext context,
int first,
@Nullable String after,
@Nullable String groupName,
@Nullable ReadableArray mimeTypes,
String assetType,
long fromTime,
long toTime,
@Nullable ReadableArray include,
Promise promise) {
ReactContext context,
int first,
@Nullable String after,
@Nullable String groupName,
@Nullable ReadableArray mimeTypes,
String assetType,
long fromTime,
long toTime,
@Nullable ReadableArray include,
Promise promise) {
super(context);
mContext = context;
mFirst = first;
@@ -338,19 +368,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
if (mAssetType.equals(ASSET_TYPE_PHOTOS)) {
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = "
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
} else if (mAssetType.equals(ASSET_TYPE_VIDEOS)) {
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = "
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
} else if (mAssetType.equals(ASSET_TYPE_ALL)) {
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " IN ("
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO + ","
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + ")");
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO + ","
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + ")");
} else {
mPromise.reject(
ERROR_UNABLE_TO_FILTER,
"Invalid filter option: '" + mAssetType + "'. Expected one of '"
+ ASSET_TYPE_PHOTOS + "', '" + ASSET_TYPE_VIDEOS + "' or '" + ASSET_TYPE_ALL + "'."
ERROR_UNABLE_TO_FILTER,
"Invalid filter option: '" + mAssetType + "'. Expected one of '"
+ ASSET_TYPE_PHOTOS + "', '" + ASSET_TYPE_VIDEOS + "' or '" + ASSET_TYPE_ALL + "'."
);
return;
}
@@ -378,19 +408,36 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
ContentResolver resolver = mContext.getContentResolver();
try {
// set LIMIT to first + 1 so that we know how to populate page_info
String limit = "limit=" + (mFirst + 1);
if (!TextUtils.isEmpty(mAfter)) {
limit = "limit=" + mAfter + "," + (mFirst + 1);
Cursor media;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bundle bundle = new Bundle();
bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection.toString());
bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
selectionArgs.toArray(new String[selectionArgs.size()]));
bundle.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, Images.Media.DATE_ADDED + " DESC, " + Images.Media.DATE_MODIFIED + " DESC");
bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, mFirst + 1);
if (!TextUtils.isEmpty(mAfter)) {
bundle.putInt(ContentResolver.QUERY_ARG_OFFSET, Integer.parseInt(mAfter));
}
media = resolver.query(
MediaStore.Files.getContentUri("external"),
PROJECTION,
bundle,
null);
} else {
// set LIMIT to first + 1 so that we know how to populate page_info
String limit = "limit=" + (mFirst + 1);
if (!TextUtils.isEmpty(mAfter)) {
limit = "limit=" + mAfter + "," + (mFirst + 1);
}
media = resolver.query(
MediaStore.Files.getContentUri("external").buildUpon().encodedQuery(limit).build(),
PROJECTION,
selection.toString(),
selectionArgs.toArray(new String[selectionArgs.size()]),
Images.Media.DATE_ADDED + " DESC, " + Images.Media.DATE_MODIFIED + " DESC");
}
Cursor media = resolver.query(
MediaStore.Files.getContentUri("external").buildUpon().encodedQuery(limit).build(),
PROJECTION,
selection.toString(),
selectionArgs.toArray(new String[selectionArgs.size()]),
Images.Media.DATE_ADDED + " DESC, " + Images.Media.DATE_MODIFIED + " DESC");
if (media == null) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
} else {
@@ -404,9 +451,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
}
} catch (SecurityException e) {
mPromise.reject(
ERROR_UNABLE_TO_LOAD_PERMISSION,
"Could not get media: need READ_EXTERNAL_STORAGE permission",
e);
ERROR_UNABLE_TO_LOAD_PERMISSION,
"Could not get media: need READ_EXTERNAL_STORAGE permission",
e);
}
}
}
@@ -453,7 +500,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
Map<String, Integer> albums = new HashMap<>();
do {
int column = media.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME);
if ( column < 0 ) {
if (column < 0) {
throw new IndexOutOfBoundsException();
}
String albumName = media.getString(column);
@@ -489,19 +536,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
pageInfo.putBoolean("has_next_page", limit < media.getCount());
if (limit < media.getCount()) {
pageInfo.putString(
"end_cursor",
Integer.toString(offset + limit)
"end_cursor",
Integer.toString(offset + limit)
);
}
response.putMap("page_info", pageInfo);
}
private static void putEdges(
ContentResolver resolver,
Cursor media,
WritableMap response,
int limit,
Set<String> include) {
ContentResolver resolver,
Cursor media,
WritableMap response,
int limit,
Set<String> include) {
WritableArray edges = new WritableNativeArray();
media.moveToFirst();
int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE);
@@ -524,9 +571,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
WritableMap edge = new WritableNativeMap();
WritableMap node = new WritableNativeMap();
boolean imageInfoSuccess =
putImageInfo(resolver, media, node, widthIndex, heightIndex, sizeIndex, dataIndex,
mimeTypeIndex, includeFilename, includeFileSize, includeImageSize,
includePlayableDuration);
putImageInfo(resolver, media, node, widthIndex, heightIndex, sizeIndex, dataIndex,
mimeTypeIndex, includeFilename, includeFileSize, includeImageSize,
includePlayableDuration);
if (imageInfoSuccess) {
putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex, dateAddedIndex, dateModifiedIndex);
putLocationInfo(media, node, dataIndex, includeLocation);
@@ -544,19 +591,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
}
private static void putBasicNodeInfo(
Cursor media,
WritableMap node,
int mimeTypeIndex,
int groupNameIndex,
int dateTakenIndex,
int dateAddedIndex,
int dateModifiedIndex) {
Cursor media,
WritableMap node,
int mimeTypeIndex,
int groupNameIndex,
int dateTakenIndex,
int dateAddedIndex,
int dateModifiedIndex) {
node.putString("type", media.getString(mimeTypeIndex));
node.putString("group_name", media.getString(groupNameIndex));
long dateTaken = media.getLong(dateTakenIndex);
if (dateTaken == 0L) {
//date added is in seconds, date taken in milliseconds, thus the multiplication
dateTaken = media.getLong(dateAddedIndex) * 1000;
//date added is in seconds, date taken in milliseconds, thus the multiplication
dateTaken = media.getLong(dateAddedIndex) * 1000;
}
node.putDouble("timestamp", dateTaken / 1000d);
node.putDouble("modified", media.getLong(dateModifiedIndex));
@@ -567,18 +614,18 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
* to include
*/
private static boolean putImageInfo(
ContentResolver resolver,
Cursor media,
WritableMap node,
int widthIndex,
int heightIndex,
int sizeIndex,
int dataIndex,
int mimeTypeIndex,
boolean includeFilename,
boolean includeFileSize,
boolean includeImageSize,
boolean includePlayableDuration) {
ContentResolver resolver,
Cursor media,
WritableMap node,
int widthIndex,
int heightIndex,
int sizeIndex,
int dataIndex,
int mimeTypeIndex,
boolean includeFilename,
boolean includeFileSize,
boolean includeImageSize,
boolean includePlayableDuration) {
WritableMap image = new WritableNativeMap();
Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
image.putString("uri", photoUri.toString());
@@ -586,9 +633,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
boolean isVideo = mimeType != null && mimeType.startsWith("video");
boolean putImageSizeSuccess = putImageSize(resolver, media, image, widthIndex, heightIndex,
photoUri, isVideo, includeImageSize);
photoUri, isVideo, includeImageSize);
boolean putPlayableDurationSuccess = putPlayableDuration(resolver, image, photoUri, isVideo,
includePlayableDuration);
includePlayableDuration);
if (includeFilename) {
File file = new File(media.getString(dataIndex));
@@ -612,11 +659,11 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
* @return Whether we succeeded in fetching and putting the playableDuration
*/
private static boolean putPlayableDuration(
ContentResolver resolver,
WritableMap image,
Uri photoUri,
boolean isVideo,
boolean includePlayableDuration) {
ContentResolver resolver,
WritableMap image,
Uri photoUri,
boolean isVideo,
boolean includePlayableDuration) {
image.putNull("playableDuration");
if (!includePlayableDuration || !isVideo) {
@@ -641,15 +688,15 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
// Do nothing. We can't handle this, and this is usually a system problem
}
try {
int timeInMillisec = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
playableDuration = timeInMillisec / 1000;
int timeInMillisecond = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
playableDuration = timeInMillisecond / 1000;
} catch (NumberFormatException e) {
success = false;
FLog.e(
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
}
retriever.release();
}
@@ -670,14 +717,14 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
}
private static boolean putImageSize(
ContentResolver resolver,
Cursor media,
WritableMap image,
int widthIndex,
int heightIndex,
Uri photoUri,
boolean isVideo,
boolean includeImageSize) {
ContentResolver resolver,
Cursor media,
WritableMap image,
int widthIndex,
int heightIndex,
Uri photoUri,
boolean isVideo,
boolean includeImageSize) {
image.putNull("width");
image.putNull("height");
@@ -686,20 +733,24 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
}
boolean success = true;
@Nullable AssetFileDescriptor photoDescriptor = null;
/* Read height and width data from the gallery cursor columns */
int width = media.getInt(widthIndex);
int height = media.getInt(heightIndex);
/* If the columns don't contain the size information, read the media file */
if (width <= 0 || height <= 0) {
@Nullable AssetFileDescriptor mediaDescriptor = null;
try {
photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
mediaDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
} catch (FileNotFoundException e) {
success = false;
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
}
if (mediaDescriptor != null) {
if (isVideo) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(photoDescriptor.getFileDescriptor());
retriever.setDataSource(mediaDescriptor.getFileDescriptor());
} catch (RuntimeException e) {
// Do nothing. We can't handle this, and this is usually a system problem
}
@@ -709,53 +760,33 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
} catch (NumberFormatException e) {
success = false;
FLog.e(
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
ReactConstants.TAG,
"Number format exception occurred while trying to fetch video metadata for "
+ photoUri.toString(),
e);
}
retriever.release();
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
// Set inJustDecodeBounds to true so we don't actually load the Bitmap in memory,
// but only get its dimensions
// Set inJustDecodeBounds to true so we don't actually load the Bitmap, but only get its
// dimensions instead.
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(photoDescriptor.getFileDescriptor(), null, options);
BitmapFactory.decodeFileDescriptor(mediaDescriptor.getFileDescriptor(), null, options);
width = options.outWidth;
height = options.outHeight;
}
} catch (FileNotFoundException e) {
success = false;
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
}
}
/* Read the EXIF photo data to update height and width in case a rotation is encoded */
if (success && !isVideo && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
if (photoDescriptor == null) photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
ExifInterface exif = new ExifInterface(photoDescriptor.getFileDescriptor());
int rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
if (rotation == ExifInterface.ORIENTATION_ROTATE_90 || rotation == ExifInterface.ORIENTATION_ROTATE_270) {
// swap values
int temp = width;
width = height;
height = temp;
try {
mediaDescriptor.close();
} catch (IOException e) {
FLog.e(
ReactConstants.TAG,
"Can't close media descriptor "
+ photoUri.toString(),
e);
}
} catch (FileNotFoundException e) {
success = false;
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
} catch (IOException e) {
FLog.e(ReactConstants.TAG, "Could not get exif data for file " + photoUri.toString(), e);
}
}
if (photoDescriptor != null) {
try {
photoDescriptor.close();
} catch (IOException e) {
// Do nothing. We can't handle this, and this is usually a system problem
}
}
image.putInt("width", width);
@@ -764,40 +795,40 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
}
private static void putLocationInfo(
Cursor media,
WritableMap node,
int dataIndex,
boolean includeLocation) {
Cursor media,
WritableMap node,
int dataIndex,
boolean includeLocation) {
node.putNull("location");
if (!includeLocation) {
return;
}
try {
// location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE
// we manually obtain location metadata using ExifInterface#getLatLong(float[]).
// ExifInterface is added in API level 5
final ExifInterface exif = new ExifInterface(media.getString(dataIndex));
float[] imageCoordinates = new float[2];
boolean hasCoordinates = exif.getLatLong(imageCoordinates);
if (hasCoordinates) {
double longitude = imageCoordinates[1];
double latitude = imageCoordinates[0];
WritableMap location = new WritableNativeMap();
location.putDouble("longitude", longitude);
location.putDouble("latitude", latitude);
node.putMap("location", location);
}
} catch (IOException e) {
FLog.e(ReactConstants.TAG, "Could not read the metadata", e);
try {
// location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE
// we manually obtain location metadata using ExifInterface#getLatLong(float[]).
// ExifInterface is added in API level 5
final ExifInterface exif = new ExifInterface(media.getString(dataIndex));
float[] imageCoordinates = new float[2];
boolean hasCoordinates = exif.getLatLong(imageCoordinates);
if (hasCoordinates) {
double longitude = imageCoordinates[1];
double latitude = imageCoordinates[0];
WritableMap location = new WritableNativeMap();
location.putDouble("longitude", longitude);
location.putDouble("latitude", latitude);
node.putMap("location", location);
}
} catch (IOException e) {
FLog.e(ReactConstants.TAG, "Could not read the metadata", e);
}
}
/**
* Delete a set of images.
*
* @param uris array of file:// URIs of the images to delete
* @param uris array of file:// URIs of the images to delete
* @param promise to be resolved
*/
@ReactMethod
@@ -806,7 +837,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
promise.reject(ERROR_UNABLE_TO_DELETE, "Need at least one URI to delete");
} else {
new DeletePhotos(getReactApplicationContext(), uris, promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@@ -828,7 +859,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
ContentResolver resolver = mContext.getContentResolver();
// Set up the projection (we only need the ID)
String[] projection = { MediaStore.Images.Media._ID };
String[] projection = {MediaStore.Images.Media._ID};
// Match on the file path
String innerWhere = "?";
@@ -864,7 +895,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
mPromise.resolve(true);
} else {
mPromise.reject(ERROR_UNABLE_TO_DELETE,
"Could not delete all media, only deleted " + deletedCount + " photos.");
"Could not delete all media, only deleted " + deletedCount + " photos.");
}
}
}
@@ -0,0 +1,16 @@
package com.reactnativecommunity.cameraroll;
import android.webkit.MimeTypeMap;
public class Utils {
public static String getMimeType(String url) {
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
}
}
+4 -4
View File
@@ -1,13 +1,13 @@
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
presets: ['module:metro-react-native-babel-preset'],
plugins: [
[
"module-resolver",
'module-resolver',
{
alias: {
"@react-native-community/cameraroll": "./js/CameraRoll.js"
'@react-native-community/cameraroll': './src/index.ts'
},
cwd: "babelrc"
cwd: 'babelrc'
}
]
]
+35 -54
View File
@@ -9,18 +9,16 @@ import com.android.build.OutputFile
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../../node_modules/react-native/react.gradle"` line.
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* // the entry file for bundle generation
* entryFile: "index.android.js",
*
* // https://reactnative.dev/docs/performance#enable-the-ram-format
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
@@ -78,11 +76,9 @@ import com.android.build.OutputFile
*/
project.ext.react = [
// Start added for this project (react-native-cameraroll)
cliPath: "node_modules/react-native/local-cli/cli.js",
entryFile: "example/index.js",
cliPath: "../../../node_modules/react-native/local-cli/cli.js",
entryFile: "example/index.tsx",
root: "../../../",
// End added
enableHermes: false, // clean and rebuild if changing
]
@@ -125,6 +121,19 @@ def jscFlavor = 'org.webkit:android-jsc:+'
*/
def enableHermes = project.ext.react.get("enableHermes", false);
task downloadDependencies() {
description 'Download all dependencies to the Gradle cache'
doLast {
configurations.findAll().each { config ->
if (config.name.contains("minReactNative") && config.canBeResolved) {
print config.name
print '\n'
config.files
}
}
}
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -134,15 +143,13 @@ android {
}
defaultConfig {
applicationId "com.camerarollexample"
applicationId "com.reactnativecommunity.cameraroll.example"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
splits {
abi {
@@ -159,20 +166,25 @@ android {
keyAlias 'androiddebugkey'
keyPassword 'android'
}
release {
if (project.hasProperty('EXAMPLE_APP_STORE_FILE')) {
storeFile file(EXAMPLE_APP_STORE_FILE)
storePassword EXAMPLE_APP_STORE_PASSWORD
keyAlias EXAMPLE_APP_KEY_ALIAS
keyPassword EXAMPLE_APP_KEY_PASSWORD
}
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
@@ -184,30 +196,17 @@ android {
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
// You will not need to manually link like this. This is a quirk of the example project
implementation project(':react-native-cameraroll')
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
if (enableHermes) {
def hermesPath = "../../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
@@ -216,11 +215,7 @@ dependencies {
implementation jscFlavor
}
// Added for this project (react-native-cameraroll)
implementation project(':react-native-cameraroll')
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
androidTestImplementation('com.wix:detox:+')
androidTestImplementation 'junit:junit:4.12'
}
// Run this once to be able to run the application with BUCK
@@ -230,18 +225,4 @@ task copyDownloadableDepsToLibs(type: Copy) {
into 'libs'
}
apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
// Added for react-native-circleci-orb from https://github.com/react-native-community/react-native-circleci-orb/tree/v4.4.2#android
task downloadDependencies() {
description 'Download all dependencies to the Gradle cache'
doLast {
configurations.findAll().each { config ->
if (config.name.contains("minReactNative") && config.canBeResolved) {
print config.name
print '\n'
config.files
}
}
}
}
apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
@@ -12,7 +12,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
+17 -21
View File
@@ -2,31 +2,34 @@
buildscript {
ext {
buildToolsVersion = "29.0.2"
// detox requires minSdkVersion 18
minSdkVersion = 18
compileSdkVersion = 29
targetSdkVersion = 29
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
kotlinVersion = "1.3.70"
buildToolsVersion = "30.0.2"
ndkVersion = "21.4.7075529"
minSdkVersion = 21
compileSdkVersion = 30
targetSdkVersion = 30
androidXVersion = "1.+" // Default AndroidX dependency
androidXCore = "1.0.2"
// e2e
kotlinVersion = '1.5.10'
}
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:3.5.3")
classpath("com.android.tools.build:gradle:3.4.2")
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
allprojects {
repositories {
mavenLocal()
mavenCentral()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../../node_modules/react-native/android")
@@ -37,13 +40,6 @@ allprojects {
}
google()
jcenter()
maven { url 'https://www.jitpack.io' }
// Start added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
maven {
url("$rootDir/../../node_modules/detox/Detox-android")
}
// End added
maven { url 'https://jitpack.io' }
}
}
}
+3 -3
View File
@@ -9,8 +9,8 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -25,4 +25,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.54.0
FLIPPER_VERSION=0.125.0
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1 -1
View File
@@ -14,7 +14,7 @@ const React = require('react');
const ReactNative = require('react-native');
const {Image, StyleSheet, View, ScrollView} = ReactNative;
import type {PhotoIdentifier} from '../../js/CameraRoll';
import type {PhotoIdentifier} from '../../src/CameraRoll';
type Props = $ReadOnly<{|
asset: PhotoIdentifier,
+2 -2
View File
@@ -21,8 +21,8 @@ const {
TouchableOpacity,
Dimensions,
} = ReactNative;
import CameraRoll from '../../js/CameraRoll';
import type {PhotoIdentifier, GroupTypes} from '../../js/CameraRoll';
import CameraRoll from '../../src/CameraRoll';
import type {PhotoIdentifier, GroupTypes} from '../../src/CameraRoll';
const invariant = require('invariant');
+1 -3
View File
@@ -25,7 +25,7 @@ const {
Linking,
} = ReactNative;
import CameraRoll from '../../js/CameraRoll';
import {CameraRoll} from '../../src';
const groupByEveryN = function groupByEveryN(num) {
const n = num;
@@ -133,7 +133,6 @@ class CameraRollView extends React.Component {
};
render() {
console.log({data: this.state.data});
return (
<FlatList
keyExtractor={(_, idx) => String(idx)}
@@ -166,7 +165,6 @@ class CameraRollView extends React.Component {
};
_renderItem = ({item}) => {
console.log({item});
return (
<View style={styles.row}>
{item.map(image => (image ? this.props.renderImage(image) : null))}
+3 -2
View File
@@ -1,4 +1,5 @@
import * as React from 'react';
import React from 'react';
import {Component} from 'react';
import {
SafeAreaView,
StyleSheet,
@@ -40,7 +41,7 @@ const examples: Example[] = [
* Shows a button which opens up a Modal to switch between examples, as well
* as the current example itself.
*/
export default class ExamplesContainer extends React.Component<Props, State> {
export default class ExamplesContainer extends Component<Props, State> {
state: State = {showChangeExampleModal: false, currentExampleIndex: 0};
render() {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
Keyboard,
} from 'react-native';
// @ts-ignore: CameraRollExample has no typings in same folder
import CameraRoll from '../../js/CameraRoll';
import CameraRoll from '../../src/CameraRoll';
interface State {
fetchingPhotos: boolean;
+9 -2
View File
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
143879351AAD238D00F088A5 /* RNCCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RNCCameraRollManager.m */; };
2F2D96EC28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F2D96EB28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m */; };
4F788CB822226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F788CB722226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m */; };
/* End PBXBuildFile section */
@@ -26,6 +27,8 @@
/* Begin PBXFileReference section */
143879331AAD238D00F088A5 /* RNCCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RNCCameraRollManager.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
143879341AAD238D00F088A5 /* RNCCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCCameraRollManager.m; sourceTree = "<group>"; };
2F2D96EA28ABF0FE00B2EF6B /* RNCCameraRollPermissionModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNCCameraRollPermissionModule.h; sourceTree = "<group>"; };
2F2D96EB28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNCCameraRollPermissionModule.m; sourceTree = "<group>"; };
4F788CB622226740001DB9D2 /* RNCAssetsLibraryRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCAssetsLibraryRequestHandler.h; sourceTree = "<group>"; };
4F788CB722226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCAssetsLibraryRequestHandler.m; sourceTree = "<group>"; };
58B5115D1A9E6B3D00147676 /* libRNCCameraRoll.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCCameraRoll.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -45,6 +48,8 @@
58B511541A9E6B3D00147676 = {
isa = PBXGroup;
children = (
2F2D96EB28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m */,
2F2D96EA28ABF0FE00B2EF6B /* RNCCameraRollPermissionModule.h */,
4F788CB622226740001DB9D2 /* RNCAssetsLibraryRequestHandler.h */,
4F788CB722226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m */,
143879331AAD238D00F088A5 /* RNCCameraRollManager.h */,
@@ -98,11 +103,12 @@
};
};
};
buildConfigurationList = 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraRoll" */;
buildConfigurationList = 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraroll" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
);
mainGroup = 58B511541A9E6B3D00147676;
@@ -122,6 +128,7 @@
files = (
4F788CB822226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m in Sources */,
143879351AAD238D00F088A5 /* RNCCameraRollManager.m in Sources */,
2F2D96EC28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -266,7 +273,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraRoll" */ = {
58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraroll" */ = {
isa = XCConfigurationList;
buildConfigurations = (
58B5116F1A9E6B3D00147676 /* Debug */,
+160
View File
@@ -472,6 +472,166 @@ RCT_EXPORT_METHOD(deletePhotos:(NSArray<NSString *>*)assets
];
}
RCT_EXPORT_METHOD(getPhotoByInternalID:(NSString *)internalId
options:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
checkPhotoLibraryConfig();
BOOL const convertHeic = [RCTConvert BOOL:options[@"convertHeicImages"]];
requestPhotoLibraryAccess(reject, ^(bool isLimited){
PHFetchResult<PHAsset *> *fetchResult;
PHAsset *asset;
NSString *mediaIdentifier = internalId;
if ([internalId rangeOfString:@"ph://"].location != NSNotFound) {
mediaIdentifier = [internalId stringByReplacingOccurrencesOfString:@"ph://"
withString:@""];
}
fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[mediaIdentifier] options:nil];
if(fetchResult){
asset = fetchResult.firstObject;//only object in the array.
}
if(asset){
__block NSURL *imageURL = [[NSURL alloc]initWithString:@""];
NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo
? @"video"
: (asset.mediaType == PHAssetMediaTypeImage
? @"image"
: (asset.mediaType == PHAssetMediaTypeAudio
? @"audio"
: @"unknown")));
CLLocation *const loc = asset.location;
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
if (![assetResources firstObject]) {
return;
}
PHAssetResource *const _Nonnull resource = [assetResources firstObject];
__block NSString *originalFilename = resource.originalFilename;
NSString *const uniformMimeType = resource.uniformTypeIdentifier;
__block NSString *filePath = @"";
// check if HEIC extension asset
if (convertHeic && asset.mediaType == PHAssetMediaTypeImage && [uniformMimeType isEqual: @"public.heic"]) {
// convert to JPEG
PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new];
requestOptions.networkAccessAllowed = YES;
requestOptions.version = PHImageRequestOptionsVersionUnadjusted;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
CGSize const targetSize = CGSizeMake((CGFloat)asset.pixelWidth, (CGFloat)asset.pixelHeight);
[[PHImageManager defaultManager] requestImageForAsset:asset
targetSize:targetSize
contentMode:PHImageContentModeDefault
options:requestOptions
resultHandler:^(UIImage * _Nullable image,
NSDictionary * _Nullable info) {
NSError *const error = [info objectForKey:PHImageErrorKey];
if (error) {
reject(@"Error while converting to JPEG image",@"Error while converting",error);
}
originalFilename = [originalFilename stringByReplacingOccurrencesOfString:@"HEIC" withString:@"JPEG" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [originalFilename length])];
NSData *const imageData = UIImageJPEGRepresentation(image, 1.0);
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *fullPath = [NSTemporaryDirectory() stringByAppendingPathComponent:originalFilename];
if ([fileManager createFileAtPath:fullPath contents:imageData attributes:nil]) {
unsigned long long fileSize = [[fileManager attributesOfItemAtPath:fullPath error:nil] fileSize];
resolve(@{
@"node": @{
@"type": assetMediaTypeLabel,
@"image": @{
@"filepath": fullPath,
@"filename": originalFilename,
@"height": @([asset pixelHeight]),
@"width": @([asset pixelWidth]),
@"isStored": @YES,
@"playableDuration": @([asset duration]), // fractional seconds
@"fileSize": @(fileSize)
},
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
@"location": (loc ? @{
@"latitude": @(loc.coordinate.latitude),
@"longitude": @(loc.coordinate.longitude),
@"altitude": @(loc.altitude),
@"heading": @(loc.course),
@"speed": @(loc.speed), // speed in m/s
} : @{})
}
});
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Failed to create tmp file for asset %@.", originalFilename];
NSError *error = RCTErrorWithMessage(errorMessage);
reject(@"Error while creating image tmp file",@"Error creating tmp file",error);
}
}];
} else {
NSNumber* fileSize = [resource valueForKey:@"fileSize"];
PHContentEditingInputRequestOptions *const editOptions = [PHContentEditingInputRequestOptions new];
// Download asset if on icloud.
editOptions.networkAccessAllowed = YES;
[asset requestContentEditingInputWithOptions:editOptions completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
imageURL = contentEditingInput.fullSizeImageURL;
if (imageURL.absoluteString.length != 0) {
filePath = [imageURL.absoluteString stringByReplacingOccurrencesOfString:@"pathfile:" withString:@"file:"];
resolve(@{
@"node": @{
@"type": assetMediaTypeLabel,
@"image": @{
@"filepath": filePath,
@"filename": originalFilename,
@"height": @([asset pixelHeight]),
@"width": @([asset pixelWidth]),
@"isStored": @YES,
@"playableDuration": @([asset duration]), // fractional seconds
@"fileSize": fileSize
},
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
@"location": (loc ? @{
@"latitude": @(loc.coordinate.latitude),
@"longitude": @(loc.coordinate.longitude),
@"altitude": @(loc.altitude),
@"heading": @(loc.course),
@"speed": @(loc.speed), // speed in m/s
} : @{})
}
});
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset"
" with localIdentifier %@ with no error message.", internalId];
NSError *error = RCTErrorWithMessage(errorMessage);
reject(@"Error while getting file path",@"Error while getting file path",error);
}
}];
}
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset"
" with localIdentifier %@ with no error message.", internalId];
NSError *error = RCTErrorWithMessage(errorMessage);
reject(@"No asset found",@"No asset found",error);
}
}, false);
}
static void checkPhotoLibraryConfig()
{
#if RCT_DEV
+23
View File
@@ -0,0 +1,23 @@
//
// CameraRollPermissionModule.h
// RNCCameraRoll
//
// Created by sakhi idris on 16/08/2022.
// Copyright © 2022 Facebook. All rights reserved.
//
#import <AVFoundation/AVFoundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <Photos/Photos.h>
typedef enum {
RNPermissionStatusNotDetermined = 0,
RNPermissionStatusRestricted = 1,
RNPermissionStatusDenied = 2,
RNPermissionStatusAuthorized = 3,
RNPermissionStatusLimited = 4,
} RNPermissionStatus;
@interface RNCCameraRollPermissionModule : RCTEventEmitter <RCTBridgeModule, PHPhotoLibraryChangeObserver>
@end
+192
View File
@@ -0,0 +1,192 @@
//
// CameraRollPermissionModule.m
// RNCCameraRoll
//
// Created by sakhi idris on 16/08/2022.
// Copyright © 2022 Facebook. All rights reserved.
//
#import "RNCCameraRollPermissionModule.h"
#import <React/RCTUtils.h>
#import <React/RCTConvert.h>
@import Photos;
@import PhotosUI;
@implementation RNCCameraRollPermissionModule
{
bool hasListeners;
}
#pragma mark - Access Levels
static NSString * const ADD_ONLY = @"addOnly";
static NSString * const READ_WRITE = @"readWrite";
// Will be called when this module's first listener is added.
-(void)startObserving {
hasListeners = YES;
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
}
// Will be called when this module's last listener is removed, or on dealloc.
-(void)stopObserving {
hasListeners = NO;
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"onLibrarySelectionChange"];
}
- (NSString *)stringForStatus:(RNPermissionStatus)status {
switch (status) {
case RNPermissionStatusRestricted:
return @"unavailable";
case RNPermissionStatusNotDetermined:
return @"not-determined";
case RNPermissionStatusDenied:
return @"denied";
case RNPermissionStatusLimited:
return @"limited";
case RNPermissionStatusAuthorized:
return @"granted";
}
}
- (void)checkCameraRollPermission:(NSString *) accessLevel
resolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
rejecter:(void (^ _Nonnull)(NSString *code, NSString *message))reject {
PHAuthorizationStatus status;
if (@available(iOS 14.0, *)) {
PHAccessLevel requestedAccessLevel;
if ([accessLevel isEqualToString: ADD_ONLY]) {
requestedAccessLevel = PHAccessLevelAddOnly;
} else if ([accessLevel isEqualToString: READ_WRITE]) {
requestedAccessLevel = PHAccessLevelReadWrite;
} else {
return reject(@"incorrect_access_level", @"The requested access level does not exist");
}
status = [PHPhotoLibrary authorizationStatusForAccessLevel:requestedAccessLevel];
} else {
status = [PHPhotoLibrary authorizationStatus];
}
switch (status) {
case PHAuthorizationStatusNotDetermined:
return resolve(RNPermissionStatusNotDetermined);
case PHAuthorizationStatusRestricted:
return resolve(RNPermissionStatusRestricted);
case PHAuthorizationStatusDenied:
return resolve(RNPermissionStatusDenied);
case PHAuthorizationStatusLimited:
return resolve(RNPermissionStatusLimited);
case PHAuthorizationStatusAuthorized:
return resolve(RNPermissionStatusAuthorized);
}
}
- (void)requestCameraRollReadWritePermission:(void (^ _Nonnull)(RNPermissionStatus))resolve
rejecter:(void (^ _Nonnull)(NSString *code, NSString *message))reject {
if (@available(iOS 14.0, *)) {
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(__unused PHAuthorizationStatus status) {
[self checkCameraRollPermission: READ_WRITE resolver: resolve rejecter:reject];
}];
} else {
[PHPhotoLibrary requestAuthorization:^(__unused PHAuthorizationStatus status) {
[self checkCameraRollPermission: READ_WRITE resolver: resolve rejecter:reject];
}];
}
}
- (void)requestCameraRollAddOnlyPermission:(void (^ _Nonnull)(RNPermissionStatus))resolve
rejecter:(void (^ _Nonnull)(NSString *code, NSString *message))reject {
if (@available(iOS 14.0, *)) {
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelAddOnly handler:^(__unused PHAuthorizationStatus status) {
[self checkCameraRollPermission: ADD_ONLY resolver: resolve rejecter:reject];
}];
} else {
[PHPhotoLibrary requestAuthorization:^(__unused PHAuthorizationStatus status) {
[self checkCameraRollPermission: ADD_ONLY resolver: resolve rejecter:reject];
}];
}
}
- (void)refreshLimitedPhotoselection:(RCTPromiseResolveBlock _Nonnull)resolve
rejecter:(RCTPromiseRejectBlock _Nonnull)reject {
if (@available(iOS 14, *)) {
if ([PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite] != PHAuthorizationStatusLimited) {
return reject(@"cannot_open_limited_picker", @"Photo library permission isn't limited", nil);
}
UIViewController *presentedViewController = RCTPresentedViewController();
[[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:presentedViewController];
resolve(@(true));
} else {
reject(@"cannot_open_limited_picker", @"Available on iOS 14 or higher", nil);
}
}
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
if (hasListeners && changeInstance != nil) {
[self sendEventWithName:@"onLibrarySelectionChange" body:@"Changes occured"];
}
}
RCT_EXPORT_METHOD(checkPermission:
(NSString *) accessLevel
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject) {
[self checkCameraRollPermission:accessLevel resolver:^(RNPermissionStatus status) {
resolve([self stringForStatus:status]);
} rejecter:^(NSString *code, NSString *message) {
reject(code, message, nil);
}];
}
RCT_EXPORT_METHOD(requestReadWritePermission:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
[self requestCameraRollReadWritePermission:^(RNPermissionStatus status) {
resolve([self stringForStatus:status]);
} rejecter:^(NSString *code, NSString *message) {
reject(code, message, nil);
}];
}
RCT_EXPORT_METHOD(requestAddOnlyPermission:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
[self requestCameraRollAddOnlyPermission:^(RNPermissionStatus status) {
resolve([self stringForStatus:status]);
} rejecter:^(NSString *code, NSString *message) {
reject(code, message, nil);
}];
}
RCT_REMAP_METHOD(refreshPhotoSelection,
refreshLimitedPhotoselectionWithResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[self refreshLimitedPhotoselection:resolve rejecter:reject];
}
@end
-1
View File
@@ -1 +0,0 @@
export default require('react-native').NativeModules.RNCCameraRoll;
+56 -33
View File
@@ -4,17 +4,30 @@
"homepage": "https://github.com/react-native-community/react-native-cameraroll#readme",
"version": "4.1.2",
"description": "React Native Camera Roll for iOS & Android",
"main": "./js/CameraRoll.js",
"types": "./typings/CameraRoll.d.ts",
"main": "lib/commonjs/index",
"module": "lib/module/index",
"types": "lib/typescript/index.d.ts",
"react-native": "src/index.ts",
"source": "src/index",
"files": [
"src/",
"lib/",
"android/",
"ios/",
"react-native-cameraroll.podspec",
"README.md"
],
"scripts": {
"prepare": "bob build",
"start": "react-native start",
"start:android": "react-native run-android --root example/",
"start:ios": "react-native run-ios --project-path example/ios",
"test": "yarn validate:eslint && yarn validate:flow && yarn validate:typescript && yarn test:jest",
"validate:eslint": "eslint 'js/**/*.js' 'example/**/*.js'",
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"validate:eslint": "eslint 'src/**/*.ts'",
"validate:flow": "flow check",
"validate:typescript": "tsc --project ./",
"test:jest": "jest js/",
"test:jest": "jest src/",
"test:detox:android:test:debug": "detox test -c android.emu.debug",
"test:detox:android:test:release": "detox test -c android.emu.release",
"test:detox:android:build:debug": "detox build -c android.emu.debug",
@@ -38,33 +51,47 @@
],
"license": "MIT",
"peerDependencies": {
"react": "16 || 17 || 18",
"react-native": ">=0.60"
"react-native": ">=0.59"
},
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/runtime": "^7.9.6",
"@react-native-community/eslint-config": "^2.0.0",
"@semantic-release/git": "7.0.8",
"@types/react-native": "^0.62.10",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.0.1",
"babel-plugin-module-resolver": "^3.2.0",
"detox": "^16.5.0",
"eslint": "^7.0.0",
"eslint-plugin-prettier": "^3.0.1",
"flow-bin": "^0.122.0",
"husky": "^2.2.0",
"jest": "^26.0.1",
"metro-react-native-babel-preset": "^0.59.0",
"prettier": "^1.17.0",
"pretty-quick": "^1.10.0",
"react": "16.13.1",
"react-native": "0.63.4",
"react-test-renderer": "16.11.0",
"rimraf": "^2.6.3",
"semantic-release": "15.13.3",
"typescript": "^3.4.1"
"react-native-builder-bob": "0.18.3",
"@babel/core": "7.18.10",
"@babel/runtime": "7.18.9",
"@react-native-community/eslint-config": "3.1.0",
"@semantic-release/git": "10.0.1",
"@types/react-native": "0.62.10",
"babel-core": "7.0.0-bridge.0",
"babel-plugin-module-resolver": "4.1.0",
"babel-jest": "28.1.3",
"detox": "19.9.3",
"eslint": "8.22.0",
"eslint-plugin-prettier": "4.2.1",
"husky": "8.0.1",
"jest": "28.1.3",
"@types/jest": "28.1.7",
"prettier": "2.7.1",
"metro-react-native-babel-preset": "0.72.0",
"pretty-quick": "3.1.3",
"react": "17.0.2",
"react-native": "0.66.0",
"react-test-renderer": "17.0.2",
"semantic-release": "19.0.3",
"rimraf": "3.0.2",
"typescript": "4.7.4"
},
"react-native-builder-bob": {
"source": "src",
"output": "lib",
"targets": [
"commonjs",
"module",
[
"typescript",
{
"project": "tsconfig.json"
}
]
]
},
"rn-docs": {
"title": "CameraRoll",
@@ -76,7 +103,6 @@
}
},
"resolutions": {
"lodash": "4.17.15",
"@react-native-community/cli-platform-android": "~4.3.0"
},
"jest": {
@@ -126,8 +152,5 @@
"repository": {
"type": "git",
"url": "https://github.com/react-native-community/react-native-cameraroll.git"
},
"dependencies": {
"invariant": "^2.2.4"
}
}
+86 -95
View File
@@ -3,16 +3,10 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {Platform} from 'react-native';
import RNCCameraRoll from './nativeInterface';
const invariant = require('invariant');
const GROUP_TYPES_OPTIONS = {
Album: 'Album',
All: 'All', // default
@@ -29,7 +23,14 @@ const ASSET_TYPE_OPTIONS = {
Photos: 'Photos',
};
export type GroupTypes = $Keys<typeof GROUP_TYPES_OPTIONS>;
export type GroupTypes =
| 'Album'
| 'All'
| 'Event'
| 'Faces'
| 'Library'
| 'PhotoStream'
| 'SavedPhotos';
export type Include =
| 'filename'
@@ -38,6 +39,8 @@ export type Include =
| 'imageSize'
| 'playableDuration';
export type AssetType = 'All' | 'Videos' | 'Photos';
/**
* Shape of the param arg for the `getPhotos` function.
*/
@@ -46,97 +49,101 @@ export type GetPhotosParams = {
* The number of photos wanted in reverse order of the photo application
* (i.e. most recent first).
*/
first: number,
first: number;
/**
* A cursor that matches `page_info { end_cursor }` returned from a previous
* call to `getPhotos`
*/
after?: string,
after?: string;
/**
* Specifies which group types to filter the results to.
*/
groupTypes?: GroupTypes,
groupTypes?: GroupTypes;
/**
* Specifies filter on group names, like 'Recent Photos' or custom album
* titles.
*/
groupName?: string,
groupName?: string;
/**
* Specifies filter on asset type
*/
assetType?: $Keys<typeof ASSET_TYPE_OPTIONS>,
assetType?: AssetType;
/**
* Earliest time to get photos from. A timestamp in milliseconds. Exclusive.
*/
fromTime?: number,
fromTime?: number;
/**
* Latest time to get photos from. A timestamp in milliseconds. Inclusive.
*/
toTime?: Number,
toTime?: number;
/**
* Filter by mimetype (e.g. image/jpeg).
*/
mimeTypes?: Array<string>,
mimeTypes?: Array<string>;
/**
* Specific fields in the output that we want to include, even though they
* might have some performance impact.
*/
include?: Include[],
include?: Include[];
};
export type PhotoIdentifier = {
node: {
type: string,
group_name: string,
type: string;
group_name: string;
image: {
filename: string | null,
uri: string,
height: number,
width: number,
fileSize: number | null,
playableDuration: number,
},
timestamp: number,
filename: string | null;
uri: string;
height: number;
width: number;
fileSize: number | null;
playableDuration: number;
};
timestamp: number;
location: {
latitude?: number,
longitude?: number,
altitude?: number,
heading?: number,
speed?: number,
} | null,
},
latitude?: number;
longitude?: number;
altitude?: number;
heading?: number;
speed?: number;
} | null;
};
};
export type PhotoConvertionOptions = {
convertHeicImages: boolean;
};
export type PhotoIdentifiersPage = {
edges: Array<PhotoIdentifier>,
edges: Array<PhotoIdentifier>;
page_info: {
has_next_page: boolean,
start_cursor?: string,
end_cursor?: string,
},
limited?: boolean,
has_next_page: boolean;
start_cursor?: string;
end_cursor?: string;
};
limited?: boolean;
};
export type SaveToCameraRollOptions = {
type?: 'photo' | 'video' | 'auto',
album?: string,
type?: 'photo' | 'video' | 'auto';
album?: string;
};
export type GetAlbumsParams = {
assetType?: $Keys<typeof ASSET_TYPE_OPTIONS>,
assetType?: AssetType;
};
export type Album = {
title: string,
count: number,
title: string;
count: number;
};
/**
@@ -144,26 +151,16 @@ export type Album = {
*
* See https://facebook.github.io/react-native/docs/cameraroll.html
*/
class CameraRoll {
export class CameraRoll {
static GroupTypesOptions = GROUP_TYPES_OPTIONS;
static AssetTypeOptions = ASSET_TYPE_OPTIONS;
/**
* `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.
*/
static saveImageWithTag(tag: string): Promise<string> {
console.warn(
'`CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.',
);
return this.saveToCameraRoll(tag, 'photo');
}
/**
* On iOS: requests deletion of a set of photos from the camera roll.
* On Android: Deletes a set of photos from the camera roll.
*
*/
static deletePhotos(photoUris: Array<string>) {
static deletePhotos(photoUris: Array<string>): void {
return RNCCameraRoll.deletePhotos(photoUris);
}
@@ -175,28 +172,18 @@ class CameraRoll {
tag: string,
options: SaveToCameraRollOptions = {},
): Promise<string> {
let {type = 'auto', album = ''} = options;
invariant(
typeof tag === 'string',
'CameraRoll.saveToCameraRoll must be a valid string.',
);
invariant(
options.type === 'photo' ||
options.type === 'video' ||
options.type === 'auto' ||
options.type === undefined,
`The second argument to saveToCameraRoll must be 'photo' or 'video' or 'auto'. You passed ${type ||
'unknown'}`,
);
let {type = 'auto'} = options;
const {album = ''} = options;
if (tag === '') throw new Error('tag must be a valid string');
if (type === 'auto') {
if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) {
type = 'video';
} else {
type = 'photo';
}
const fileExtension = tag.split('.').slice(-1)[0] ?? '';
if (['mov', 'mp4'].indexOf(fileExtension) >= 0) type = 'video';
else type = 'photo';
}
return RNCCameraRoll.saveToCameraRoll(tag, {type, album});
}
static saveToCameraRoll(
tag: string,
type?: 'photo' | 'video' | 'auto',
@@ -207,19 +194,18 @@ class CameraRoll {
return CameraRoll.save(tag, {type});
}
static getAlbums(
params?: GetAlbumsParams = {assetType: ASSET_TYPE_OPTIONS.All},
params: GetAlbumsParams = {assetType: 'All'},
): Promise<Album[]> {
return RNCCameraRoll.getAlbums(params);
}
static getParamsWithDefaults(params: GetPhotosParams): GetPhotosParams {
const newParams = {...params};
if (!newParams.assetType) {
newParams.assetType = ASSET_TYPE_OPTIONS.All;
}
if (!newParams.groupTypes && Platform.OS !== 'android') {
newParams.groupTypes = GROUP_TYPES_OPTIONS.All;
}
if (newParams.assetType === undefined) newParams.assetType = 'All';
if (newParams.groupTypes === undefined && Platform.OS !== 'android')
newParams.groupTypes = 'All';
return newParams;
}
@@ -230,20 +216,25 @@ class CameraRoll {
* See https://facebook.github.io/react-native/docs/cameraroll.html#getphotos
*/
static getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage> {
params = CameraRoll.getParamsWithDefaults(params);
const promise = RNCCameraRoll.getPhotos(params);
params = this.getParamsWithDefaults(params);
return RNCCameraRoll.getPhotos(params);
}
if (arguments.length > 1) {
console.warn(
'CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead',
);
let successCallback = arguments[1];
const errorCallback = arguments[2] || (() => {});
promise.then(successCallback, errorCallback);
}
return promise;
/**
* Returns a Promise with photo internal path.
* if conversion is requested from HEIC then temporary file is created.
*
* @param internalID - PH photo internal ID.
* @param convertHeicImages - whether to convert or not heic images to JPEG.
* @returns Promise<PhotoIdentifier>
*/
static iosGetImageDataById(
internalID: string,
convertHeicImages = false,
): Promise<PhotoIdentifier> {
const conversionOption: PhotoConvertionOptions = {
convertHeicImages: convertHeicImages,
};
return RNCCameraRoll.getPhotoByInternalID(internalID, conversionOption);
}
}
module.exports = CameraRoll;
+51
View File
@@ -0,0 +1,51 @@
import {NativeEventEmitter, Platform} from 'react-native';
import CameraRollPermissionModule from './cameraRollPermissionNativeInterface';
/** Defines ios permission access levels for gallery */
export type AccessLevel = 'addOnly' | 'readWrite';
export type CameraRollAuthorizationStatus =
| 'granted'
| 'limited'
| 'denied'
| 'unavailable'
| 'blocked'
| 'not-determined';
const isIOS = Platform.OS === 'ios';
if (isIOS && CameraRollPermissionModule == null) {
console.error(
"photoLibraryPermissionModule: Native Module 'photoLibraryPermissionModule' was null! Did you run pod install?",
);
}
export const cameraRollEventEmitter = new NativeEventEmitter(
isIOS ? CameraRollPermissionModule : undefined,
);
export const iosReadGalleryPermission = (
accessLevel: AccessLevel,
): Promise<CameraRollAuthorizationStatus> => {
if (!isIOS) throw new Error('this module is available only for ios');
return CameraRollPermissionModule.checkPermission(accessLevel);
};
export const iosRequestReadWriteGalleryPermission =
(): Promise<CameraRollAuthorizationStatus> => {
if (!isIOS) throw new Error('this module is available only for ios');
return CameraRollPermissionModule.requestReadWritePermission();
};
export const iosRequestAddOnlyGalleryPermission =
(): Promise<CameraRollAuthorizationStatus> => {
if (!isIOS) throw new Error('this module is available only for ios');
return CameraRollPermissionModule.requestAddOnlyPermission();
};
export const iosRefreshGallerySelection = (): Promise<boolean> => {
if (!isIOS) throw new Error('this module is available only for ios');
return CameraRollPermissionModule.refreshPhotoSelection();
};
@@ -1,16 +1,6 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import {CameraRoll} from '../CameraRoll';
import CameraRoll from '../CameraRoll';
const NativeModule = require('../nativeInterface');
import NativeModule from '../nativeInterface';
jest.mock('../nativeInterface');
@@ -0,0 +1,3 @@
import {NativeModules} from 'react-native';
export default NativeModules.RNCCameraRollPermissionModule;
+2
View File
@@ -0,0 +1,2 @@
export * from './CameraRoll';
export * from './CameraRollIOSPermission';
+3
View File
@@ -0,0 +1,3 @@
import {NativeModules} from 'react-native';
export default NativeModules.RNCCameraRoll;
+20 -18
View File
@@ -1,25 +1,27 @@
{
"include": ["typings/**/*.d.ts", "example/**/*.ts", "example/**/*.tsx"],
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"importHelpers": true,
"jsx": "react",
"sourceMap": true,
"strict": true,
"allowJs": false,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"esModuleInterop": true,
"importsNotUsedAsValues": "error",
"forceConsistentCasingInFileNames": true,
"jsx": "react-native",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noStrictGenericChecks": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"lib": ["es2015", "es2016", "esnext", "dom"],
"allowSyntheticDefaultImports": true,
"noEmit": true,
"resolveJsonModule": true
"strict": true,
"target": "esnext",
"outDir": "lib"
},
"exclude": ["node_modules", "**/*.spec.ts"]
"include": ["src", ".eslintrc.js", "babel.config.js"],
"exclude": ["node_modules", "lib", "docs", "example"]
}
-182
View File
@@ -1,182 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
declare namespace CameraRoll {
type GroupType =
| 'Album'
| 'All'
| 'Event'
| 'Faces'
| 'Library'
| 'PhotoStream'
| 'SavedPhotos';
type AssetType = 'All' | 'Videos' | 'Photos';
type Include =
/** Ensures the filename is included. Has a large performance hit on iOS */
| 'filename'
/** Ensures the fileSize is included. Has a large performance hit on iOS */
| 'fileSize'
/** Ensures the location is included. Has a medium performance hit on Android */
| 'location'
/** Ensures the image width and height are included. Has a small performance hit on Android */
| 'imageSize'
/** Ensures the image playableDuration is included. Has a medium performance hit on Android */
| 'playableDuration';
/**
* Shape of the param arg for the `getPhotosFast` function.
*/
interface GetPhotosParams {
/**
* The number of photos wanted in reverse order of the photo application
* (i.e. most recent first).
*/
first: number;
/**
* A cursor that matches `page_info { end_cursor }` returned from a previous
* call to `getPhotos`. Note that using this will reduce performance
* slightly on iOS. An alternative is just using the `fromTime` and `toTime`
* filters, which have no such impact.
*/
after?: string;
/**
* Specifies which group types to filter the results to.
*/
groupTypes?: GroupType;
/**
* Specifies filter on group names, like 'Recent Photos' or custom album
* titles.
*/
groupName?: string;
/**
* Specifies filter on asset type
*/
assetType?: AssetType;
/**
* Filter by creation time with a timestamp in milliseconds. This time is
* exclusive, so we'll select all photos with `timestamp > fromTime`.
*/
fromTime?: number;
/**
* Filter by creation time with a timestamp in milliseconds. This time is
* inclusive, so we'll select all photos with `timestamp <= toTime`.
*/
toTime?: number;
/**
* Filter by mimetype (e.g. image/jpeg). Note that using this will reduce
* performance slightly on iOS.
*/
mimeTypes?: Array<string>;
/**
* Specific fields in the output that we want to include, even though they
* might have some performance impact.
*/
include?: Include[];
}
interface PhotoIdentifier {
node: {
type: string;
group_name: string;
image: {
/** Only set if the `include` parameter contains `filename`. */
filename: string | null;
uri: string;
/** Only set if the `include` parameter contains `imageSize`. */
height: number;
/** Only set if the `include` parameter contains `imageSize`. */
width: number;
/** Only set if the `include` parameter contains `fileSize`. */
fileSize: number | null;
/**
* Only set if the `include` parameter contains `playableDuration`.
* Will be null for images.
*/
playableDuration: number | null;
};
/** Timestamp in seconds. */
timestamp: number;
/** Only set if the `include` parameter contains `location`. */
location: {
latitude?: number;
longitude?: number;
altitude?: number;
heading?: number;
speed?: number;
} | null;
};
}
interface PhotoIdentifiersPage {
edges: Array<PhotoIdentifier>;
page_info: {
has_next_page: boolean;
start_cursor?: string;
end_cursor?: string;
};
}
interface GetAlbumsParams {
assetType?: AssetType;
}
interface Album {
title: string;
count: number;
}
type SaveToCameraRollOptions = {
type?: 'photo' | 'video' | 'auto';
album?: string;
};
/**
* `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.
*/
function saveImageWithTag(tag: string): Promise<string>;
/**
* Delete a photo from the camera roll or media library. photoUris is an array of photo uri's.
*/
function deletePhotos(photoUris: Array<string>): Promise<boolean>;
/**
* Saves the photo or video to the camera roll or photo library.
*/
function saveToCameraRoll(
tag: string,
type?: 'photo' | 'video',
): Promise<string>;
/**
* Saves the photo or video to the camera roll or photo library.
*/
function save(
tag: string,
options?: SaveToCameraRollOptions,
): Promise<string>;
/**
* Returns a Promise with photo identifier objects from the local camera
* roll of the device matching shape defined by `getPhotosReturnChecker`.
*/
function getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage>;
function getAlbums(params: GetAlbumsParams): Promise<Album[]>;
}
export = CameraRoll;
+5358 -5056
View File
File diff suppressed because it is too large Load Diff