From 2eff57f2466144a5ee9bdc81a2359da36829dcb1 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Mon, 9 Nov 2020 14:24:14 -0500 Subject: [PATCH 01/58] Initial recreation. --- camera/.eslintignore | 2 + camera/.gitignore | 61 ++ camera/.prettierignore | 2 + camera/CONTRIBUTING.md | 42 ++ camera/CapacitorCamera.podspec | 17 + camera/README.md | 19 + camera/android/.gitignore | 1 + camera/android/build.gradle | 56 ++ camera/android/gradle.properties | 24 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58695 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + camera/android/gradlew | 183 ++++++ camera/android/gradlew.bat | 100 +++ camera/android/proguard-rules.pro | 21 + camera/android/settings.gradle | 2 + .../android/ExampleInstrumentedTest.java | 26 + camera/android/src/main/AndroidManifest.xml | 3 + .../capacitorjs/plugins/camera/Camera.java | 8 + .../plugins/camera/CameraPlugin.java | 22 + .../main/res/layout/bridge_layout_main.xml | 15 + .../com/getcapacitor/ExampleUnitTest.java | 18 + camera/ios/Plugin.xcodeproj/project.pbxproj | 569 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Plugin.xcscheme | 77 +++ .../xcschemes/PluginTests.xcscheme | 68 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + camera/ios/Plugin/Camera.swift | 7 + camera/ios/Plugin/CameraPlugin.h | 10 + camera/ios/Plugin/CameraPlugin.m | 8 + camera/ios/Plugin/CameraPlugin.swift | 18 + camera/ios/Plugin/Info.plist | 24 + .../ios/PluginTests/CameraPluginTests.swift | 25 + camera/ios/PluginTests/Info.plist | 22 + camera/ios/Podfile | 16 + camera/package.json | 71 +++ camera/rollup.config.js | 21 + camera/src/definitions.ts | 9 + camera/src/index.ts | 15 + camera/src/web.ts | 14 + camera/tsconfig.json | 23 + 42 files changed, 1657 insertions(+) create mode 100644 camera/.eslintignore create mode 100644 camera/.gitignore create mode 100644 camera/.prettierignore create mode 100644 camera/CONTRIBUTING.md create mode 100644 camera/CapacitorCamera.podspec create mode 100644 camera/README.md create mode 100644 camera/android/.gitignore create mode 100644 camera/android/build.gradle create mode 100644 camera/android/gradle.properties create mode 100644 camera/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 camera/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 camera/android/gradlew create mode 100644 camera/android/gradlew.bat create mode 100644 camera/android/proguard-rules.pro create mode 100644 camera/android/settings.gradle create mode 100644 camera/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java create mode 100644 camera/android/src/main/AndroidManifest.xml create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java create mode 100644 camera/android/src/main/res/layout/bridge_layout_main.xml create mode 100644 camera/android/src/test/java/com/getcapacitor/ExampleUnitTest.java create mode 100644 camera/ios/Plugin.xcodeproj/project.pbxproj create mode 100644 camera/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 camera/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme create mode 100644 camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme create mode 100644 camera/ios/Plugin.xcworkspace/contents.xcworkspacedata create mode 100644 camera/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 camera/ios/Plugin/Camera.swift create mode 100644 camera/ios/Plugin/CameraPlugin.h create mode 100644 camera/ios/Plugin/CameraPlugin.m create mode 100644 camera/ios/Plugin/CameraPlugin.swift create mode 100644 camera/ios/Plugin/Info.plist create mode 100644 camera/ios/PluginTests/CameraPluginTests.swift create mode 100644 camera/ios/PluginTests/Info.plist create mode 100644 camera/ios/Podfile create mode 100644 camera/package.json create mode 100644 camera/rollup.config.js create mode 100644 camera/src/definitions.ts create mode 100644 camera/src/index.ts create mode 100644 camera/src/web.ts create mode 100644 camera/tsconfig.json diff --git a/camera/.eslintignore b/camera/.eslintignore new file mode 100644 index 000000000..9d0b71a3c --- /dev/null +++ b/camera/.eslintignore @@ -0,0 +1,2 @@ +build +dist diff --git a/camera/.gitignore b/camera/.gitignore new file mode 100644 index 000000000..70ccbf713 --- /dev/null +++ b/camera/.gitignore @@ -0,0 +1,61 @@ +# node files +dist +node_modules + +# iOS files +Pods +Podfile.lock +Build +xcuserdata + +# macOS files +.DS_Store + + + +# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin +gen +out + +# Gradle files +.gradle +build + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation + +# Android Studio captures folder +captures + +# IntelliJ +*.iml +.idea + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild diff --git a/camera/.prettierignore b/camera/.prettierignore new file mode 100644 index 000000000..9d0b71a3c --- /dev/null +++ b/camera/.prettierignore @@ -0,0 +1,2 @@ +build +dist diff --git a/camera/CONTRIBUTING.md b/camera/CONTRIBUTING.md new file mode 100644 index 000000000..019c41eef --- /dev/null +++ b/camera/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +This guide provides instructions for contributing to this Capacitor plugin. + +## Developing + +### Local Setup + +1. Fork and clone the repo. +1. Install the dependencies. + + ```shell + npm install + ``` + +1. Install SwiftLint if you're on macOS. + + ```shell + brew install swiftlint + ``` + +### Scripts + +#### `npm run build` + +Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen). + +It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported. + +Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`. + +#### `npm run verify` + +Build and validate the web and native projects. + +This is useful to run in CI to verify that the plugin builds for all platforms. + +#### `npm run lint` / `npm run fmt` + +Check formatting and code quality, autoformat/autofix if possible. + +This template is integrated with ESLint, Prettier, and SwiftLint. Using these tools is completely optional, but the [Capacitor Community](https://github.com/capacitor-community/) strives to have consistent code style and structure for easier cooperation. diff --git a/camera/CapacitorCamera.podspec b/camera/CapacitorCamera.podspec new file mode 100644 index 000000000..0d686c8b5 --- /dev/null +++ b/camera/CapacitorCamera.podspec @@ -0,0 +1,17 @@ +require 'json' + +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + +Pod::Spec.new do |s| + s.name = 'CapacitorCamera' + s.version = package['version'] + s.summary = package['description'] + s.license = package['license'] + s.homepage = package['repository']['url'] + s.author = package['author'] + s.source = { :git => package['repository']['url'], :tag => s.version.to_s } + s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' + s.ios.deployment_target = '11.0' + s.dependency 'Capacitor' + s.swift_version = '5.1' +end diff --git a/camera/README.md b/camera/README.md new file mode 100644 index 000000000..ba7543804 --- /dev/null +++ b/camera/README.md @@ -0,0 +1,19 @@ +# @capacitor/camera + +The Camera API provides the ability to take a photo with the camera or choose an existing one from the photo album. + +## Install + +```bash +npm install @capacitor/camera +npx cap sync +``` + +## API + + + + + + + diff --git a/camera/android/.gitignore b/camera/android/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/camera/android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/camera/android/build.gradle b/camera/android/build.gradle new file mode 100644 index 000000000..034dc47f7 --- /dev/null +++ b/camera/android/build.gradle @@ -0,0 +1,56 @@ +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' +} + +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:4.0.1' + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 29 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 21 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +repositories { + google() + jcenter() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" +} diff --git a/camera/android/gradle.properties b/camera/android/gradle.properties new file mode 100644 index 000000000..0566c221d --- /dev/null +++ b/camera/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true diff --git a/camera/android/gradle/wrapper/gradle-wrapper.jar b/camera/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f3d88b1c2faf2fc91d853cd5d4242b5547257070 GIT binary patch literal 58695 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 literal 0 HcmV?d00001 diff --git a/camera/android/gradle/wrapper/gradle-wrapper.properties b/camera/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..4e1cc9db6 --- /dev/null +++ b/camera/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/camera/android/gradlew b/camera/android/gradlew new file mode 100755 index 000000000..2fe81a7d9 --- /dev/null +++ b/camera/android/gradlew @@ -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" "$@" diff --git a/camera/android/gradlew.bat b/camera/android/gradlew.bat new file mode 100644 index 000000000..24467a141 --- /dev/null +++ b/camera/android/gradlew.bat @@ -0,0 +1,100 @@ +@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 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 diff --git a/camera/android/proguard-rules.pro b/camera/android/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/camera/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/camera/android/settings.gradle b/camera/android/settings.gradle new file mode 100644 index 000000000..1e5b8431f --- /dev/null +++ b/camera/android/settings.gradle @@ -0,0 +1,2 @@ +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') \ No newline at end of file diff --git a/camera/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/camera/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java new file mode 100644 index 000000000..58020e16c --- /dev/null +++ b/camera/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.android; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.android", appContext.getPackageName()); + } +} diff --git a/camera/android/src/main/AndroidManifest.xml b/camera/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d71b42f37 --- /dev/null +++ b/camera/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java new file mode 100644 index 000000000..efc0918dc --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java @@ -0,0 +1,8 @@ +package com.capacitorjs.plugins.camera; + +public class Camera { + + public String echo(String value) { + return value; + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java new file mode 100644 index 000000000..002d654fe --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -0,0 +1,22 @@ +package com.capacitorjs.plugins.camera; + +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +@CapacitorPlugin(name = "Camera") +public class CameraPlugin extends Plugin { + + private Camera implementation = new Camera(); + + @PluginMethod + public void echo(PluginCall call) { + String value = call.getString("value"); + + JSObject ret = new JSObject(); + ret.put("value", implementation.echo(value)); + call.resolve(ret); + } +} diff --git a/camera/android/src/main/res/layout/bridge_layout_main.xml b/camera/android/src/main/res/layout/bridge_layout_main.xml new file mode 100644 index 000000000..56fec1546 --- /dev/null +++ b/camera/android/src/main/res/layout/bridge_layout_main.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/camera/android/src/test/java/com/getcapacitor/ExampleUnitTest.java b/camera/android/src/test/java/com/getcapacitor/ExampleUnitTest.java new file mode 100644 index 000000000..a0fed0cfb --- /dev/null +++ b/camera/android/src/test/java/com/getcapacitor/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/camera/ios/Plugin.xcodeproj/project.pbxproj b/camera/ios/Plugin.xcodeproj/project.pbxproj new file mode 100644 index 000000000..a5d0abfa2 --- /dev/null +++ b/camera/ios/Plugin.xcodeproj/project.pbxproj @@ -0,0 +1,569 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; + 2F98D68224C9AAE500613A4C /* Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F98D68124C9AAE400613A4C /* Camera.swift */; }; + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; + 50ADFF97201F53D600D50D53 /* CameraPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* CameraPluginTests.swift */; }; + 50ADFF99201F53D600D50D53 /* CameraPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; }; + 50ADFFA82020EE4F00D50D53 /* CameraPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */; }; + 50E1A94820377CB70090CE1A /* CameraPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* CameraPlugin.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 50ADFF7F201F53D600D50D53 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 50ADFF87201F53D600D50D53; + remoteInfo = Plugin; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2F98D68124C9AAE400613A4C /* Camera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Camera.swift; sourceTree = ""; }; + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraPlugin.h; sourceTree = ""; }; + 50ADFF8C201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF96201F53D600D50D53 /* CameraPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginTests.swift; sourceTree = ""; }; + 50ADFF98201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFFA52020D75100D50D53 /* Capacitor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Capacitor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPlugin.m; sourceTree = ""; }; + 50E1A94720377CB70090CE1A /* CameraPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPlugin.swift; sourceTree = ""; }; + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = ""; }; + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 50ADFF84201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */, + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8E201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */, + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 50ADFF7E201F53D600D50D53 = { + isa = PBXGroup; + children = ( + 50ADFF8A201F53D600D50D53 /* Plugin */, + 50ADFF95201F53D600D50D53 /* PluginTests */, + 50ADFF89201F53D600D50D53 /* Products */, + 8C8E7744173064A9F6D438E3 /* Pods */, + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */, + ); + sourceTree = ""; + }; + 50ADFF89201F53D600D50D53 /* Products */ = { + isa = PBXGroup; + children = ( + 50ADFF88201F53D600D50D53 /* Plugin.framework */, + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 50ADFF8A201F53D600D50D53 /* Plugin */ = { + isa = PBXGroup; + children = ( + 50E1A94720377CB70090CE1A /* CameraPlugin.swift */, + 2F98D68124C9AAE400613A4C /* Camera.swift */, + 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */, + 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */, + 50ADFF8C201F53D600D50D53 /* Info.plist */, + ); + path = Plugin; + sourceTree = ""; + }; + 50ADFF95201F53D600D50D53 /* PluginTests */ = { + isa = PBXGroup; + children = ( + 50ADFF96201F53D600D50D53 /* CameraPluginTests.swift */, + 50ADFF98201F53D600D50D53 /* Info.plist */, + ); + path = PluginTests; + sourceTree = ""; + }; + 8C8E7744173064A9F6D438E3 /* Pods */ = { + isa = PBXGroup; + children = ( + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */, + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */, + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */, + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 50ADFFA52020D75100D50D53 /* Capacitor.framework */, + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */, + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 50ADFF85201F53D600D50D53 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF99201F53D600D50D53 /* CameraPlugin.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 50ADFF87201F53D600D50D53 /* Plugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */; + buildPhases = ( + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */, + 50ADFF83201F53D600D50D53 /* Sources */, + 50ADFF84201F53D600D50D53 /* Frameworks */, + 50ADFF85201F53D600D50D53 /* Headers */, + 50ADFF86201F53D600D50D53 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Plugin; + productName = Plugin; + productReference = 50ADFF88201F53D600D50D53 /* Plugin.framework */; + productType = "com.apple.product-type.framework"; + }; + 50ADFF90201F53D600D50D53 /* PluginTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */; + buildPhases = ( + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */, + 50ADFF8D201F53D600D50D53 /* Sources */, + 50ADFF8E201F53D600D50D53 /* Frameworks */, + 50ADFF8F201F53D600D50D53 /* Resources */, + 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */, + ); + name = PluginTests; + productName = PluginTests; + productReference = 50ADFF91201F53D600D50D53 /* PluginTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 50ADFF7F201F53D600D50D53 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Max Lynch"; + TargetAttributes = { + 50ADFF87201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + 50ADFF90201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 50ADFF7E201F53D600D50D53; + productRefGroup = 50ADFF89201F53D600D50D53 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 50ADFF87201F53D600D50D53 /* Plugin */, + 50ADFF90201F53D600D50D53 /* PluginTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 50ADFF86201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8F201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PluginTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Capacitor/Capacitor.framework", + "${BUILT_PRODUCTS_DIR}/CapacitorCordova/Cordova.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Capacitor.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cordova.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Plugin-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 50ADFF83201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50E1A94820377CB70090CE1A /* CameraPlugin.swift in Sources */, + 2F98D68224C9AAE500613A4C /* Camera.swift in Sources */, + 50ADFFA82020EE4F00D50D53 /* CameraPlugin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8D201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF97201F53D600D50D53 /* CameraPluginTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 50ADFF87201F53D600D50D53 /* Plugin */; + targetProxy = 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 50ADFF9A201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", + "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 50ADFF9B201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", + "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 50ADFF9D201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFF9E201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 50ADFFA0201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFFA1201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9A201F53D600D50D53 /* Debug */, + 50ADFF9B201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9D201F53D600D50D53 /* Debug */, + 50ADFF9E201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFFA0201F53D600D50D53 /* Debug */, + 50ADFFA1201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 50ADFF7F201F53D600D50D53 /* Project object */; +} diff --git a/camera/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/camera/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/camera/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/camera/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/camera/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/camera/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme b/camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme new file mode 100644 index 000000000..303f2621b --- /dev/null +++ b/camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme b/camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme new file mode 100644 index 000000000..3d8c88d25 --- /dev/null +++ b/camera/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/camera/ios/Plugin.xcworkspace/contents.xcworkspacedata b/camera/ios/Plugin.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..afad624ec --- /dev/null +++ b/camera/ios/Plugin.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/camera/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/camera/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/camera/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/camera/ios/Plugin/Camera.swift b/camera/ios/Plugin/Camera.swift new file mode 100644 index 000000000..2b68b82be --- /dev/null +++ b/camera/ios/Plugin/Camera.swift @@ -0,0 +1,7 @@ +import Foundation + +@objc public class Camera: NSObject { + @objc public func echo(_ value: String) -> String { + return value + } +} diff --git a/camera/ios/Plugin/CameraPlugin.h b/camera/ios/Plugin/CameraPlugin.h new file mode 100644 index 000000000..f2bd9e0bb --- /dev/null +++ b/camera/ios/Plugin/CameraPlugin.h @@ -0,0 +1,10 @@ +#import + +//! Project version number for Plugin. +FOUNDATION_EXPORT double PluginVersionNumber; + +//! Project version string for Plugin. +FOUNDATION_EXPORT const unsigned char PluginVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/camera/ios/Plugin/CameraPlugin.m b/camera/ios/Plugin/CameraPlugin.m new file mode 100644 index 000000000..af8e14a12 --- /dev/null +++ b/camera/ios/Plugin/CameraPlugin.m @@ -0,0 +1,8 @@ +#import +#import + +// Define the plugin using the CAP_PLUGIN Macro, and +// each method the plugin supports using the CAP_PLUGIN_METHOD macro. +CAP_PLUGIN(CameraPlugin, "Camera", + CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise); +) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift new file mode 100644 index 000000000..75947dc4d --- /dev/null +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -0,0 +1,18 @@ +import Foundation +import Capacitor + +/** + * Please read the Capacitor iOS Plugin Development Guide + * here: https://capacitorjs.com/docs/plugins/ios + */ +@objc(CameraPlugin) +public class CameraPlugin: CAPPlugin { + private let implementation = Camera() + + @objc func echo(_ call: CAPPluginCall) { + let value = call.getString("value") ?? "" + call.resolve([ + "value": implementation.echo(value) + ]) + } +} diff --git a/camera/ios/Plugin/Info.plist b/camera/ios/Plugin/Info.plist new file mode 100644 index 000000000..1007fd9dd --- /dev/null +++ b/camera/ios/Plugin/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/camera/ios/PluginTests/CameraPluginTests.swift b/camera/ios/PluginTests/CameraPluginTests.swift new file mode 100644 index 000000000..bdde46a9a --- /dev/null +++ b/camera/ios/PluginTests/CameraPluginTests.swift @@ -0,0 +1,25 @@ +import XCTest +@testable import Plugin + +class CameraTests: XCTestCase { + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testEcho() { + // This is an example of a functional test case for a plugin. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + let implementation = Camera() + let value = "Hello, World!" + let result = implementation.echo(value) + + XCTAssertEqual(value, result) + } +} diff --git a/camera/ios/PluginTests/Info.plist b/camera/ios/PluginTests/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/camera/ios/PluginTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/camera/ios/Podfile b/camera/ios/Podfile new file mode 100644 index 000000000..350751435 --- /dev/null +++ b/camera/ios/Podfile @@ -0,0 +1,16 @@ +platform :ios, '11.0' + +def capacitor_pods + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + pod 'Capacitor', :path => '../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../node_modules/@capacitor/ios' +end + +target 'Plugin' do + capacitor_pods +end + +target 'PluginTests' do + capacitor_pods +end diff --git a/camera/package.json b/camera/package.json new file mode 100644 index 000000000..0a8757880 --- /dev/null +++ b/camera/package.json @@ -0,0 +1,71 @@ +{ + "name": "@capacitor/camera", + "version": "0.0.1", + "description": "The Camera API provides the ability to take a photo with the camera or choose an existing one from the photo album.", + "main": "dist/plugin.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "author": "Ionic ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ionic-team/capacitor-plugins.git" + }, + "bugs": { + "url": "https://github.com/ionic-team/capacitor-plugins/issues" + }, + "keywords": [ + "capacitor", + "plugin", + "native" + ], + "scripts": { + "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", + "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin && cd ..", + "verify:android": "cd android && ./gradlew clean build test && cd ..", + "verify:web": "npm run build", + "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", + "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- autocorrect --format", + "eslint": "eslint . --ext ts", + "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", + "swiftlint": "node-swiftlint", + "docgen": "docgen --api CameraPlugin --output-readme README.md --output-json dist/docs.json", + "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js", + "clean": "rimraf ./dist", + "watch": "tsc --watch", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "@capacitor/android": "next", + "@capacitor/core": "next", + "@capacitor/docgen": "^0.0.10", + "@capacitor/ios": "next", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "^1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "@rollup/plugin-node-resolve": "^9.0.0", + "eslint": "^7.11.0", + "prettier": "^2.1.2", + "prettier-plugin-java": "^0.8.3", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.0.3" + }, + "peerDependencies": { + "@capacitor/core": "next" + }, + "prettier": "@ionic/prettier-config", + "swiftlint": "@ionic/swiftlint-config", + "eslintConfig": { + "extends": "@ionic/eslint-config/recommended" + }, + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + } +} diff --git a/camera/rollup.config.js b/camera/rollup.config.js new file mode 100644 index 000000000..d58bfbea1 --- /dev/null +++ b/camera/rollup.config.js @@ -0,0 +1,21 @@ +import nodeResolve from '@rollup/plugin-node-resolve'; + +export default { + input: 'dist/esm/index.js', + output: { + file: 'dist/plugin.js', + format: 'iife', + name: 'capacitorPlugin', // TODO: change this + globals: { + '@capacitor/core': 'capacitorExports', + }, + sourcemap: true, + }, + plugins: [ + nodeResolve({ + // allowlist of dependencies to bundle in + // @see https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve#resolveonly + resolveOnly: ['lodash'], + }), + ], +}; diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts new file mode 100644 index 000000000..0cf10c9d0 --- /dev/null +++ b/camera/src/definitions.ts @@ -0,0 +1,9 @@ +declare module '@capacitor/core' { + interface PluginRegistry { + Camera: CameraPlugin; + } +} + +export interface CameraPlugin { + echo(options: { value: string }): Promise<{ value: string }>; +} diff --git a/camera/src/index.ts b/camera/src/index.ts new file mode 100644 index 000000000..136a5fb79 --- /dev/null +++ b/camera/src/index.ts @@ -0,0 +1,15 @@ +import type { PluginImplementations } from '@capacitor/core'; +import { Plugins, registerPlugin } from '@capacitor/core'; + +import type { CameraPlugin } from './definitions'; +import { CameraWeb } from './web'; + +const implementations: PluginImplementations = { + android: Plugins.Camera, + ios: Plugins.Camera, + web: new CameraWeb(), +}; + +const Camera = registerPlugin('Camera', implementations).getImplementation(); + +export { Camera }; diff --git a/camera/src/web.ts b/camera/src/web.ts new file mode 100644 index 000000000..02c5824df --- /dev/null +++ b/camera/src/web.ts @@ -0,0 +1,14 @@ +import { WebPlugin } from '@capacitor/core'; + +import type { CameraPlugin } from './definitions'; + +export class CameraWeb extends WebPlugin implements CameraPlugin { + constructor() { + super({ name: 'Camera' }); + } + + async echo(options: { value: string }): Promise<{ value: string }> { + console.log('ECHO', options); + return options; + } +} diff --git a/camera/tsconfig.json b/camera/tsconfig.json new file mode 100644 index 000000000..538e088fd --- /dev/null +++ b/camera/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "declaration": true, + "esModuleInterop": true, + "lib": [ + "dom" + ], + "module": "es2015", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist/esm", + "pretty": true, + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "files": [ + "src/index.ts" + ] +} From 042474c7c84912bb8b584966f735deed646866a3 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Sep 2020 17:30:35 -0400 Subject: [PATCH 02/58] Adding license, lerna package --- camera/LICENSE | 23 +++++++++++++++++++++++ lerna.json | 1 + 2 files changed, 24 insertions(+) create mode 100644 camera/LICENSE diff --git a/camera/LICENSE b/camera/LICENSE new file mode 100644 index 000000000..e73c9ca0d --- /dev/null +++ b/camera/LICENSE @@ -0,0 +1,23 @@ +Copyright 2020-present Ionic +https://ionic.io + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/lerna.json b/lerna.json index 55932b913..cf9805131 100644 --- a/lerna.json +++ b/lerna.json @@ -4,6 +4,7 @@ "app", "app-launcher", "browser", + "camera", "clipboard", "device", "dialog", From 066c3425c211a3619cb262d9d7bc7c5efead13bb Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 10 Nov 2020 18:44:36 -0500 Subject: [PATCH 03/58] Importing --- .../CameraBottomSheetDialogFragment.java | 123 ++++ .../plugins/camera/CameraPlugin.java | 549 +++++++++++++++++- .../plugins/camera/CameraResultType.java | 17 + .../plugins/camera/CameraSettings.java | 89 +++ .../plugins/camera/CameraSource.java | 17 + .../plugins/camera/CameraUtils.java | 34 ++ .../plugins/camera/ExifWrapper.java | 153 +++++ .../plugins/camera/ImageUtils.java | 139 +++++ camera/ios/Plugin.xcodeproj/project.pbxproj | 14 +- camera/ios/Plugin/Camera.swift | 7 - camera/ios/Plugin/CameraExtensions.swift | 105 ++++ camera/ios/Plugin/CameraPlugin.m | 6 +- camera/ios/Plugin/CameraPlugin.swift | 336 ++++++++++- camera/ios/Plugin/CameraTypes.swift | 148 +++++ .../ios/PluginTests/CameraPluginTests.swift | 11 - camera/src/definitions.ts | 131 ++++- camera/src/index.ts | 16 +- camera/src/web.ts | 149 ++++- 18 files changed, 1992 insertions(+), 52 deletions(-) create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraResultType.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSource.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraUtils.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java create mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java delete mode 100644 camera/ios/Plugin/Camera.swift create mode 100644 camera/ios/Plugin/CameraExtensions.swift create mode 100644 camera/ios/Plugin/CameraTypes.swift diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java new file mode 100644 index 000000000..5f31d33f4 --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java @@ -0,0 +1,123 @@ +package com.capacitorjs.plugins.camera; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.DialogInterface; +import android.graphics.Color; +import android.view.View; +import android.view.Window; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import java.util.List; + +public class CameraBottomSheetDialogFragment extends BottomSheetDialogFragment { + + interface BottomSheetOnSelectedListener { + void onSelected(int index); + } + + interface BottomSheetOnCanceledListener { + void onCanceled(); + } + + private BottomSheetOnSelectedListener selectedListener; + private BottomSheetOnCanceledListener canceledListener; + private List options; + private String title; + + void setTitle(String title) { + this.title = title; + } + + void setOptions(List options, BottomSheetOnSelectedListener selectedListener, BottomSheetOnCanceledListener canceledListener) { + this.options = options; + this.selectedListener = selectedListener; + this.canceledListener = canceledListener; + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + if (canceledListener != null) { + this.canceledListener.onCanceled(); + } + } + + private BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() { + + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + dismiss(); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) {} + }; + + @Override + @SuppressLint("RestrictedApi") + public void setupDialog(Dialog dialog, int style) { + super.setupDialog(dialog, style); + + if (options == null || options.size() == 0) { + return; + } + + Window w = dialog.getWindow(); + + final float scale = getResources().getDisplayMetrics().density; + + float layoutPaddingDp16 = 16.0f; + float layoutPaddingDp12 = 12.0f; + float layoutPaddingDp8 = 8.0f; + int layoutPaddingPx16 = (int) (layoutPaddingDp16 * scale + 0.5f); + int layoutPaddingPx12 = (int) (layoutPaddingDp12 * scale + 0.5f); + int layoutPaddingPx8 = (int) (layoutPaddingDp8 * scale + 0.5f); + + CoordinatorLayout parentLayout = new CoordinatorLayout(getContext()); + + LinearLayout layout = new LinearLayout(getContext()); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(layoutPaddingPx16, layoutPaddingPx16, layoutPaddingPx16, layoutPaddingPx16); + TextView ttv = new TextView(getContext()); + ttv.setTextColor(Color.parseColor("#757575")); + ttv.setPadding(layoutPaddingPx8, layoutPaddingPx8, layoutPaddingPx8, layoutPaddingPx8); + ttv.setText(title); + layout.addView(ttv); + + for (int i = 0; i < options.size(); i++) { + final int optionIndex = i; + + TextView tv = new TextView(getContext()); + tv.setTextColor(Color.parseColor("#000000")); + tv.setPadding(layoutPaddingPx12, layoutPaddingPx12, layoutPaddingPx12, layoutPaddingPx12); + tv.setText(options.get(i)); + tv.setOnClickListener( + view -> { + if (selectedListener != null) { + selectedListener.onSelected(optionIndex); + } + dismiss(); + } + ); + layout.addView(tv); + } + + parentLayout.addView(layout.getRootView()); + + dialog.setContentView(parentLayout.getRootView()); + + CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) parentLayout.getParent()).getLayoutParams(); + CoordinatorLayout.Behavior behavior = params.getBehavior(); + + if (behavior != null && behavior instanceof BottomSheetBehavior) { + ((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback); + } + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 002d654fe..aebcf504d 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -1,22 +1,555 @@ package com.capacitorjs.plugins.camera; +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Base64; +import androidx.core.content.FileProvider; +import com.getcapacitor.FileUtils; import com.getcapacitor.JSObject; +import com.getcapacitor.Logger; +import com.getcapacitor.NativePlugin; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; -import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.PluginRequestCodes; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; -@CapacitorPlugin(name = "Camera") +/** + * The Camera plugin makes it easy to take a photo or have the user select a photo + * from their albums. + * + * On Android, this plugin sends an intent that opens the stock Camera app. + * + * Adapted from https://developer.android.com/training/camera/photobasics.html + */ +@NativePlugin( + name = "Camera", + requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT } +) public class CameraPlugin extends Plugin { + // Request codes + static final int REQUEST_IMAGE_CAPTURE = PluginRequestCodes.CAMERA_IMAGE_CAPTURE; + static final int REQUEST_IMAGE_PICK = PluginRequestCodes.CAMERA_IMAGE_PICK; + static final int REQUEST_IMAGE_EDIT = PluginRequestCodes.CAMERA_IMAGE_EDIT; + // Message constants + private static final String INVALID_RESULT_TYPE_ERROR = "Invalid resultType option"; + private static final String PERMISSION_DENIED_ERROR = "Unable to access camera, user denied permission request"; + private static final String NO_CAMERA_ERROR = "Device doesn't have a camera available"; + private static final String NO_CAMERA_ACTIVITY_ERROR = "Unable to resolve camera activity"; + private static final String IMAGE_FILE_SAVE_ERROR = "Unable to create photo on disk"; + private static final String IMAGE_PROCESS_NO_FILE_ERROR = "Unable to process image, file not found on disk"; + private static final String UNABLE_TO_PROCESS_IMAGE = "Unable to process image"; + private static final String IMAGE_EDIT_ERROR = "Unable to edit image"; + private static final String IMAGE_GALLERY_SAVE_ERROR = "Unable to save the image in the gallery"; - private Camera implementation = new Camera(); + private String imageFileSavePath; + private String imageEditedFileSavePath; + private Uri imageFileUri; + private boolean isEdited = false; + + private CameraSettings settings = new CameraSettings(); @PluginMethod - public void echo(PluginCall call) { - String value = call.getString("value"); + public void getPhoto(PluginCall call) { + isEdited = false; + + saveCall(call); + + settings = getSettings(call); + + doShow(call); + } + + private void doShow(PluginCall call) { + switch (settings.getSource()) { + case prompt: + showPrompt(call); + break; + case camera: + showCamera(call); + break; + case photos: + showPhotos(call); + break; + default: + showPrompt(call); + break; + } + } + + private void showPrompt(final PluginCall call) { + // We have all necessary permissions, open the camera + List options = new ArrayList(); + options.add(call.getString("promptLabelPhoto", "From Photos")); + options.add(call.getString("promptLabelPicture", "Take Picture")); + + final CameraBottomSheetDialogFragment fragment = new CameraBottomSheetDialogFragment(); + fragment.setTitle(call.getString("promptLabelHeader", "Photo")); + fragment.setOptions( + options, + index -> { + if (index == 0) { + settings.setSource(CameraSource.photos); + openPhotos(call); + } else if (index == 1) { + settings.setSource(CameraSource.camera); + openCamera(call); + } + }, + () -> call.reject("User cancelled photos app") + ); + fragment.show(getActivity().getSupportFragmentManager(), "capacitorModalsActionSheet"); + } + + private void showCamera(final PluginCall call) { + if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { + call.reject(NO_CAMERA_ERROR); + return; + } + openCamera(call); + } + + private void showPhotos(final PluginCall call) { + openPhotos(call); + } + + private boolean checkCameraPermissions(PluginCall call) { + // If we want to save to the gallery, we need two permissions + if ( + settings.isSaveToGallery() && + !(hasPermission(Manifest.permission.CAMERA) && hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) + ) { + pluginRequestPermissions( + new String[] { + Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + }, + REQUEST_IMAGE_CAPTURE + ); + return false; + } + // If we don't need to save to the gallery, we can just ask for camera permissions + else if (!hasPermission(Manifest.permission.CAMERA)) { + pluginRequestPermission(Manifest.permission.CAMERA, REQUEST_IMAGE_CAPTURE); + return false; + } + return true; + } + + private boolean checkPhotosPermissions(PluginCall call) { + if (!hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + pluginRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_IMAGE_CAPTURE); + return false; + } + return true; + } + + private CameraSettings getSettings(PluginCall call) { + CameraSettings settings = new CameraSettings(); + settings.setResultType(getResultType(call.getString("resultType"))); + settings.setSaveToGallery(call.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY)); + settings.setAllowEditing(call.getBoolean("allowEditing", false)); + settings.setQuality(call.getInt("quality", CameraSettings.DEFAULT_QUALITY)); + settings.setWidth(call.getInt("width", 0)); + settings.setHeight(call.getInt("height", 0)); + settings.setShouldResize(settings.getWidth() > 0 || settings.getHeight() > 0); + settings.setShouldCorrectOrientation(call.getBoolean("correctOrientation", CameraSettings.DEFAULT_CORRECT_ORIENTATION)); + try { + String foo = CameraSource.prompt.getSource(); + settings.setSource(CameraSource.valueOf(call.getString("source", CameraSource.prompt.getSource()))); + } catch (IllegalArgumentException ex) { + settings.setSource(CameraSource.prompt); + } + return settings; + } + + private CameraResultType getResultType(String resultType) { + if (resultType == null) { + return null; + } + try { + return CameraResultType.valueOf(resultType.toUpperCase()); + } catch (IllegalArgumentException ex) { + Logger.debug(getLogTag(), "Invalid result type \"" + resultType + "\", defaulting to base64"); + return CameraResultType.BASE64; + } + } + + public void openCamera(final PluginCall call) { + if (checkCameraPermissions(call)) { + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (takePictureIntent.resolveActivity(getContext().getPackageManager()) != null) { + // If we will be saving the photo, send the target file along + try { + String appId = getAppId(); + File photoFile = CameraUtils.createImageFile(getActivity()); + imageFileSavePath = photoFile.getAbsolutePath(); + // TODO: Verify provider config exists + imageFileUri = FileProvider.getUriForFile(getActivity(), appId + ".fileprovider", photoFile); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); + } catch (Exception ex) { + call.reject(IMAGE_FILE_SAVE_ERROR, ex); + return; + } + + startActivityForResult(call, takePictureIntent, REQUEST_IMAGE_CAPTURE); + } else { + call.reject(NO_CAMERA_ACTIVITY_ERROR); + } + } + } + + public void openPhotos(final PluginCall call) { + if (checkPhotosPermissions(call)) { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType("image/*"); + startActivityForResult(call, intent, REQUEST_IMAGE_PICK); + } + } + + public void processCameraImage(PluginCall call) { + if (imageFileSavePath == null) { + call.reject(IMAGE_PROCESS_NO_FILE_ERROR); + return; + } + // Load the image as a Bitmap + File f = new File(imageFileSavePath); + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + Uri contentUri = Uri.fromFile(f); + Bitmap bitmap = BitmapFactory.decodeFile(imageFileSavePath, bmOptions); + + if (bitmap == null) { + call.reject("User cancelled photos app"); + return; + } + + returnResult(call, bitmap, contentUri); + } + + public void processPickedImage(PluginCall call, Intent data) { + if (data == null) { + call.reject("No image picked"); + return; + } + + Uri u = data.getData(); + + InputStream imageStream = null; + + try { + imageStream = getContext().getContentResolver().openInputStream(u); + Bitmap bitmap = BitmapFactory.decodeStream(imageStream); + + if (bitmap == null) { + call.reject("Unable to process bitmap"); + return; + } + + returnResult(call, bitmap, u); + } catch (OutOfMemoryError err) { + call.reject("Out of memory"); + } catch (FileNotFoundException ex) { + call.reject("No such image found", ex); + } finally { + if (imageStream != null) { + try { + imageStream.close(); + } catch (IOException e) { + Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e); + } + } + } + } + + /** + * Save the modified image we've created to a temporary location, so we can + * return a URI to it later + * @param bitmap + * @param contentUri + * @param is + * @return + * @throws IOException + */ + private Uri saveTemporaryImage(Bitmap bitmap, Uri contentUri, InputStream is) throws IOException { + String filename = contentUri.getLastPathSegment(); + if (!filename.contains(".jpg") && !filename.contains(".jpeg")) { + filename += "." + (new java.util.Date()).getTime() + ".jpeg"; + } + File cacheDir = getContext().getCacheDir(); + File outFile = new File(cacheDir, filename); + FileOutputStream fos = new FileOutputStream(outFile); + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + fos.close(); + return Uri.fromFile(outFile); + } + + /** + * After processing the image, return the final result back to the caller. + * @param call + * @param bitmap + * @param u + */ + private void returnResult(PluginCall call, Bitmap bitmap, Uri u) { + try { + bitmap = prepareBitmap(bitmap, u); + } catch (IOException e) { + call.reject(UNABLE_TO_PROCESS_IMAGE); + return; + } + + ExifWrapper exif = ImageUtils.getExifData(getContext(), bitmap, u); + + // Compress the final image and prepare for output to client + ByteArrayOutputStream bitmapOutputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, settings.getQuality(), bitmapOutputStream); + + if (settings.isAllowEditing() && !isEdited) { + editImage(call, bitmap, u, bitmapOutputStream); + return; + } + + boolean saveToGallery = call.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY); + if (saveToGallery && (imageEditedFileSavePath != null || imageFileSavePath != null)) { + try { + String fileToSavePath = imageEditedFileSavePath != null ? imageEditedFileSavePath : imageFileSavePath; + File fileToSave = new File(fileToSavePath); + MediaStore.Images.Media.insertImage(getContext().getContentResolver(), fileToSavePath, fileToSave.getName(), ""); + } catch (FileNotFoundException e) { + Logger.error(getLogTag(), IMAGE_GALLERY_SAVE_ERROR, e); + } + } + + if (settings.getResultType() == CameraResultType.BASE64) { + returnBase64(call, exif, bitmapOutputStream); + } else if (settings.getResultType() == CameraResultType.URI) { + returnFileURI(call, exif, bitmap, u, bitmapOutputStream); + } else if (settings.getResultType() == CameraResultType.DATAURL) { + returnDataUrl(call, exif, bitmapOutputStream); + } else { + call.reject(INVALID_RESULT_TYPE_ERROR); + } + + // Result returned, clear stored paths + imageFileSavePath = null; + imageFileUri = null; + imageEditedFileSavePath = null; + } + + private void returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri u, ByteArrayOutputStream bitmapOutputStream) { + Uri newUri = getTempImage(bitmap, u, bitmapOutputStream); + if (newUri != null) { + JSObject ret = new JSObject(); + ret.put("format", "jpeg"); + ret.put("exif", exif.toJson()); + ret.put("path", newUri.toString()); + ret.put("webPath", FileUtils.getPortablePath(getContext(), bridge.getLocalUrl(), newUri)); + call.resolve(ret); + } else { + call.reject(UNABLE_TO_PROCESS_IMAGE); + } + } + + private Uri getTempImage(Bitmap bitmap, Uri u, ByteArrayOutputStream bitmapOutputStream) { + ByteArrayInputStream bis = null; + Uri newUri = null; + try { + bis = new ByteArrayInputStream(bitmapOutputStream.toByteArray()); + newUri = saveTemporaryImage(bitmap, u, bis); + } catch (IOException ex) {} finally { + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e); + } + } + } + return newUri; + } + + /** + * Apply our standard processing of the bitmap, returning a new one and + * recycling the old one in the process + * @param bitmap + * @param imageUri + * @return + */ + private Bitmap prepareBitmap(Bitmap bitmap, Uri imageUri) throws IOException { + if (settings.isShouldCorrectOrientation()) { + final Bitmap newBitmap = ImageUtils.correctOrientation(getContext(), bitmap, imageUri); + bitmap = replaceBitmap(bitmap, newBitmap); + } + + if (settings.isShouldResize()) { + final Bitmap newBitmap = ImageUtils.resize(bitmap, settings.getWidth(), settings.getHeight()); + bitmap = replaceBitmap(bitmap, newBitmap); + } + + return bitmap; + } + + private Bitmap replaceBitmap(Bitmap bitmap, final Bitmap newBitmap) { + if (bitmap != newBitmap) { + bitmap.recycle(); + } + bitmap = newBitmap; + return bitmap; + } + + private void returnDataUrl(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) { + byte[] byteArray = bitmapOutputStream.toByteArray(); + String encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP); + + JSObject data = new JSObject(); + data.put("format", "jpeg"); + data.put("dataUrl", "data:image/jpeg;base64," + encoded); + data.put("exif", exif.toJson()); + call.resolve(data); + } + + private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) { + byte[] byteArray = bitmapOutputStream.toByteArray(); + String encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP); + + JSObject data = new JSObject(); + data.put("format", "jpeg"); + data.put("base64String", encoded); + data.put("exif", exif.toJson()); + call.resolve(data); + } + + @Override + protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.handleRequestPermissionsResult(requestCode, permissions, grantResults); + + Logger.debug(getLogTag(), "handling request perms result"); + + if (getSavedCall() == null) { + Logger.debug(getLogTag(), "No stored plugin call for permissions request result"); + return; + } + + PluginCall savedCall = getSavedCall(); + + for (int i = 0; i < grantResults.length; i++) { + int result = grantResults[i]; + String perm = permissions[i]; + if (result == PackageManager.PERMISSION_DENIED) { + Logger.debug(getLogTag(), "User denied camera permission: " + perm); + savedCall.reject(PERMISSION_DENIED_ERROR); + return; + } + } + + if (requestCode == REQUEST_IMAGE_CAPTURE) { + doShow(savedCall); + } + } + + @Override + protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) { + super.handleOnActivityResult(requestCode, resultCode, data); + + PluginCall savedCall = getSavedCall(); + + if (savedCall == null) { + return; + } + + settings = getSettings(savedCall); + + if (requestCode == REQUEST_IMAGE_CAPTURE) { + processCameraImage(savedCall); + } else if (requestCode == REQUEST_IMAGE_PICK) { + processPickedImage(savedCall, data); + } else if (requestCode == REQUEST_IMAGE_EDIT && resultCode == Activity.RESULT_OK) { + isEdited = true; + processPickedImage(savedCall, data); + } else if (resultCode == Activity.RESULT_CANCELED && imageFileSavePath != null) { + imageEditedFileSavePath = null; + isEdited = true; + processCameraImage(savedCall); + } + } + + private void editImage(PluginCall call, Bitmap bitmap, Uri uri, ByteArrayOutputStream bitmapOutputStream) { + Uri origPhotoUri = uri; + if (imageFileUri != null) { + origPhotoUri = imageFileUri; + } + try { + Intent editIntent = createEditIntent(origPhotoUri, false); + startActivityForResult(call, editIntent, REQUEST_IMAGE_EDIT); + } catch (SecurityException ex) { + Uri tempImage = getTempImage(bitmap, uri, bitmapOutputStream); + Intent editIntent = createEditIntent(tempImage, true); + if (editIntent != null) { + startActivityForResult(call, editIntent, REQUEST_IMAGE_EDIT); + } else { + call.reject(IMAGE_EDIT_ERROR); + } + } catch (Exception ex) { + call.reject(IMAGE_EDIT_ERROR, ex); + } + } + + private Intent createEditIntent(Uri origPhotoUri, boolean expose) { + Uri editUri = origPhotoUri; + try { + if (expose) { + editUri = + FileProvider.getUriForFile( + getActivity(), + getContext().getPackageName() + ".fileprovider", + new File(origPhotoUri.getPath()) + ); + } + Intent editIntent = new Intent(Intent.ACTION_EDIT); + editIntent.setDataAndType(editUri, "image/*"); + File editedFile = CameraUtils.createImageFile(getActivity()); + imageEditedFileSavePath = editedFile.getAbsolutePath(); + Uri editedUri = Uri.fromFile(editedFile); + editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + editIntent.putExtra(MediaStore.EXTRA_OUTPUT, editedUri); + return editIntent; + } catch (Exception ex) { + return null; + } + } + + @Override + protected Bundle saveInstanceState() { + Bundle bundle = super.saveInstanceState(); + bundle.putString("cameraImageFileSavePath", imageFileSavePath); + return bundle; + } - JSObject ret = new JSObject(); - ret.put("value", implementation.echo(value)); - call.resolve(ret); + @Override + protected void restoreState(Bundle state) { + String storedImageFileSavePath = state.getString("cameraImageFileSavePath"); + if (storedImageFileSavePath != null) { + imageFileSavePath = storedImageFileSavePath; + } } } diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraResultType.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraResultType.java new file mode 100644 index 000000000..5d050590a --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraResultType.java @@ -0,0 +1,17 @@ +package com.capacitorjs.plugins.camera; + +public enum CameraResultType { + BASE64("base64"), + URI("uri"), + DATAURL("dataUrl"); + + private String type; + + CameraResultType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java new file mode 100644 index 000000000..a70e5d64b --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java @@ -0,0 +1,89 @@ +package com.capacitorjs.plugins.camera; + +public class CameraSettings { + public static final int DEFAULT_QUALITY = 90; + public static final boolean DEFAULT_SAVE_IMAGE_TO_GALLERY = false; + public static final boolean DEFAULT_CORRECT_ORIENTATION = true; + + private CameraResultType resultType = CameraResultType.BASE64; + private int quality = DEFAULT_QUALITY; + private boolean shouldResize = false; + private boolean shouldCorrectOrientation = DEFAULT_CORRECT_ORIENTATION; + private boolean saveToGallery = DEFAULT_SAVE_IMAGE_TO_GALLERY; + private boolean allowEditing = false; + private int width = 0; + private int height = 0; + private CameraSource source = CameraSource.prompt; + + public CameraResultType getResultType() { + return resultType; + } + + public void setResultType(CameraResultType resultType) { + this.resultType = resultType; + } + + public int getQuality() { + return quality; + } + + public void setQuality(int quality) { + this.quality = quality; + } + + public boolean isShouldResize() { + return shouldResize; + } + + public void setShouldResize(boolean shouldResize) { + this.shouldResize = shouldResize; + } + + public boolean isShouldCorrectOrientation() { + return shouldCorrectOrientation; + } + + public void setShouldCorrectOrientation(boolean shouldCorrectOrientation) { + this.shouldCorrectOrientation = shouldCorrectOrientation; + } + + public boolean isSaveToGallery() { + return saveToGallery; + } + + public void setSaveToGallery(boolean saveToGallery) { + this.saveToGallery = saveToGallery; + } + + public boolean isAllowEditing() { + return allowEditing; + } + + public void setAllowEditing(boolean allowEditing) { + this.allowEditing = allowEditing; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public CameraSource getSource() { + return source; + } + + public void setSource(CameraSource source) { + this.source = source; + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSource.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSource.java new file mode 100644 index 000000000..8e01e343e --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSource.java @@ -0,0 +1,17 @@ +package com.capacitorjs.plugins.camera; + +public enum CameraSource { + prompt("prompt"), + camera("camera"), + photos("photos"); + + private String source; + + CameraSource(String source) { + this.source = source; + } + + public String getSource() { + return this.source; + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraUtils.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraUtils.java new file mode 100644 index 000000000..d4d2ca069 --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraUtils.java @@ -0,0 +1,34 @@ +package com.capacitorjs.plugins.camera; + +import android.app.Activity; +import android.net.Uri; +import android.os.Environment; +import androidx.core.content.FileProvider; +import com.getcapacitor.Logger; +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class CameraUtils { + + public static Uri createImageFileUri(Activity activity, String appId) throws IOException { + File photoFile = CameraUtils.createImageFile(activity); + return FileProvider.getUriForFile(activity, appId + ".fileprovider", photoFile); + } + + public static File createImageFile(Activity activity) throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + + File image = File.createTempFile(imageFileName, /* prefix */".jpg", /* suffix */storageDir/* directory */); + + return image; + } + + protected static String getLogTag() { + return Logger.tags("CameraUtils"); + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java new file mode 100644 index 000000000..fcafa3be4 --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java @@ -0,0 +1,153 @@ +package com.capacitorjs.plugins.camera; + +import static androidx.exifinterface.media.ExifInterface.*; + +import androidx.exifinterface.media.ExifInterface; +import com.getcapacitor.JSObject; + +public class ExifWrapper { + private final ExifInterface exif; + + public ExifWrapper(ExifInterface exif) { + this.exif = exif; + } + + public JSObject toJson() { + JSObject ret = new JSObject(); + + if (this.exif == null) { + return ret; + } + + // Commented fields are for API 24. Left in to save someone the wrist damage later + + p(ret, TAG_APERTURE_VALUE); + /* + p(ret, TAG_ARTIST); + p(ret, TAG_BITS_PER_SAMPLE); + p(ret, TAG_BRIGHTNESS_VALUE); + p(ret, TAG_CFA_PATTERN); + p(ret, TAG_COLOR_SPACE); + p(ret, TAG_COMPONENTS_CONFIGURATION); + p(ret, TAG_COMPRESSED_BITS_PER_PIXEL); + p(ret, TAG_COMPRESSION); + p(ret, TAG_CONTRAST); + p(ret, TAG_COPYRIGHT); + */ + p(ret, TAG_DATETIME); + /* + p(ret, TAG_DATETIME_DIGITIZED); + p(ret, TAG_DATETIME_ORIGINAL); + p(ret, TAG_DEFAULT_CROP_SIZE); + p(ret, TAG_DEVICE_SETTING_DESCRIPTION); + p(ret, TAG_DIGITAL_ZOOM_RATIO); + p(ret, TAG_DNG_VERSION); + p(ret, TAG_EXIF_VERSION); + p(ret, TAG_EXPOSURE_BIAS_VALUE); + p(ret, TAG_EXPOSURE_INDEX); + p(ret, TAG_EXIF_VERSION); + p(ret, TAG_EXPOSURE_MODE); + p(ret, TAG_EXPOSURE_PROGRAM); + */ + p(ret, TAG_EXPOSURE_TIME); + // p(ret, TAG_F_NUMBER); + // p(ret, TAG_FILE_SOURCE); + p(ret, TAG_FLASH); + // p(ret, TAG_FLASH_ENERGY); + // p(ret, TAG_FLASHPIX_VERSION); + p(ret, TAG_FOCAL_LENGTH); + // p(ret, TAG_FOCAL_LENGTH_IN_35MM_FILM); + // p(ret, TAG_FOCAL_PLANE_RESOLUTION_UNIT); + p(ret, TAG_FOCAL_LENGTH); + // p(ret, TAG_GAIN_CONTROL); + p(ret, TAG_GPS_LATITUDE); + p(ret, TAG_GPS_LATITUDE_REF); + p(ret, TAG_GPS_LONGITUDE); + p(ret, TAG_GPS_LONGITUDE_REF); + p(ret, TAG_GPS_ALTITUDE); + p(ret, TAG_GPS_ALTITUDE_REF); + // p(ret, TAG_GPS_AREA_INFORMATION); + p(ret, TAG_GPS_DATESTAMP); + /* + API 24 + p(ret, TAG_GPS_DEST_BEARING); + p(ret, TAG_GPS_DEST_BEARING_REF); + p(ret, TAG_GPS_DEST_DISTANCE_REF); + p(ret, TAG_GPS_DEST_DISTANCE_REF); + p(ret, TAG_GPS_DEST_LATITUDE); + p(ret, TAG_GPS_DEST_LATITUDE_REF); + p(ret, TAG_GPS_DEST_LONGITUDE); + p(ret, TAG_GPS_DEST_LONGITUDE_REF); + p(ret, TAG_GPS_DIFFERENTIAL); + p(ret, TAG_GPS_DOP); + p(ret, TAG_GPS_IMG_DIRECTION); + p(ret, TAG_GPS_IMG_DIRECTION_REF); + p(ret, TAG_GPS_MAP_DATUM); + p(ret, TAG_GPS_MEASURE_MODE); + */ + p(ret, TAG_GPS_PROCESSING_METHOD); + /* + API 24 + p(ret, TAG_GPS_SATELLITES); + p(ret, TAG_GPS_SPEED); + p(ret, TAG_GPS_SPEED_REF); + p(ret, TAG_GPS_STATUS); + */ + p(ret, TAG_GPS_TIMESTAMP); + /* + API 24 + p(ret, TAG_GPS_TRACK); + p(ret, TAG_GPS_TRACK_REF); + p(ret, TAG_GPS_VERSION_ID); + p(ret, TAG_IMAGE_DESCRIPTION); + */ + p(ret, TAG_IMAGE_LENGTH); + // p(ret, TAG_IMAGE_UNIQUE_ID); + p(ret, TAG_IMAGE_WIDTH); + p(ret, TAG_ISO_SPEED); + /* + p(ret, TAG_INTEROPERABILITY_INDEX); + p(ret, TAG_ISO_SPEED_RATINGS); + p(ret, TAG_JPEG_INTERCHANGE_FORMAT); + p(ret, TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + p(ret, TAG_LIGHT_SOURCE); + */ + p(ret, TAG_MAKE); + /* + p(ret, TAG_MAKER_NOTE); + p(ret, TAG_MAX_APERTURE_VALUE); + p(ret, TAG_METERING_MODE); + */ + p(ret, TAG_MODEL); + /* + p(ret, TAG_NEW_SUBFILE_TYPE); + p(ret, TAG_OECF); + p(ret, TAG_ORF_ASPECT_FRAME); + p(ret, TAG_ORF_PREVIEW_IMAGE_LENGTH); + p(ret, TAG_ORF_PREVIEW_IMAGE_START); + */ + p(ret, TAG_ORIENTATION); + /* + p(ret, TAG_ORF_THUMBNAIL_IMAGE); + p(ret, TAG_PHOTOMETRIC_INTERPRETATION); + p(ret, TAG_PIXEL_X_DIMENSION); + p(ret, TAG_PIXEL_Y_DIMENSION); + p(ret, TAG_PLANAR_CONFIGURATION); + p(ret, TAG_PRIMARY_CHROMATICITIES); + p(ret, TAG_REFERENCE_BLACK_WHITE); + p(ret, TAG_RELATED_SOUND_FILE); + p(ret, TAG_RESOLUTION_UNIT); + p(ret, TAG_ROWS_PER_STRIP); + p(ret, TAG_RW2_ISO); + p(ret, TAG_RW2_JPG_FROM_RAW); + */ + p(ret, TAG_WHITE_BALANCE); + + return ret; + } + + public void p(JSObject o, String tag) { + String val = exif.getAttribute(tag); + o.put(tag, val); + } +} diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java new file mode 100644 index 000000000..e1fea7a9c --- /dev/null +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java @@ -0,0 +1,139 @@ +package com.capacitorjs.plugins.camera; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import androidx.exifinterface.media.ExifInterface; +import com.getcapacitor.FileUtils; +import com.getcapacitor.Logger; +import java.io.IOException; +import java.io.InputStream; + +public class ImageUtils { + + /** + * Resize an image to the given width and height considering the preserveAspectRatio flag. + * @param bitmap + * @param width + * @param height + * @return a new, scaled Bitmap + */ + public static Bitmap resize(Bitmap bitmap, final int width, final int height) { + return ImageUtils.resizePreservingAspectRatio(bitmap, width, height); + } + + /** + * Resize an image to the given max width and max height. Constraint can be put + * on one dimension, or both. Resize will always preserve aspect ratio. + * @param bitmap + * @param desiredMaxWidth + * @param desiredMaxHeight + * @return a new, scaled Bitmap + */ + private static Bitmap resizePreservingAspectRatio(Bitmap bitmap, final int desiredMaxWidth, final int desiredMaxHeight) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + // 0 is treated as 'no restriction' + int maxHeight = desiredMaxHeight == 0 ? height : desiredMaxHeight; + int maxWidth = desiredMaxWidth == 0 ? width : desiredMaxWidth; + + // resize with preserved aspect ratio + float newWidth = Math.min(width, maxWidth); + float newHeight = (height * newWidth) / width; + + if (newHeight > maxHeight) { + newWidth = (width * maxHeight) / height; + newHeight = maxHeight; + } + return Bitmap.createScaledBitmap(bitmap, Math.round(newWidth), Math.round(newHeight), false); + } + + /** + * Transform an image with the given matrix + * @param bitmap + * @param matrix + * @return + */ + private static Bitmap transform(final Bitmap bitmap, final Matrix matrix) { + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + /** + * Correct the orientation of an image by reading its exif information and rotating + * the appropriate amount for portrait mode + * @param bitmap + * @param imageUri + * @return + */ + public static Bitmap correctOrientation(final Context c, final Bitmap bitmap, final Uri imageUri) throws IOException { + if (Build.VERSION.SDK_INT < 24) { + return correctOrientationOlder(c, bitmap, imageUri); + } else { + final int orientation = getOrientation(c, imageUri); + + if (orientation != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(orientation); + + return transform(bitmap, matrix); + } else { + return bitmap; + } + } + } + + private static Bitmap correctOrientationOlder(final Context c, final Bitmap bitmap, final Uri imageUri) { + // TODO: To be tested on older phone using Android API < 24 + + String[] orientationColumn = { MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION }; + Cursor cur = c.getContentResolver().query(imageUri, orientationColumn, null, null, null); + int orientation = -1; + if (cur != null && cur.moveToFirst()) { + orientation = cur.getInt(cur.getColumnIndex(orientationColumn[0])); + } + Matrix matrix = new Matrix(); + + if (orientation != -1) { + matrix.postRotate(orientation); + } + + return transform(bitmap, matrix); + } + + private static int getOrientation(final Context c, final Uri imageUri) throws IOException { + int result = 0; + + try (InputStream iStream = c.getContentResolver().openInputStream(imageUri)) { + final ExifInterface exifInterface = new ExifInterface(iStream); + + final int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + + if (orientation == ExifInterface.ORIENTATION_ROTATE_90) { + result = 90; + } else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) { + result = 180; + } else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) { + result = 270; + } + } + + return result; + } + + public static ExifWrapper getExifData(final Context c, final Bitmap bitmap, final Uri imageUri) { + try { + String fu = FileUtils.getFileUrlForUri(c, imageUri); + final ExifInterface exifInterface = new ExifInterface(fu); + + return new ExifWrapper(exifInterface); + } catch (IOException ex) { + Logger.error("Error loading exif data from image", ex); + } finally {} + return new ExifWrapper(null); + } +} diff --git a/camera/ios/Plugin.xcodeproj/project.pbxproj b/camera/ios/Plugin.xcodeproj/project.pbxproj index a5d0abfa2..fccfd5fcc 100644 --- a/camera/ios/Plugin.xcodeproj/project.pbxproj +++ b/camera/ios/Plugin.xcodeproj/project.pbxproj @@ -9,13 +9,14 @@ /* Begin PBXBuildFile section */ 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; - 2F98D68224C9AAE500613A4C /* Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F98D68124C9AAE400613A4C /* Camera.swift */; }; 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; 50ADFF97201F53D600D50D53 /* CameraPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* CameraPluginTests.swift */; }; 50ADFF99201F53D600D50D53 /* CameraPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; }; 50ADFFA82020EE4F00D50D53 /* CameraPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */; }; 50E1A94820377CB70090CE1A /* CameraPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* CameraPlugin.swift */; }; + 6276AAD3255B3E0E00097815 /* CameraExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6276AAD2255B3E0E00097815 /* CameraExtensions.swift */; }; + 6276AAD7255B3E1400097815 /* CameraTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6276AAD6255B3E1400097815 /* CameraTypes.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -29,7 +30,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 2F98D68124C9AAE400613A4C /* Camera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Camera.swift; sourceTree = ""; }; 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraPlugin.h; sourceTree = ""; }; @@ -41,6 +41,8 @@ 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPlugin.m; sourceTree = ""; }; 50E1A94720377CB70090CE1A /* CameraPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPlugin.swift; sourceTree = ""; }; 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = ""; }; + 6276AAD2255B3E0E00097815 /* CameraExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraExtensions.swift; sourceTree = ""; }; + 6276AAD6255B3E1400097815 /* CameraTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraTypes.swift; sourceTree = ""; }; 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; @@ -92,10 +94,11 @@ 50ADFF8A201F53D600D50D53 /* Plugin */ = { isa = PBXGroup; children = ( - 50E1A94720377CB70090CE1A /* CameraPlugin.swift */, - 2F98D68124C9AAE400613A4C /* Camera.swift */, 50ADFF8B201F53D600D50D53 /* CameraPlugin.h */, 50ADFFA72020EE4F00D50D53 /* CameraPlugin.m */, + 50E1A94720377CB70090CE1A /* CameraPlugin.swift */, + 6276AAD2255B3E0E00097815 /* CameraExtensions.swift */, + 6276AAD6255B3E1400097815 /* CameraTypes.swift */, 50ADFF8C201F53D600D50D53 /* Info.plist */, ); path = Plugin; @@ -307,8 +310,9 @@ buildActionMask = 2147483647; files = ( 50E1A94820377CB70090CE1A /* CameraPlugin.swift in Sources */, - 2F98D68224C9AAE500613A4C /* Camera.swift in Sources */, 50ADFFA82020EE4F00D50D53 /* CameraPlugin.m in Sources */, + 6276AAD7255B3E1400097815 /* CameraTypes.swift in Sources */, + 6276AAD3255B3E0E00097815 /* CameraExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/camera/ios/Plugin/Camera.swift b/camera/ios/Plugin/Camera.swift deleted file mode 100644 index 2b68b82be..000000000 --- a/camera/ios/Plugin/Camera.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -@objc public class Camera: NSObject { - @objc public func echo(_ value: String) -> String { - return value - } -} diff --git a/camera/ios/Plugin/CameraExtensions.swift b/camera/ios/Plugin/CameraExtensions.swift new file mode 100644 index 000000000..873ccb33f --- /dev/null +++ b/camera/ios/Plugin/CameraExtensions.swift @@ -0,0 +1,105 @@ +import UIKit +import Photos + +internal protocol CameraAuthorizationState { + var authorizationState: String { get } +} + +extension AVAuthorizationStatus: CameraAuthorizationState { + var authorizationState: String { + switch self { + case .denied, .restricted: + return "denied" + case .authorized: + return "granted" + case .notDetermined: + fallthrough + @unknown default: + return "prompt" + } + } +} + +extension PHAuthorizationStatus: CameraAuthorizationState { + var authorizationState: String { + switch self { + case .denied, .restricted: + return "denied" + case .authorized: + return "granted" + #if swift(>=5.3) + // poor proxy for Xcode 12/iOS 14, should be removed once building with Xcode 12 is required + case .limited: + return "limited" + #endif + case .notDetermined: + fallthrough + @unknown default: + return "prompt" + } + } +} + +internal extension PHAsset { + /** + Retrieves the image metadata for the asset. + */ + var imageData: [String: Any] { + let options = PHImageRequestOptions() + options.isSynchronous = true + options.resizeMode = .none + options.isNetworkAccessAllowed = false + options.version = .current + + var result: [String: Any] = [:] + _ = PHCachingImageManager().requestImageData(for: self, options: options) { (data, _, _, _) in + if let data = data as NSData? { + let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse] as CFDictionary + if let imgSrc = CGImageSourceCreateWithData(data, options), + let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options) as? [String: Any] { + result = metadata + } + } + } + return result + } +} + +internal extension UIImage { + /** + Generates a new image from the existing one, implicitly resetting any orientation. + Dimensions greater than 0 will resize the image while preserving the aspect ratio. + */ + func reformat(to size: CGSize? = nil) -> UIImage { + let imageHeight = self.size.height + let imageWidth = self.size.width + // determine the max dimensions, 0 is treated as 'no restriction' + var maxWidth: CGFloat + if let size = size, size.width > 0 { + maxWidth = size.width + } else { + maxWidth = imageWidth + } + let maxHeight: CGFloat + if let size = size, size.height > 0 { + maxHeight = size.height + } else { + maxHeight = imageHeight + } + // adjust to preserve aspect ratio + var targetWidth = min(imageWidth, maxWidth) + var targetHeight = (imageHeight * targetWidth) / imageWidth + if targetHeight > maxHeight { + targetWidth = (imageWidth * targetHeight) / imageHeight + targetHeight = maxHeight + } + // generate the new image and return + let format: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat.default() + format.scale = 1.0 + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: CGSize(width: targetWidth, height: targetHeight), format: format) + return renderer.image { (_) in + self.draw(in: CGRect(origin: .zero, size: CGSize(width: targetWidth, height: targetHeight))) + } + } +} diff --git a/camera/ios/Plugin/CameraPlugin.m b/camera/ios/Plugin/CameraPlugin.m index af8e14a12..4d0f9dcf0 100644 --- a/camera/ios/Plugin/CameraPlugin.m +++ b/camera/ios/Plugin/CameraPlugin.m @@ -1,8 +1,6 @@ #import #import -// Define the plugin using the CAP_PLUGIN Macro, and -// each method the plugin supports using the CAP_PLUGIN_METHOD macro. -CAP_PLUGIN(CameraPlugin, "Camera", - CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise); +CAP_PLUGIN(CAPCameraPlugin, "Camera", + CAP_PLUGIN_METHOD(getPhoto, CAPPluginReturnPromise); ) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 75947dc4d..fa3d3c355 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -1,18 +1,332 @@ import Foundation import Capacitor +import Photos -/** - * Please read the Capacitor iOS Plugin Development Guide - * here: https://capacitorjs.com/docs/plugins/ios - */ -@objc(CameraPlugin) +@objc(CAPCameraPlugin) public class CameraPlugin: CAPPlugin { - private let implementation = Camera() + private var call: CAPPluginCall? + private var settings = CameraSettings() + private var imagePicker: UIImagePickerController? - @objc func echo(_ call: CAPPluginCall) { - let value = call.getString("value") ?? "" - call.resolve([ - "value": implementation.echo(value) - ]) + private let defaultSource = CameraSource.prompt + private let defaultDirection = CameraDirection.rear + + private var imageCounter = 0 + + @objc func checkPermissions(_ call: CAPPluginCall) { + var result: [String: Any] = [:] + for permission in CameraPermissionType.allCases { + let state: String + switch permission { + case .camera: + state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState + case .photos: + state = PHPhotoLibrary.authorizationStatus().authorizationState + } + result[permission.rawValue] = state + } + call.resolve(result) + } + + @objc func requestPermissions(_ call: CAPPluginCall) { + // get the list of desired types, if passed + let typeList = call.getArray("types", String.self)?.compactMap({ (type) -> CameraPermissionType? in + return CameraPermissionType(rawValue: type) + }) ?? [] + // otherwise check everything + let permissions: [CameraPermissionType] = (typeList.count > 0) ? typeList : CameraPermissionType.allCases + // request the permissions + var result: [String: Any] = [:] + let group = DispatchGroup() + for permission in permissions { + switch permission { + case .camera: + group.enter() + AVCaptureDevice.requestAccess(for: .video) { granted in + result[permission.rawValue] = granted ? + AVAuthorizationStatus.authorized.authorizationState : + AVAuthorizationStatus.denied.authorizationState + group.leave() + } + case .photos: + group.enter() + PHPhotoLibrary.requestAuthorization({ (status) in + result[permission.rawValue] = status.authorizationState + group.leave() + }) + } + } + group.notify(queue: DispatchQueue.main) { + call.resolve(result) + } + } + + @objc func getPhoto(_ call: CAPPluginCall) { + self.call = call + self.settings = cameraSettings(from: call) + + // Make sure they have all the necessary info.plist settings + if let missingUsageDescription = checkUsageDescriptions() { + bridge?.modulePrint(self, missingUsageDescription) + call.reject(missingUsageDescription) + bridge?.alert("Camera Error", "Missing required usage description. See console for more information") + return + } + + DispatchQueue.main.async { + self.imagePicker = UIImagePickerController() + self.imagePicker?.delegate = self + self.imagePicker?.allowsEditing = self.settings.allowEditing + + switch self.settings.source { + case .prompt: + self.showPrompt() + case .camera: + self.showCamera() + case .photos: + self.showPhotos() + } + } + } + + private func checkUsageDescriptions() -> String? { + if let dict = Bundle.main.infoDictionary { + for key in CameraPropertyListKeys.allCases where dict[key.rawValue] == nil { + return key.missingMessage + } + } + return nil + } + + private func cameraSettings(from call: CAPPluginCall) -> CameraSettings { + var settings = CameraSettings() + settings.jpegQuality = min(abs(CGFloat(call.getFloat("quality") ?? 100.0)) / 100.0, 1.0) + settings.allowEditing = call.getBool("allowEditing") ?? false + settings.source = CameraSource(rawValue: call.getString("source") ?? defaultSource.rawValue) ?? defaultSource + settings.direction = CameraDirection(rawValue: call.getString("direction") ?? defaultDirection.rawValue) ?? defaultDirection + if let typeString = call.getString("resultType"), let type = CameraResultType(rawValue: typeString) { + settings.resultType = type + } + settings.saveToGallery = call.getBool("saveToGallery") ?? false + + // Get the new image dimensions if provided + settings.width = CGFloat(call.getInt("width") ?? 0) + settings.height = CGFloat(call.getInt("height") ?? 0) + if settings.width > 0 || settings.height > 0 { + // We resize only if a dimension was provided + settings.shouldResize = true + } + settings.shouldCorrectOrientation = call.getBool("correctOrientation") ?? true + settings.userPromptText = CameraPromptText(title: call.getString("promptLabelHeader"), + photoAction: call.getString("promptLabelPhoto"), + cameraAction: call.getString("promptLabelPicture"), + cancelAction: call.getString("promptLabelCancel")) + if let styleString = call.getString("presentationStyle"), styleString == "popover" { + settings.presentationStyle = .popover + } else { + settings.presentationStyle = .fullScreen + } + + return settings + } +} + +// public delegate methods +extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate { + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + self.call?.reject("User cancelled photos app") + } + + public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { + self.call?.reject("User cancelled photos app") + } + + public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.call?.reject("User cancelled photos app") + } + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + + guard let processedImage = self.processImage(from: info) else { + self.call?.reject("Error processing image") + return + } + + guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { + self.call?.reject("Unable to convert image to jpeg") + return + } + + if settings.resultType == CameraResultType.base64 { + call?.resolve([ + "base64String": jpeg.base64EncodedString(), + "exif": processedImage.exifData, + "format": "jpeg" + ]) + } else if settings.resultType == CameraResultType.dataURL { + call?.resolve([ + "dataUrl": "data:image/jpeg;base64," + jpeg.base64EncodedString(), + "exif": processedImage.exifData, + "format": "jpeg" + ]) + } else if settings.resultType == CameraResultType.uri { + guard let path = try? saveTemporaryImage(jpeg), + let webPath = CAPFileManager.getPortablePath(host: bridge?.getLocalUrl() ?? "", uri: URL(string: path)) else { + call?.reject("Unable to get portable path to file") + return + } + call?.resolve([ + "path": path, + "exif": processedImage.exifData, + "webPath": webPath, + "format": "jpeg" + ]) + } + + picker.dismiss(animated: true, completion: nil) + } +} + +private extension CameraPlugin { + func showPrompt() { + // Build the action sheet + let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet) + alert.addAction(UIAlertAction(title: settings.userPromptText.photoAction, style: .default, handler: { [weak self] (_: UIAlertAction) in + self?.showPhotos() + })) + + alert.addAction(UIAlertAction(title: settings.userPromptText.cameraAction, style: .default, handler: { [weak self] (_: UIAlertAction) in + self?.showCamera() + })) + + alert.addAction(UIAlertAction(title: settings.userPromptText.cancelAction, style: .cancel, handler: { [weak self] (_: UIAlertAction) in + self?.call?.reject("User cancelled photos app") + })) + + self.bridge?.viewController?.present(alert, animated: true, completion: nil) + } + + func showCamera() { + // check if we have a camera + if (bridge?.isSimulator() ?? false) || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) { + bridge?.modulePrint(self, "Camera not available in simulator") + bridge?.alert("Camera Error", "Camera not available in Simulator") + call?.reject("Camera not available while running in Simulator") + return + } + // check for permission + let authStatus = AVCaptureDevice.authorizationStatus(for: .video) + if authStatus == .restricted || authStatus == .denied { + call?.reject("User denied access to camera") + return + } + // we either already have permission or can prompt + AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + if granted { + DispatchQueue.main.async { + guard let imagePicker = self?.imagePicker, let settings = self?.settings else { + return + } + // select the input + if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) { + imagePicker.cameraDevice = .rear + } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) { + imagePicker.cameraDevice = .front + } + // present + imagePicker.modalPresentationStyle = settings.presentationStyle + imagePicker.sourceType = .camera + + self?.bridge?.viewController?.present(imagePicker, animated: true, completion: nil) + } + } else { + self?.call?.reject("User denied access to camera") + } + } + } + + func showPhotos() { + // check for permission + let authStatus = PHPhotoLibrary.authorizationStatus() + if authStatus == .restricted || authStatus == .denied { + call?.reject("User denied access to photos") + return + } + // we either already have permission or can prompt + if authStatus == .authorized { + presentPhotoPicker() + } else { + PHPhotoLibrary.requestAuthorization({ [weak self] (status) in + if status == PHAuthorizationStatus.authorized { + DispatchQueue.main.async { [weak self] in + self?.presentPhotoPicker() + } + } else { + self?.call?.reject("User denied access to photos") + } + }) + } + } + + func presentPhotoPicker() { + guard let imagePicker = imagePicker else { + return + } + if settings.presentationStyle == .popover { + imagePicker.modalPresentationStyle = .popover + imagePicker.popoverPresentationController?.delegate = self + setCenteredPopover(imagePicker) + } + imagePicker.sourceType = .photoLibrary + self.bridge?.viewController?.present(imagePicker, animated: true, completion: nil) + } + + func saveTemporaryImage(_ data: Data) throws -> String { + var url: URL + repeat { + imageCounter += 1 + url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("photo-\(imageCounter).jpg") + } while FileManager.default.fileExists(atPath: url.absoluteString) + + try data.write(to: url, options: .atomic) + return url.absoluteString + } + + func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? { + // get the image + var result: ProcessedImage + var flags: PhotoFlags = [] + if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { + result = ProcessedImage(image: image, metadata: [:]) // use the edited version + flags = flags.union([.edited]) + } else if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + result = ProcessedImage(image: image, metadata: [:]) // use the original version + } else { + return nil + } + // get the image's metadata from the picker or from the photo album + if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { + result.metadata = photoMetadata + flags = flags.union([.gallery]) + } + if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset { + result.metadata = asset.imageData + } + // resizing the image only makes sense if we have real values to which to constrain it + if settings.shouldResize, settings.width > 0 || settings.height > 0 { + result.image = result.image.reformat(to: CGSize(width: settings.width, height: settings.height)) + result.overwriteMetadataOrientation(to: 1) + } else if settings.shouldCorrectOrientation { + // resizing implicitly reformats the image so this is only needed if we aren't resizing + result.image = result.image.reformat() + result.overwriteMetadataOrientation(to: 1) + } + // conditionally save the image + if settings.saveToGallery, flags.contains(.edited) == true, flags.contains(.gallery) == false { + UIImageWriteToSavedPhotosAlbum(result.image, nil, nil, nil) + } + + return result } } diff --git a/camera/ios/Plugin/CameraTypes.swift b/camera/ios/Plugin/CameraTypes.swift new file mode 100644 index 000000000..22425a3c0 --- /dev/null +++ b/camera/ios/Plugin/CameraTypes.swift @@ -0,0 +1,148 @@ +import UIKit + +// MARK: - Public + +public enum CameraSource: String { + case prompt + case camera + case photos +} + +public enum CameraDirection: String { + case rear + case front +} + +public enum CameraResultType: String { + case base64 + case uri + case dataURL = "dataUrl" +} + +struct CameraPromptText { + let title: String + let photoAction: String + let cameraAction: String + let cancelAction: String + + init(title: String? = nil, photoAction: String? = nil, cameraAction: String? = nil, cancelAction: String? = nil) { + self.title = title ?? "Photo" + self.photoAction = photoAction ?? "From Photos" + self.cameraAction = cameraAction ?? "Take Picture" + self.cancelAction = cancelAction ?? "Cancel" + } +} + +public struct CameraSettings { + var source: CameraSource = CameraSource.prompt + var direction: CameraDirection = CameraDirection.rear + var resultType = CameraResultType.base64 + var userPromptText = CameraPromptText() + var jpegQuality: CGFloat = 1.0 + var width: CGFloat = 0 + var height: CGFloat = 0 + var allowEditing = false + var shouldResize = false + var shouldCorrectOrientation = true + var saveToGallery = false + var presentationStyle = UIModalPresentationStyle.fullScreen +} + +public struct CameraResult { + let image: UIImage? + let metadata: [AnyHashable: Any] +} + +// MARK: - Internal + +internal enum CameraPermissionType: String, CaseIterable { + case camera + case photos +} + +internal enum CameraPropertyListKeys: String, CaseIterable { + case photoLibraryAddUsage = "NSPhotoLibraryAddUsageDescription" + case photoLibraryUsage = "NSPhotoLibraryUsageDescription" + case cameraUsage = "NSCameraUsageDescription" + + var link: String { + switch self { + case .photoLibraryAddUsage: + return "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW73" + case .photoLibraryUsage: + return "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW17" + case .cameraUsage: + return "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW24" + } + } + + var missingMessage: String { + return "You are missing \(self.rawValue) in your Info.plist file." + + " Camera will not function without it. Learn more: \(self.link)" + } +} + +internal struct PhotoFlags: OptionSet { + let rawValue: Int + + static let edited = PhotoFlags(rawValue: 1 << 0) + static let gallery = PhotoFlags(rawValue: 1 << 1) + + static let all: PhotoFlags = [.edited, .gallery] +} + +internal struct ProcessedImage { + var image: UIImage + var metadata: [String: Any] + + var exifData: [String: Any] { + var exifData = metadata["{Exif}"] as? [String: Any] + exifData?["Orientation"] = metadata["Orientation"] + exifData?["GPS"] = metadata["{GPS}"] + return exifData ?? [:] + } + + mutating func overwriteMetadataOrientation(to orientation: Int) { + let original = metadata + replaceDictionaryOrientation(atNode: &metadata, to: orientation) + + if (original as NSDictionary).isEqual(to: metadata) { + print("dictionary is unchanged") + } else { + print("dictionaries no longer match") + } + } + + func replaceDictionaryOrientation(atNode node: inout [String: Any], to orientation: Int) { + for key in node.keys { + if key == "Orientation", (node[key] as? Int) != nil { + node[key] = orientation + } else if var child = node[key] as? [String: Any] { + replaceDictionaryOrientation(atNode: &child, to: orientation) + node[key] = child + } + } + } + + func generateJPEG(with quality: CGFloat) -> Data? { + // convert the UIImage to a jpeg + guard let data = self.image.jpegData(compressionQuality: quality) else { + return nil + } + // define our jpeg data as an image source and get its type + guard let source = CGImageSourceCreateWithData(data as CFData, nil), let type = CGImageSourceGetType(source) else { + return data + } + // allocate an output buffer and create the destination to receive the new data + guard let output = NSMutableData(capacity: data.count), let destination = CGImageDestinationCreateWithData(output, type, 1, nil) else { + return data + } + // pipe the source into the destination while overwriting the metadata, this encodes the metadata information into the image + CGImageDestinationAddImageFromSource(destination, source, 0, self.metadata as CFDictionary) + // finish + guard CGImageDestinationFinalize(destination) else { + return data + } + return output as Data + } +} diff --git a/camera/ios/PluginTests/CameraPluginTests.swift b/camera/ios/PluginTests/CameraPluginTests.swift index bdde46a9a..9e3487a04 100644 --- a/camera/ios/PluginTests/CameraPluginTests.swift +++ b/camera/ios/PluginTests/CameraPluginTests.swift @@ -11,15 +11,4 @@ class CameraTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } - - func testEcho() { - // This is an example of a functional test case for a plugin. - // Use XCTAssert and related functions to verify your tests produce the correct results. - - let implementation = Camera() - let value = "Hello, World!" - let result = implementation.echo(value) - - XCTAssertEqual(value, result) - } } diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index 0cf10c9d0..65ed7e178 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -1,3 +1,5 @@ +import {} from '@capacitor/core'; + declare module '@capacitor/core' { interface PluginRegistry { Camera: CameraPlugin; @@ -5,5 +7,132 @@ declare module '@capacitor/core' { } export interface CameraPlugin { - echo(options: { value: string }): Promise<{ value: string }>; + /** + * Prompt the user to pick a photo from an album, or take a new photo + * with the camera. + */ + getPhoto(options: CameraOptions): Promise; +} + +export interface CameraOptions { + /** + * The quality of image to return as JPEG, from 0-100 + */ + quality?: number; + /** + * Whether to allow the user to crop or make small edits (platform specific) + */ + allowEditing?: boolean; + /** + * How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported + */ + resultType: CameraResultType; + /** + * Whether to save the photo to the gallery. + * If the photo was picked from the gallery, it will only be saved if edited. + * Default: false + */ + saveToGallery?: boolean; + /** + * The width of the saved image + */ + width?: number; + /** + * The height of the saved image + */ + height?: number; + /** + * Whether to preserve the aspect ratio of the image. + * If this flag is true, the width and height will be used as max values + * and the aspect ratio will be preserved. This is only relevant when + * both a width and height are passed. When only width or height is provided + * the aspect ratio is always preserved (and this option is a no-op). + * + * A future major version will change this behavior to be default, + * and may also remove this option altogether. + * Default: false + */ + preserveAspectRatio?: boolean; + /** + * Whether to automatically rotate the image "up" to correct for orientation + * in portrait mode + * Default: true + */ + correctOrientation?: boolean; + /** + * The source to get the photo from. By default this prompts the user to select + * either the photo album or take a photo. + * Default: CameraSource.Prompt + */ + source?: CameraSource; + /** + * iOS and Web only: The camera direction. + * Default: CameraDirection.Rear + */ + direction?: CameraDirection; + + /** + * iOS only: The presentation style of the Camera. Defaults to fullscreen. + */ + presentationStyle?: 'fullscreen' | 'popover'; + + /** + * Web only: Whether to use the PWA Element experience or file input. The + * default is to use PWA Elements if installed and fall back to file input. + * To always use file input, set this to `true`. + * + * Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements + */ + webUseInput?: boolean; + + /** + * If use CameraSource.Prompt only, can change Prompt label. + * default: + * promptLabelHeader : 'Photo' // iOS only + * promptLabelCancel : 'Cancel' // iOS only + * promptLabelPhoto : 'From Photos' + * promptLabelPicture : 'Take Picture' + */ + promptLabelHeader?: string; + promptLabelCancel?: string; + promptLabelPhoto?: string; + promptLabelPicture?: string; } + +export interface CameraPhoto { + /** + * The base64 encoded string representation of the image, if using CameraResultType.Base64. + */ + base64String?: string; + /** + * The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. + */ + dataUrl?: string; + /** + * If using CameraResultType.Uri, the path will contain a full, + * platform-specific file URL that can be read later using the Filsystem API. + */ + path?: string; + /** + * webPath returns a path that can be used to set the src attribute of an image for efficient + * loading and rendering. + */ + webPath?: string; + /** + * Exif data, if any, retrieved from the image + */ + exif?: any; + /** + * The format of the image, ex: jpeg, png, gif. + * + * iOS and Android only support jpeg. + * Web supports jpeg and png. gif is only supported if using file input. + */ + format: string; +} + +export type CameraSource = 'prompt' | 'camera' | 'photos'; + +export type CameraDirection = 'rear' | 'front'; + +export type CameraResultType = 'uri' | 'base64' | 'dataUrl'; diff --git a/camera/src/index.ts b/camera/src/index.ts index 136a5fb79..a937ca091 100644 --- a/camera/src/index.ts +++ b/camera/src/index.ts @@ -2,6 +2,13 @@ import type { PluginImplementations } from '@capacitor/core'; import { Plugins, registerPlugin } from '@capacitor/core'; import type { CameraPlugin } from './definitions'; +import { + CameraOptions, + CameraDirection, + CameraPhoto, + CameraResultType, + CameraSource, +} from './definitions'; import { CameraWeb } from './web'; const implementations: PluginImplementations = { @@ -12,4 +19,11 @@ const implementations: PluginImplementations = { const Camera = registerPlugin('Camera', implementations).getImplementation(); -export { Camera }; +export { + Camera, + CameraOptions, + CameraDirection, + CameraPhoto, + CameraResultType, + CameraSource, +}; diff --git a/camera/src/web.ts b/camera/src/web.ts index 02c5824df..0af3b98dc 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -1,14 +1,155 @@ import { WebPlugin } from '@capacitor/core'; -import type { CameraPlugin } from './definitions'; +import type { CameraPlugin, CameraPhoto, CameraOptions } from './definitions'; export class CameraWeb extends WebPlugin implements CameraPlugin { constructor() { super({ name: 'Camera' }); } - async echo(options: { value: string }): Promise<{ value: string }> { - console.log('ECHO', options); - return options; + async getPhoto(options: CameraOptions): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + if (options.webUseInput) { + this.fileInputExperience(options, resolve); + } else { + if (customElements.get('pwa-camera-modal')) { + const cameraModal: any = document.createElement('pwa-camera-modal'); + document.body.appendChild(cameraModal); + try { + await cameraModal.componentOnReady(); + cameraModal.addEventListener('onPhoto', async (e: any) => { + const photo = e.detail; + + if (photo === null) { + reject('User cancelled photos app'); + } else if (photo instanceof Error) { + reject(photo.message); + } else { + resolve(await this._getCameraPhoto(photo, options)); + } + + cameraModal.dismiss(); + document.body.removeChild(cameraModal); + }); + + cameraModal.present(); + } catch (e) { + this.fileInputExperience(options, resolve); + } + } else { + console.error( + `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/pwa-elements.`, + ); + this.fileInputExperience(options, resolve); + } + } + }); + } + + private fileInputExperience(options: CameraOptions, resolve: any) { + let input = document.querySelector( + '#_capacitor-camera-input', + ) as HTMLInputElement; + + const cleanup = () => { + input.parentNode?.removeChild(input); + }; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = '_capacitor-camera-input'; + input.type = 'file'; + document.body.appendChild(input); + } + + input.accept = 'image/*'; + (input as any).capture = true; + + if (options.source === 'photos' || options.source === 'prompt') { + input.removeAttribute('capture'); + } else if (options.direction === 'front') { + (input as any).capture = 'user'; + } else if (options.direction === 'rear') { + (input as any).capture = 'environment'; + } + + input.addEventListener('change', (_e: any) => { + const file = input.files![0]; + let format = 'jpeg'; + + if (file.type === 'image/png') { + format = 'png'; + } else if (file.type === 'image/gif') { + format = 'gif'; + } + + if (options.resultType === 'dataUrl' || options.resultType === 'base64') { + const reader = new FileReader(); + + reader.addEventListener('load', () => { + if (options.resultType === 'dataUrl') { + resolve({ + dataUrl: reader.result, + format, + } as CameraPhoto); + } else if (options.resultType === 'base64') { + const b64 = (reader.result as string).split(',')[1]; + resolve({ + base64String: b64, + format, + } as CameraPhoto); + } + + cleanup(); + }); + + reader.readAsDataURL(file); + } else { + resolve({ + webPath: URL.createObjectURL(file), + format: format, + }); + cleanup(); + } + }); + + input.click(); + } + + private _getCameraPhoto(photo: Blob, options: CameraOptions) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + const format = photo.type.split('/')[1]; + if (options.resultType === 'uri') { + resolve({ + webPath: URL.createObjectURL(photo), + format: format, + }); + } else { + reader.readAsDataURL(photo); + reader.onloadend = () => { + const r = reader.result as string; + if (options.resultType === 'dataUrl') { + resolve({ + dataUrl: r, + format: format, + }); + } else { + resolve({ + base64String: r.split(',')[1], + format: format, + }); + } + }; + reader.onerror = e => { + reject(e); + }; + } + }); } } + +const Camera = new CameraWeb(); + +export { Camera }; From a6b4e12b22ba65cd9bdcd07ffb9a0cb86ab7b03b Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 10 Nov 2020 19:14:25 -0500 Subject: [PATCH 04/58] Updating gradle file, docgen result --- camera/README.md | 65 +++++++++++++++++++++++++++++++++++-- camera/android/build.gradle | 5 +++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/camera/README.md b/camera/README.md index ba7543804..cabd7140e 100644 --- a/camera/README.md +++ b/camera/README.md @@ -11,9 +11,68 @@ npx cap sync ## API - + + +* [`getPhoto(...)`](#getphoto) +* [Interfaces](#interfaces) + + - - + + +### getPhoto(...) + +```typescript +getPhoto(options: CameraOptions) => Promise +``` + +Prompt the user to pick a photo from an album, or take a new photo +with the camera. + +| Param | Type | +| ------------- | ------------------------------------------------------- | +| **`options`** | CameraOptions | + +**Returns:** Promise<CameraPhoto> + +-------------------- + + +### Interfaces + + +#### CameraPhoto + +| Prop | Type | Description | +| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | +| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | +| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | +| **`exif`** | any | Exif data, if any, retrieved from the image | +| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | + + +#### CameraOptions + +| Prop | Type | Description | +| ------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`quality`** | number | The quality of image to return as JPEG, from 0-100 | +| **`allowEditing`** | boolean | Whether to allow the user to crop or make small edits (platform specific) | +| **`resultType`** | "uri" \| "base64" \| "dataUrl" | How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported | +| **`saveToGallery`** | boolean | Whether to save the photo to the gallery. If the photo was picked from the gallery, it will only be saved if edited. Default: false | +| **`width`** | number | The width of the saved image | +| **`height`** | number | The height of the saved image | +| **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. Default: false | +| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode Default: true | +| **`source`** | "prompt" \| "camera" \| "photos" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. Default: CameraSource.Prompt | +| **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. Default: CameraDirection.Rear | +| **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. Defaults to fullscreen. | +| **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | +| **`promptLabelHeader`** | string | If use CameraSource.Prompt only, can change Prompt label. default: promptLabelHeader : 'Photo' // iOS only promptLabelCancel : 'Cancel' // iOS only promptLabelPhoto : 'From Photos' promptLabelPicture : 'Take Picture' | +| **`promptLabelCancel`** | string | | +| **`promptLabelPhoto`** | string | | +| **`promptLabelPicture`** | string | | + diff --git a/camera/android/build.gradle b/camera/android/build.gradle index 034dc47f7..dc9bef47a 100644 --- a/camera/android/build.gradle +++ b/camera/android/build.gradle @@ -2,6 +2,7 @@ ext { junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' + androidxMaterialVersion = project.hasProperty('androidxMaterialVersion') ? rootProject.ext.androidxMaterialVersion : '1.1.0-rc02' } buildscript { @@ -50,6 +51,10 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':capacitor-android') + implementation 'androidx.exifinterface:exifinterface:1.2.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' + implementation "com.google.android.material:material:$androidxMaterialVersion" testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" From 2f6ed228fa5f0f7cfea19feeb026268dbd77207a Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 17 Nov 2020 15:37:33 -0500 Subject: [PATCH 05/58] Migrated to new permission annotation on android. --- .../plugins/camera/CameraPlugin.java | 88 +++++++++++++++---- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index aebcf504d..a59630340 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -3,6 +3,7 @@ import android.Manifest; import android.app.Activity; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -10,6 +11,8 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; + +import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import com.getcapacitor.FileUtils; import com.getcapacitor.JSObject; @@ -19,6 +22,8 @@ import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; import com.getcapacitor.PluginRequestCodes; +import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.annotation.Permission; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -37,12 +42,19 @@ * * Adapted from https://developer.android.com/training/camera/photobasics.html */ -@NativePlugin( - name = "Camera", - requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT } +@CapacitorPlugin( + name = "Camera", + permissions = { + @Permission(permission = Manifest.permission.CAMERA, alias = "camera"), + @Permission(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, alias = "writePhotos"), + @Permission(permission = Manifest.permission.READ_EXTERNAL_STORAGE, alias = "readPhotos") + }, + requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT }, + permissionRequestCode = CameraPlugin.CAMERA_REQUEST_PERMISSIONS ) public class CameraPlugin extends Plugin { // Request codes + static final int CAMERA_REQUEST_PERMISSIONS = PluginRequestCodes.CAMERA_IMAGE_CAPTURE; static final int REQUEST_IMAGE_CAPTURE = PluginRequestCodes.CAMERA_IMAGE_CAPTURE; static final int REQUEST_IMAGE_PICK = PluginRequestCodes.CAMERA_IMAGE_PICK; static final int REQUEST_IMAGE_EDIT = PluginRequestCodes.CAMERA_IMAGE_EDIT; @@ -140,13 +152,13 @@ private boolean checkCameraPermissions(PluginCall call) { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, - REQUEST_IMAGE_CAPTURE + CAMERA_REQUEST_PERMISSIONS ); return false; } // If we don't need to save to the gallery, we can just ask for camera permissions else if (!hasPermission(Manifest.permission.CAMERA)) { - pluginRequestPermission(Manifest.permission.CAMERA, REQUEST_IMAGE_CAPTURE); + pluginRequestPermission(Manifest.permission.CAMERA, CAMERA_REQUEST_PERMISSIONS); return false; } return true; @@ -154,7 +166,7 @@ else if (!hasPermission(Manifest.permission.CAMERA)) { private boolean checkPhotosPermissions(PluginCall call) { if (!hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - pluginRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_IMAGE_CAPTURE); + pluginRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, CAMERA_REQUEST_PERMISSIONS); return false; } return true; @@ -439,31 +451,69 @@ private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStre } @Override - protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.handleRequestPermissionsResult(requestCode, permissions, grantResults); - + protected + void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { Logger.debug(getLogTag(), "handling request perms result"); - if (getSavedCall() == null) { + PluginCall savedCall = getSavedCall(); + if (savedCall == null) { Logger.debug(getLogTag(), "No stored plugin call for permissions request result"); return; } - PluginCall savedCall = getSavedCall(); + // TODO: remove this block once handlePermissions is available in core + // + // handlePermissions(permissions, grantResults); + // + // start block + SharedPreferences prefs = getContext().getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE); - for (int i = 0; i < grantResults.length; i++) { - int result = grantResults[i]; - String perm = permissions[i]; - if (result == PackageManager.PERMISSION_DENIED) { - Logger.debug(getLogTag(), "User denied camera permission: " + perm); - savedCall.reject(PERMISSION_DENIED_ERROR); - return; + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted. If previously denied, remove cached state + for (String permission : permissions) { + String state = prefs.getString(permission, null); + + if (state != null) { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(permission); + editor.apply(); + } + } + } else { + for (String permission : permissions) { + SharedPreferences.Editor editor = prefs.edit(); + + if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission)) { + // Permission denied, can prompt again with rationale + editor.putString(permission, "prompt-with-rationale"); + } else { + // Permission denied permanently, store this state for future reference + editor.putString(permission, "denied"); + } + + editor.apply(); } } + // end block - if (requestCode == REQUEST_IMAGE_CAPTURE) { + + if (savedCall.getMethodName().equals("getPhoto")) { + for (int i = 0; i < grantResults.length; i++) { + int result = grantResults[i]; + String perm = permissions[i]; + if (result == PackageManager.PERMISSION_DENIED) { + Logger.debug(getLogTag(), "User denied camera permission: " + perm); + savedCall.reject(PERMISSION_DENIED_ERROR); + savedCall.release(bridge); + return; + } + } doShow(savedCall); } + else { + savedCall.resolve(getPermissionStates()); + savedCall.release(bridge); + } } @Override From 2990dba779c2efd04872d27e4ba6f72e7e32c6ad Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 17 Nov 2020 15:51:42 -0500 Subject: [PATCH 06/58] Matching iOS permission types to android --- camera/ios/Plugin/CameraPlugin.swift | 9 +++++++-- camera/ios/Plugin/CameraTypes.swift | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index fa3d3c355..edd788806 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -20,7 +20,10 @@ public class CameraPlugin: CAPPlugin { switch permission { case .camera: state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState - case .photos: + case .writePhotos: + // inserting captured photos into the camera roll is always allowed + state = PHAuthorizationStatus.authorized.authorizationState + case .readPhotos: state = PHPhotoLibrary.authorizationStatus().authorizationState } result[permission.rawValue] = state @@ -48,7 +51,9 @@ public class CameraPlugin: CAPPlugin { AVAuthorizationStatus.denied.authorizationState group.leave() } - case .photos: + case .writePhotos: + result[permission.rawValue] = PHAuthorizationStatus.authorized.authorizationState + case .readPhotos: group.enter() PHPhotoLibrary.requestAuthorization({ (status) in result[permission.rawValue] = status.authorizationState diff --git a/camera/ios/Plugin/CameraTypes.swift b/camera/ios/Plugin/CameraTypes.swift index 22425a3c0..548063033 100644 --- a/camera/ios/Plugin/CameraTypes.swift +++ b/camera/ios/Plugin/CameraTypes.swift @@ -57,7 +57,8 @@ public struct CameraResult { internal enum CameraPermissionType: String, CaseIterable { case camera - case photos + case writePhotos + case readPhotos } internal enum CameraPropertyListKeys: String, CaseIterable { From d34c9ca36e50cef9aaca3e7c454a2844894a82f0 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 17 Nov 2020 18:02:01 -0500 Subject: [PATCH 07/58] Export iOS methods --- camera/ios/Plugin/CameraPlugin.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/camera/ios/Plugin/CameraPlugin.m b/camera/ios/Plugin/CameraPlugin.m index 4d0f9dcf0..572765aa9 100644 --- a/camera/ios/Plugin/CameraPlugin.m +++ b/camera/ios/Plugin/CameraPlugin.m @@ -3,4 +3,6 @@ CAP_PLUGIN(CAPCameraPlugin, "Camera", CAP_PLUGIN_METHOD(getPhoto, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(checkPermissions, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(requestPermissions, CAPPluginReturnPromise); ) From 4d4e37a551da40cc0eda44432aef281b3ee4701f Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 17 Nov 2020 18:04:27 -0500 Subject: [PATCH 08/58] Adding web definitions and implementation for permissions. --- camera/README.md | 56 ++++++++++++++++++++++++++++++++++++++- camera/src/definitions.ts | 32 ++++++++++++++++++++++ camera/src/web.ts | 35 +++++++++++++++++++++++- 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/camera/README.md b/camera/README.md index cabd7140e..ebe33edd8 100644 --- a/camera/README.md +++ b/camera/README.md @@ -14,6 +14,8 @@ npx cap sync * [`getPhoto(...)`](#getphoto) +* [`checkPermissions()`](#checkpermissions) +* [`requestPermissions(...)`](#requestpermissions) * [Interfaces](#interfaces) @@ -36,6 +38,42 @@ with the camera. **Returns:** Promise<CameraPhoto> +**Since:** 1.0.0 + +-------------------- + + +### checkPermissions() + +```typescript +checkPermissions() => Promise +``` + +Check camera and photo album permissions + +**Returns:** Promise<CameraPermissionStatus> + +**Since:** 1.0.0 + +-------------------- + + +### requestPermissions(...) + +```typescript +requestPermissions(types: CameraPluginPermissions | null) => Promise +``` + +Request camera and photo album permissions + +| Param | Type | +| ----------- | ----------------------------------------------------------------------------------- | +| **`types`** | CameraPluginPermissions \| null | + +**Returns:** Promise<CameraPermissionStatus> + +**Since:** 1.0.0 + -------------------- @@ -66,7 +104,7 @@ with the camera. | **`height`** | number | The height of the saved image | | **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. Default: false | | **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode Default: true | -| **`source`** | "prompt" \| "camera" \| "photos" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. Default: CameraSource.Prompt | +| **`source`** | "camera" \| "prompt" \| "photos" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. Default: CameraSource.Prompt | | **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. Default: CameraDirection.Rear | | **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. Defaults to fullscreen. | | **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | @@ -75,4 +113,20 @@ with the camera. | **`promptLabelPhoto`** | string | | | **`promptLabelPicture`** | string | | + +#### CameraPermissionStatus + +| Prop | Type | +| ----------------- | ---------------- | +| **`camera`** | any | +| **`writePhotos`** | any | +| **`readPhotos`** | any | + + +#### CameraPluginPermissions + +| Prop | Type | +| ----------- | ----------------------------------- | +| **`types`** | CameraPermissionType[] | + diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index 65ed7e178..dbb726733 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -6,12 +6,44 @@ declare module '@capacitor/core' { } } +export type CameraPermissionState = PermissionState; + +export type CameraPermissionType = 'camera' | 'writePhotos' | 'readPhotos'; + +export interface CameraPermissionStatus { + camera: CameraPermissionState; + writePhotos: CameraPermissionState; + readPhotos: CameraPermissionState; +} + +export interface CameraPluginPermissions { + types: CameraPermissionType[]; +} + export interface CameraPlugin { /** * Prompt the user to pick a photo from an album, or take a new photo * with the camera. + * + * @since 1.0.0 */ getPhoto(options: CameraOptions): Promise; + + /** + * Check camera and photo album permissions + * + * @since 1.0.0 + */ + checkPermissions(): Promise; + + /** + * Request camera and photo album permissions + * + * @since 1.0.0 + */ + requestPermissions( + types: CameraPluginPermissions | null, + ): Promise; } export interface CameraOptions { diff --git a/camera/src/web.ts b/camera/src/web.ts index 0af3b98dc..55384bab9 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -1,6 +1,11 @@ import { WebPlugin } from '@capacitor/core'; -import type { CameraPlugin, CameraPhoto, CameraOptions } from './definitions'; +import type { + CameraPlugin, + CameraPhoto, + CameraOptions, + CameraPermissionStatus, +} from './definitions'; export class CameraWeb extends WebPlugin implements CameraPlugin { constructor() { @@ -148,6 +153,34 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } }); } + + async checkPermissions(): Promise { + if (typeof navigator === 'undefined' || !navigator.permissions) { + throw this.unavailable('Permissions API not available in this browser'); + } + + try { + // https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query + // the specific permissions that are supported varies among browsers that implement the + // permissions API, so we need a try/catch in case 'camera' is invalid + const permission = await window.navigator.permissions.query({ + name: 'camera', + }); + return { + camera: permission.state, + writePhotos: 'denied', + readPhotos: 'denied', + }; + } catch { + throw this.unavailable( + 'Camera permissions are not available in this browser', + ); + } + } + + async requestPermissions(): Promise { + throw this.unimplemented('Not implemented on web.'); + } } const Camera = new CameraWeb(); From f7d3bbd2caf80bf95e7ee9c3f4dad582c456e1b7 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 17 Nov 2020 18:04:48 -0500 Subject: [PATCH 09/58] Code formatting --- .../CameraBottomSheetDialogFragment.java | 1 - .../plugins/camera/CameraPlugin.java | 27 +++++++++---------- .../plugins/camera/CameraSettings.java | 1 + .../plugins/camera/ExifWrapper.java | 1 + 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java index 5f31d33f4..de2fff134 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java @@ -48,7 +48,6 @@ public void onCancel(DialogInterface dialog) { } private BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() { - @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index a59630340..6eeb92a4b 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -11,7 +11,6 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; - import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import com.getcapacitor.FileUtils; @@ -43,16 +42,17 @@ * Adapted from https://developer.android.com/training/camera/photobasics.html */ @CapacitorPlugin( - name = "Camera", - permissions = { - @Permission(permission = Manifest.permission.CAMERA, alias = "camera"), - @Permission(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, alias = "writePhotos"), - @Permission(permission = Manifest.permission.READ_EXTERNAL_STORAGE, alias = "readPhotos") - }, - requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT }, - permissionRequestCode = CameraPlugin.CAMERA_REQUEST_PERMISSIONS + name = "Camera", + permissions = { + @Permission(permission = Manifest.permission.CAMERA, alias = "camera"), + @Permission(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, alias = "writePhotos"), + @Permission(permission = Manifest.permission.READ_EXTERNAL_STORAGE, alias = "readPhotos") + }, + requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT }, + permissionRequestCode = CameraPlugin.CAMERA_REQUEST_PERMISSIONS ) public class CameraPlugin extends Plugin { + // Request codes static final int CAMERA_REQUEST_PERMISSIONS = PluginRequestCodes.CAMERA_IMAGE_CAPTURE; static final int REQUEST_IMAGE_CAPTURE = PluginRequestCodes.CAMERA_IMAGE_CAPTURE; @@ -152,7 +152,7 @@ private boolean checkCameraPermissions(PluginCall call) { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, - CAMERA_REQUEST_PERMISSIONS + CAMERA_REQUEST_PERMISSIONS ); return false; } @@ -451,8 +451,7 @@ private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStre } @Override - protected - void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + protected void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { Logger.debug(getLogTag(), "handling request perms result"); PluginCall savedCall = getSavedCall(); @@ -496,7 +495,6 @@ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] gra } // end block - if (savedCall.getMethodName().equals("getPhoto")) { for (int i = 0; i < grantResults.length; i++) { int result = grantResults[i]; @@ -509,8 +507,7 @@ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] gra } } doShow(savedCall); - } - else { + } else { savedCall.resolve(getPermissionStates()); savedCall.release(bridge); } diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java index a70e5d64b..118f8bfd3 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraSettings.java @@ -1,6 +1,7 @@ package com.capacitorjs.plugins.camera; public class CameraSettings { + public static final int DEFAULT_QUALITY = 90; public static final boolean DEFAULT_SAVE_IMAGE_TO_GALLERY = false; public static final boolean DEFAULT_CORRECT_ORIENTATION = true; diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java index fcafa3be4..663484f89 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java @@ -6,6 +6,7 @@ import com.getcapacitor.JSObject; public class ExifWrapper { + private final ExifInterface exif; public ExifWrapper(ExifInterface exif) { From 44dc648acd07bda9c7bb459e7517644a9e5f4047 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 19 Nov 2020 17:24:34 -0500 Subject: [PATCH 10/58] Update camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java Co-authored-by: Carl Poole --- .../java/com/capacitorjs/plugins/camera/CameraPlugin.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 6eeb92a4b..fb6cbe623 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -44,9 +44,9 @@ @CapacitorPlugin( name = "Camera", permissions = { - @Permission(permission = Manifest.permission.CAMERA, alias = "camera"), - @Permission(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, alias = "writePhotos"), - @Permission(permission = Manifest.permission.READ_EXTERNAL_STORAGE, alias = "readPhotos") + @Permission(strings = { Manifest.permission.CAMERA }, alias = "camera"), + @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "writePhotos"), + @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "readPhotos") }, requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT }, permissionRequestCode = CameraPlugin.CAMERA_REQUEST_PERMISSIONS From 26c25cd9d0027ad32c5a902670b034b05100bb56 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 19 Nov 2020 17:24:43 -0500 Subject: [PATCH 11/58] Update camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java Co-authored-by: Carl Poole --- .../plugins/camera/CameraPlugin.java | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index fb6cbe623..25e11cb1b 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -460,40 +460,10 @@ protected void onRequestPermissionsResult(int requestCode, String[] permissions, return; } - // TODO: remove this block once handlePermissions is available in core - // - // handlePermissions(permissions, grantResults); - // - // start block - SharedPreferences prefs = getContext().getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE); - - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Permission granted. If previously denied, remove cached state - for (String permission : permissions) { - String state = prefs.getString(permission, null); - - if (state != null) { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(permission); - editor.apply(); - } - } - } else { - for (String permission : permissions) { - SharedPreferences.Editor editor = prefs.edit(); - - if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission)) { - // Permission denied, can prompt again with rationale - editor.putString(permission, "prompt-with-rationale"); - } else { - // Permission denied permanently, store this state for future reference - editor.putString(permission, "denied"); - } - - editor.apply(); - } + if (!validatePermissions(permissions, grantResults)) { + freeSavedCall(); + return; } - // end block if (savedCall.getMethodName().equals("getPhoto")) { for (int i = 0; i < grantResults.length; i++) { From cdf8d90c900bfc89c23ddf967c55daf7fbde6b53 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 25 Nov 2020 11:46:21 -0500 Subject: [PATCH 12/58] Update camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java Co-authored-by: Carl Poole --- .../plugins/camera/CameraPlugin.java | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 25e11cb1b..577bdf66f 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -451,20 +451,9 @@ private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStre } @Override - protected void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + protected void onRequestPermissionsResult(PluginCall savedCall, int requestCode, String[] permissions, int[] grantResults) { Logger.debug(getLogTag(), "handling request perms result"); - PluginCall savedCall = getSavedCall(); - if (savedCall == null) { - Logger.debug(getLogTag(), "No stored plugin call for permissions request result"); - return; - } - - if (!validatePermissions(permissions, grantResults)) { - freeSavedCall(); - return; - } - if (savedCall.getMethodName().equals("getPhoto")) { for (int i = 0; i < grantResults.length; i++) { int result = grantResults[i]; @@ -472,14 +461,10 @@ protected void onRequestPermissionsResult(int requestCode, String[] permissions, if (result == PackageManager.PERMISSION_DENIED) { Logger.debug(getLogTag(), "User denied camera permission: " + perm); savedCall.reject(PERMISSION_DENIED_ERROR); - savedCall.release(bridge); return; } } doShow(savedCall); - } else { - savedCall.resolve(getPermissionStates()); - savedCall.release(bridge); } } From 4436d33ae0d494c07686dd4b98d57e6e07469a04 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 25 Nov 2020 11:46:31 -0500 Subject: [PATCH 13/58] Update camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java Co-authored-by: Carl Poole --- .../java/com/capacitorjs/plugins/camera/CameraPlugin.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 577bdf66f..406b19a9e 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -469,11 +469,7 @@ protected void onRequestPermissionsResult(PluginCall savedCall, int requestCode, } @Override - protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) { - super.handleOnActivityResult(requestCode, resultCode, data); - - PluginCall savedCall = getSavedCall(); - + protected void handleOnActivityResult(PluginCall savedCall, int requestCode, int resultCode, Intent data) { if (savedCall == null) { return; } From 5b9098de268657619858244074c2b3c558a602e9 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 25 Nov 2020 15:01:11 -0500 Subject: [PATCH 14/58] Updating plugin registration for runtime refactor. --- camera/package.json | 11 +++++------ camera/rollup.config.js | 13 +++---------- camera/src/index.ts | 14 ++++---------- camera/src/web.ts | 3 --- camera/tsconfig.json | 10 +++------- 5 files changed, 15 insertions(+), 36 deletions(-) diff --git a/camera/package.json b/camera/package.json index 0a8757880..b0ff07f8a 100644 --- a/camera/package.json +++ b/camera/package.json @@ -2,9 +2,9 @@ "name": "@capacitor/camera", "version": "0.0.1", "description": "The Camera API provides the ability to take a photo with the camera or choose an existing one from the photo album.", - "main": "dist/plugin.js", - "module": "dist/esm/index.js", + "main": "dist/esm/index.js", "types": "dist/esm/index.d.ts", + "unpkg": "dist/plugin.js", "author": "Ionic ", "license": "MIT", "repository": { @@ -36,14 +36,13 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "@capacitor/android": "next", - "@capacitor/core": "next", + "@capacitor/android": "file:../../capacitor/android", + "@capacitor/core": "file:../../capacitor/core", "@capacitor/docgen": "^0.0.10", - "@capacitor/ios": "next", + "@capacitor/ios": "file:../../capacitor/ios", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^1.0.1", "@ionic/swiftlint-config": "^1.1.2", - "@rollup/plugin-node-resolve": "^9.0.0", "eslint": "^7.11.0", "prettier": "^2.1.2", "prettier-plugin-java": "^0.8.3", diff --git a/camera/rollup.config.js b/camera/rollup.config.js index d58bfbea1..9f8048847 100644 --- a/camera/rollup.config.js +++ b/camera/rollup.config.js @@ -1,21 +1,14 @@ -import nodeResolve from '@rollup/plugin-node-resolve'; - export default { input: 'dist/esm/index.js', output: { file: 'dist/plugin.js', format: 'iife', - name: 'capacitorPlugin', // TODO: change this + name: 'capacitorCamera', globals: { '@capacitor/core': 'capacitorExports', }, sourcemap: true, + inlineDynamicImports: true, }, - plugins: [ - nodeResolve({ - // allowlist of dependencies to bundle in - // @see https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve#resolveonly - resolveOnly: ['lodash'], - }), - ], + external: ['@capacitor/core'], }; diff --git a/camera/src/index.ts b/camera/src/index.ts index a937ca091..a9978ddef 100644 --- a/camera/src/index.ts +++ b/camera/src/index.ts @@ -1,5 +1,4 @@ -import type { PluginImplementations } from '@capacitor/core'; -import { Plugins, registerPlugin } from '@capacitor/core'; +import { registerPlugin } from '@capacitor/core'; import type { CameraPlugin } from './definitions'; import { @@ -9,15 +8,10 @@ import { CameraResultType, CameraSource, } from './definitions'; -import { CameraWeb } from './web'; -const implementations: PluginImplementations = { - android: Plugins.Camera, - ios: Plugins.Camera, - web: new CameraWeb(), -}; - -const Camera = registerPlugin('Camera', implementations).getImplementation(); +const Camera = registerPlugin('Camera', { + web: () => import('./web').then(m => new m.CameraWeb()), +}); export { Camera, diff --git a/camera/src/web.ts b/camera/src/web.ts index 55384bab9..407f7104a 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -8,9 +8,6 @@ import type { } from './definitions'; export class CameraWeb extends WebPlugin implements CameraPlugin { - constructor() { - super({ name: 'Camera' }); - } async getPhoto(options: CameraOptions): Promise { // eslint-disable-next-line no-async-promise-executor diff --git a/camera/tsconfig.json b/camera/tsconfig.json index 538e088fd..3bb999d96 100644 --- a/camera/tsconfig.json +++ b/camera/tsconfig.json @@ -3,10 +3,8 @@ "allowUnreachableCode": false, "declaration": true, "esModuleInterop": true, - "lib": [ - "dom" - ], - "module": "es2015", + "lib": ["dom"], + "module": "esnext", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, @@ -17,7 +15,5 @@ "strict": true, "target": "es2017" }, - "files": [ - "src/index.ts" - ] + "files": ["src/index.ts"] } From c6d990449c8d59d986cefc7f580b3840f0878efa Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 25 Nov 2020 15:01:48 -0500 Subject: [PATCH 15/58] Updating methods for alpha. --- camera/ios/Plugin/CameraPlugin.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index edd788806..78dd6fee6 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -13,7 +13,7 @@ public class CameraPlugin: CAPPlugin { private var imageCounter = 0 - @objc func checkPermissions(_ call: CAPPluginCall) { + @objc public override func checkPermissions(_ call: CAPPluginCall) { var result: [String: Any] = [:] for permission in CameraPermissionType.allCases { let state: String @@ -31,7 +31,7 @@ public class CameraPlugin: CAPPlugin { call.resolve(result) } - @objc func requestPermissions(_ call: CAPPluginCall) { + @objc public override func requestPermissions(_ call: CAPPluginCall) { // get the list of desired types, if passed let typeList = call.getArray("types", String.self)?.compactMap({ (type) -> CameraPermissionType? in return CameraPermissionType(rawValue: type) @@ -177,7 +177,7 @@ extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerD ]) } else if settings.resultType == CameraResultType.uri { guard let path = try? saveTemporaryImage(jpeg), - let webPath = CAPFileManager.getPortablePath(host: bridge?.getLocalUrl() ?? "", uri: URL(string: path)) else { + let webPath = CAPFileManager.getPortablePath(host: bridge?.config.localURL.path ?? "", uri: URL(string: path)) else { call?.reject("Unable to get portable path to file") return } @@ -214,7 +214,7 @@ private extension CameraPlugin { func showCamera() { // check if we have a camera - if (bridge?.isSimulator() ?? false) || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) { + if (bridge?.isSimEnvironment ?? false) || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) { bridge?.modulePrint(self, "Camera not available in simulator") bridge?.alert("Camera Error", "Camera not available in Simulator") call?.reject("Camera not available while running in Simulator") From 5fd7614f1d90259e95697165051e152d57bc31a3 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:27:31 -0500 Subject: [PATCH 16/58] Removing logging code. --- camera/ios/Plugin/CameraTypes.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/camera/ios/Plugin/CameraTypes.swift b/camera/ios/Plugin/CameraTypes.swift index 548063033..865e2c5b3 100644 --- a/camera/ios/Plugin/CameraTypes.swift +++ b/camera/ios/Plugin/CameraTypes.swift @@ -104,14 +104,7 @@ internal struct ProcessedImage { } mutating func overwriteMetadataOrientation(to orientation: Int) { - let original = metadata replaceDictionaryOrientation(atNode: &metadata, to: orientation) - - if (original as NSDictionary).isEqual(to: metadata) { - print("dictionary is unchanged") - } else { - print("dictionaries no longer match") - } } func replaceDictionaryOrientation(atNode node: inout [String: Any], to orientation: Int) { From b189933e0bd2e49d0e2dcdf43dcd2f9dce9b8999 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:28:46 -0500 Subject: [PATCH 17/58] Avoiding exception by setting source type, fixing logic bugs. --- camera/ios/Plugin/CameraPlugin.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 78dd6fee6..d380cd43a 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -234,6 +234,7 @@ private extension CameraPlugin { return } // select the input + imagePicker.sourceType = .camera if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) { imagePicker.cameraDevice = .rear } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) { @@ -313,6 +314,8 @@ private extension CameraPlugin { // get the image's metadata from the picker or from the photo album if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { result.metadata = photoMetadata + } + else { flags = flags.union([.gallery]) } if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset { @@ -328,7 +331,7 @@ private extension CameraPlugin { result.overwriteMetadataOrientation(to: 1) } // conditionally save the image - if settings.saveToGallery, flags.contains(.edited) == true, flags.contains(.gallery) == false { + if settings.saveToGallery && (flags.contains(.edited) == true || flags.contains(.gallery) == false) { UIImageWriteToSavedPhotosAlbum(result.image, nil, nil, nil) } From e58b1b49ccb7866adcec4e134f4ec11a2d0fd069 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:29:27 -0500 Subject: [PATCH 18/58] Updating to latest path methods. --- camera/ios/Plugin/CameraPlugin.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index d380cd43a..d9ccbc8c7 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -176,15 +176,15 @@ extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerD "format": "jpeg" ]) } else if settings.resultType == CameraResultType.uri { - guard let path = try? saveTemporaryImage(jpeg), - let webPath = CAPFileManager.getPortablePath(host: bridge?.config.localURL.path ?? "", uri: URL(string: path)) else { + guard let fileURL = try? saveTemporaryImage(jpeg), + let webURL = bridge?.portablePath(fromLocalURL: fileURL) else { call?.reject("Unable to get portable path to file") return } call?.resolve([ - "path": path, + "path": fileURL.path, "exif": processedImage.exifData, - "webPath": webPath, + "webPath": webURL.path, "format": "jpeg" ]) } @@ -288,15 +288,15 @@ private extension CameraPlugin { self.bridge?.viewController?.present(imagePicker, animated: true, completion: nil) } - func saveTemporaryImage(_ data: Data) throws -> String { + func saveTemporaryImage(_ data: Data) throws -> URL { var url: URL repeat { imageCounter += 1 url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("photo-\(imageCounter).jpg") - } while FileManager.default.fileExists(atPath: url.absoluteString) + } while FileManager.default.fileExists(atPath: url.path) try data.write(to: url, options: .atomic) - return url.absoluteString + return url } func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? { From da26eee7bd64891994753aaa1e11a3d3c89dc912 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:29:43 -0500 Subject: [PATCH 19/58] Removing deprecated calls. --- camera/ios/Plugin/CameraPlugin.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index d9ccbc8c7..748fbdd0c 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -72,7 +72,7 @@ public class CameraPlugin: CAPPlugin { // Make sure they have all the necessary info.plist settings if let missingUsageDescription = checkUsageDescriptions() { - bridge?.modulePrint(self, missingUsageDescription) + CAPLog.print("⚡️ ", self.pluginId, "-", missingUsageDescription) call.reject(missingUsageDescription) bridge?.alert("Camera Error", "Missing required usage description. See console for more information") return @@ -215,7 +215,7 @@ private extension CameraPlugin { func showCamera() { // check if we have a camera if (bridge?.isSimEnvironment ?? false) || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) { - bridge?.modulePrint(self, "Camera not available in simulator") + CAPLog.print("⚡️ ", self.pluginId, "-", "Camera not available in simulator") bridge?.alert("Camera Error", "Camera not available in Simulator") call?.reject("Camera not available while running in Simulator") return From ec9091b94b95252219e81d9d2c8cf1c76dd0da81 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:31:54 -0500 Subject: [PATCH 20/58] minor cleanup --- .../capacitorjs/plugins/camera/CameraPlugin.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 406b19a9e..c21425476 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -3,7 +3,6 @@ import android.Manifest; import android.app.Activity; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -11,12 +10,10 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; -import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import com.getcapacitor.FileUtils; import com.getcapacitor.JSObject; import com.getcapacitor.Logger; -import com.getcapacitor.NativePlugin; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; @@ -89,9 +86,6 @@ public void getPhoto(PluginCall call) { private void doShow(PluginCall call) { switch (settings.getSource()) { - case prompt: - showPrompt(call); - break; case camera: showCamera(call); break; @@ -106,7 +100,7 @@ private void doShow(PluginCall call) { private void showPrompt(final PluginCall call) { // We have all necessary permissions, open the camera - List options = new ArrayList(); + List options = new ArrayList<>(); options.add(call.getString("promptLabelPhoto", "From Photos")); options.add(call.getString("promptLabelPicture", "Take Picture")); @@ -183,7 +177,6 @@ private CameraSettings getSettings(PluginCall call) { settings.setShouldResize(settings.getWidth() > 0 || settings.getHeight() > 0); settings.setShouldCorrectOrientation(call.getBoolean("correctOrientation", CameraSettings.DEFAULT_CORRECT_ORIENTATION)); try { - String foo = CameraSource.prompt.getSource(); settings.setSource(CameraSource.valueOf(call.getString("source", CameraSource.prompt.getSource()))); } catch (IllegalArgumentException ex) { settings.setSource(CameraSource.prompt); @@ -539,7 +532,9 @@ private Intent createEditIntent(Uri origPhotoUri, boolean expose) { @Override protected Bundle saveInstanceState() { Bundle bundle = super.saveInstanceState(); - bundle.putString("cameraImageFileSavePath", imageFileSavePath); + if (bundle != null) { + bundle.putString("cameraImageFileSavePath", imageFileSavePath); + } return bundle; } From 4c58bc057a3c2fd56fcfc70c977421c10c0735b8 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:32:33 -0500 Subject: [PATCH 21/58] Use stream to get exif data. --- .../com/capacitorjs/plugins/camera/ImageUtils.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java index e1fea7a9c..f984492d2 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java @@ -126,14 +126,21 @@ private static int getOrientation(final Context c, final Uri imageUri) throws IO } public static ExifWrapper getExifData(final Context c, final Bitmap bitmap, final Uri imageUri) { + InputStream stream = null; try { - String fu = FileUtils.getFileUrlForUri(c, imageUri); - final ExifInterface exifInterface = new ExifInterface(fu); + stream = c.getContentResolver().openInputStream(imageUri); + final ExifInterface exifInterface = new ExifInterface(stream); return new ExifWrapper(exifInterface); } catch (IOException ex) { Logger.error("Error loading exif data from image", ex); - } finally {} + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) {} + } + } return new ExifWrapper(null); } } From c5d57110f0962a343448325b0ef87aa3d68681a5 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 15:32:56 -0500 Subject: [PATCH 22/58] Updating permissions. --- camera/README.md | 25 +++++++++---------- .../plugins/camera/CameraPlugin.java | 18 ++++++------- camera/ios/Plugin/CameraPlugin.swift | 9 ++----- camera/ios/Plugin/CameraTypes.swift | 3 +-- camera/src/definitions.ts | 9 +++---- camera/src/web.ts | 3 +-- 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/camera/README.md b/camera/README.md index ebe33edd8..f6369aa70 100644 --- a/camera/README.md +++ b/camera/README.md @@ -61,14 +61,14 @@ Check camera and photo album permissions ### requestPermissions(...) ```typescript -requestPermissions(types: CameraPluginPermissions | null) => Promise +requestPermissions(permissions: CameraPluginPermissions | null) => Promise ``` Request camera and photo album permissions -| Param | Type | -| ----------- | ----------------------------------------------------------------------------------- | -| **`types`** | CameraPluginPermissions \| null | +| Param | Type | +| ----------------- | ----------------------------------------------------------------------------------- | +| **`permissions`** | CameraPluginPermissions \| null | **Returns:** Promise<CameraPermissionStatus> @@ -104,7 +104,7 @@ Request camera and photo album permissions | **`height`** | number | The height of the saved image | | **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. Default: false | | **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode Default: true | -| **`source`** | "camera" \| "prompt" \| "photos" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. Default: CameraSource.Prompt | +| **`source`** | "camera" \| "photos" \| "prompt" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. Default: CameraSource.Prompt | | **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. Default: CameraDirection.Rear | | **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. Defaults to fullscreen. | | **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | @@ -116,17 +116,16 @@ Request camera and photo album permissions #### CameraPermissionStatus -| Prop | Type | -| ----------------- | ---------------- | -| **`camera`** | any | -| **`writePhotos`** | any | -| **`readPhotos`** | any | +| Prop | Type | +| ------------ | ---------------- | +| **`camera`** | any | +| **`photos`** | any | #### CameraPluginPermissions -| Prop | Type | -| ----------- | ----------------------------------- | -| **`types`** | CameraPermissionType[] | +| Prop | Type | +| ----------------- | ----------------------------------- | +| **`permissions`** | CameraPermissionType[] | diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index c21425476..1f9ccc2e1 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -42,8 +42,7 @@ name = "Camera", permissions = { @Permission(strings = { Manifest.permission.CAMERA }, alias = "camera"), - @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "writePhotos"), - @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "readPhotos") + @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "photos"), }, requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT }, permissionRequestCode = CameraPlugin.CAMERA_REQUEST_PERMISSIONS @@ -140,19 +139,19 @@ private boolean checkCameraPermissions(PluginCall call) { settings.isSaveToGallery() && !(hasPermission(Manifest.permission.CAMERA) && hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) ) { - pluginRequestPermissions( - new String[] { + requestPermissions(call, + new String[] { Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE - }, - CAMERA_REQUEST_PERMISSIONS + }, + CAMERA_REQUEST_PERMISSIONS ); return false; } // If we don't need to save to the gallery, we can just ask for camera permissions else if (!hasPermission(Manifest.permission.CAMERA)) { - pluginRequestPermission(Manifest.permission.CAMERA, CAMERA_REQUEST_PERMISSIONS); + requestPermission(call, Manifest.permission.CAMERA, CAMERA_REQUEST_PERMISSIONS); return false; } return true; @@ -160,7 +159,7 @@ else if (!hasPermission(Manifest.permission.CAMERA)) { private boolean checkPhotosPermissions(PluginCall call) { if (!hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - pluginRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, CAMERA_REQUEST_PERMISSIONS); + requestPermission(call, Manifest.permission.READ_EXTERNAL_STORAGE, CAMERA_REQUEST_PERMISSIONS); return false; } return true; @@ -451,7 +450,8 @@ protected void onRequestPermissionsResult(PluginCall savedCall, int requestCode, for (int i = 0; i < grantResults.length; i++) { int result = grantResults[i]; String perm = permissions[i]; - if (result == PackageManager.PERMISSION_DENIED) { + + if (result == PackageManager.PERMISSION_DENIED && perm != Manifest.permission.WRITE_EXTERNAL_STORAGE) { Logger.debug(getLogTag(), "User denied camera permission: " + perm); savedCall.reject(PERMISSION_DENIED_ERROR); return; diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 748fbdd0c..b5cc889b8 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -20,10 +20,7 @@ public class CameraPlugin: CAPPlugin { switch permission { case .camera: state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState - case .writePhotos: - // inserting captured photos into the camera roll is always allowed - state = PHAuthorizationStatus.authorized.authorizationState - case .readPhotos: + case .photos: state = PHPhotoLibrary.authorizationStatus().authorizationState } result[permission.rawValue] = state @@ -51,9 +48,7 @@ public class CameraPlugin: CAPPlugin { AVAuthorizationStatus.denied.authorizationState group.leave() } - case .writePhotos: - result[permission.rawValue] = PHAuthorizationStatus.authorized.authorizationState - case .readPhotos: + case .photos: group.enter() PHPhotoLibrary.requestAuthorization({ (status) in result[permission.rawValue] = status.authorizationState diff --git a/camera/ios/Plugin/CameraTypes.swift b/camera/ios/Plugin/CameraTypes.swift index 865e2c5b3..1b7daf865 100644 --- a/camera/ios/Plugin/CameraTypes.swift +++ b/camera/ios/Plugin/CameraTypes.swift @@ -57,8 +57,7 @@ public struct CameraResult { internal enum CameraPermissionType: String, CaseIterable { case camera - case writePhotos - case readPhotos + case photos } internal enum CameraPropertyListKeys: String, CaseIterable { diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index dbb726733..c45bc0091 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -8,16 +8,15 @@ declare module '@capacitor/core' { export type CameraPermissionState = PermissionState; -export type CameraPermissionType = 'camera' | 'writePhotos' | 'readPhotos'; +export type CameraPermissionType = 'camera' | 'photos'; export interface CameraPermissionStatus { camera: CameraPermissionState; - writePhotos: CameraPermissionState; - readPhotos: CameraPermissionState; + photos: CameraPermissionState; } export interface CameraPluginPermissions { - types: CameraPermissionType[]; + permissions: CameraPermissionType[]; } export interface CameraPlugin { @@ -42,7 +41,7 @@ export interface CameraPlugin { * @since 1.0.0 */ requestPermissions( - types: CameraPluginPermissions | null, + permissions: CameraPluginPermissions | null, ): Promise; } diff --git a/camera/src/web.ts b/camera/src/web.ts index 407f7104a..aa7155603 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -165,8 +165,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); return { camera: permission.state, - writePhotos: 'denied', - readPhotos: 'denied', + photos: 'denied', }; } catch { throw this.unavailable( From 312157a472c4c13907472269379b425cce45af0b Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 16:27:14 -0500 Subject: [PATCH 23/58] Fmt --- .../com/capacitorjs/plugins/camera/CameraPlugin.java | 11 ++++++----- camera/ios/Plugin/CameraPlugin.swift | 7 +++---- camera/src/web.ts | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 1f9ccc2e1..77fb6f6fd 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -42,7 +42,7 @@ name = "Camera", permissions = { @Permission(strings = { Manifest.permission.CAMERA }, alias = "camera"), - @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "photos"), + @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "photos") }, requestCodes = { CameraPlugin.REQUEST_IMAGE_CAPTURE, CameraPlugin.REQUEST_IMAGE_PICK, CameraPlugin.REQUEST_IMAGE_EDIT }, permissionRequestCode = CameraPlugin.CAMERA_REQUEST_PERMISSIONS @@ -139,13 +139,14 @@ private boolean checkCameraPermissions(PluginCall call) { settings.isSaveToGallery() && !(hasPermission(Manifest.permission.CAMERA) && hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) ) { - requestPermissions(call, - new String[] { + requestPermissions( + call, + new String[] { Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE - }, - CAMERA_REQUEST_PERMISSIONS + }, + CAMERA_REQUEST_PERMISSIONS ); return false; } diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index b5cc889b8..fc7083b39 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -13,7 +13,7 @@ public class CameraPlugin: CAPPlugin { private var imageCounter = 0 - @objc public override func checkPermissions(_ call: CAPPluginCall) { + @objc override public func checkPermissions(_ call: CAPPluginCall) { var result: [String: Any] = [:] for permission in CameraPermissionType.allCases { let state: String @@ -28,7 +28,7 @@ public class CameraPlugin: CAPPlugin { call.resolve(result) } - @objc public override func requestPermissions(_ call: CAPPluginCall) { + @objc override public func requestPermissions(_ call: CAPPluginCall) { // get the list of desired types, if passed let typeList = call.getArray("types", String.self)?.compactMap({ (type) -> CameraPermissionType? in return CameraPermissionType(rawValue: type) @@ -309,8 +309,7 @@ private extension CameraPlugin { // get the image's metadata from the picker or from the photo album if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { result.metadata = photoMetadata - } - else { + } else { flags = flags.union([.gallery]) } if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset { diff --git a/camera/src/web.ts b/camera/src/web.ts index aa7155603..f3eed0de4 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -8,7 +8,6 @@ import type { } from './definitions'; export class CameraWeb extends WebPlugin implements CameraPlugin { - async getPhoto(options: CameraOptions): Promise { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { From 68384ded3a4519f1c0f3f0ab5ecafc1ad492dcab Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 22:13:12 -0500 Subject: [PATCH 24/58] Exposing 'limited' permissions state. --- camera/src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index c45bc0091..3a1300537 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -6,7 +6,7 @@ declare module '@capacitor/core' { } } -export type CameraPermissionState = PermissionState; +export type CameraPermissionState = PermissionState | 'limited'; export type CameraPermissionType = 'camera' | 'photos'; From 1c20ee42e8b041476d6e3436076a14def9e590b5 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 22:14:01 -0500 Subject: [PATCH 25/58] Adopting new iOS 14 APIs. --- camera/ios/Plugin/CameraPlugin.swift | 212 ++++++++++++++++++++------- 1 file changed, 159 insertions(+), 53 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index fc7083b39..083135059 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -1,13 +1,22 @@ import Foundation import Capacitor import Photos +import PhotosUI + +/* + Runtime detection of iOS 14 with @available and #available is straightforward but that code will fail to compile + under Xcode 11. So we need to use statements in the form of + + #if swift(>=5.3) + #endif + + as a poor proxy for Xcode 12 detection. The conditionals should be removed once Xcode 12 is required. + */ @objc(CAPCameraPlugin) public class CameraPlugin: CAPPlugin { private var call: CAPPluginCall? private var settings = CameraSettings() - private var imagePicker: UIImagePickerController? - private let defaultSource = CameraSource.prompt private let defaultDirection = CameraDirection.rear @@ -21,7 +30,15 @@ public class CameraPlugin: CAPPlugin { case .camera: state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState case .photos: + #if swift(>=5.3) + if #available(iOS 14, *) { + state = PHPhotoLibrary.authorizationStatus(for: .readWrite).authorizationState + } else { + state = PHPhotoLibrary.authorizationStatus().authorizationState + } + #else state = PHPhotoLibrary.authorizationStatus().authorizationState + #endif } result[permission.rawValue] = state } @@ -50,10 +67,24 @@ public class CameraPlugin: CAPPlugin { } case .photos: group.enter() + #if swift(>=5.3) + if #available(iOS 14, *) { + PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in + result[permission.rawValue] = status.authorizationState + group.leave() + } + } else { + PHPhotoLibrary.requestAuthorization({ (status) in + result[permission.rawValue] = status.authorizationState + group.leave() + }) + } + #else PHPhotoLibrary.requestAuthorization({ (status) in result[permission.rawValue] = status.authorizationState group.leave() }) + #endif } } group.notify(queue: DispatchQueue.main) { @@ -74,10 +105,6 @@ public class CameraPlugin: CAPPlugin { } DispatchQueue.main.async { - self.imagePicker = UIImagePickerController() - self.imagePicker?.delegate = self - self.imagePicker?.allowsEditing = self.settings.allowEditing - switch self.settings.source { case .prompt: self.showPrompt() @@ -147,12 +174,49 @@ extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerD } public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - - guard let processedImage = self.processImage(from: info) else { + picker.dismiss(animated: true, completion: nil) + if let processedImage = processImage(from: info) { + returnProcessedImage(processedImage) + } + else { self.call?.reject("Error processing image") + } + } +} + +#if swift(>=5.3) +@available(iOS 14, *) +extension CameraPlugin: PHPickerViewControllerDelegate { + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true, completion: nil) + guard let result = results.first else { + self.call?.reject("User cancelled photos app") return } + guard result.itemProvider.canLoadObject(ofClass: UIImage.self) else { + self.call?.reject("Error loading image") + return + } + // extract the image + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, error) in + if let image = reading as? UIImage { + var asset: PHAsset? + if let assetId = result.assetIdentifier { + asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject + } + if let processedImage = self?.processedImage(from: image, with: asset?.imageData) { + self?.returnProcessedImage(processedImage) + return + } + } + self?.call?.reject("Error loading image") + } + } +} +#endif +private extension CameraPlugin { + func returnProcessedImage(_ processedImage: ProcessedImage) { guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { self.call?.reject("Unable to convert image to jpeg") return @@ -183,12 +247,8 @@ extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerD "format": "jpeg" ]) } - - picker.dismiss(animated: true, completion: nil) } -} - -private extension CameraPlugin { + func showPrompt() { // Build the action sheet let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet) @@ -225,21 +285,7 @@ private extension CameraPlugin { AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in if granted { DispatchQueue.main.async { - guard let imagePicker = self?.imagePicker, let settings = self?.settings else { - return - } - // select the input - imagePicker.sourceType = .camera - if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) { - imagePicker.cameraDevice = .rear - } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) { - imagePicker.cameraDevice = .front - } - // present - imagePicker.modalPresentationStyle = settings.presentationStyle - imagePicker.sourceType = .camera - - self?.bridge?.viewController?.present(imagePicker, animated: true, completion: nil) + self?.presentCameraPicker() } } else { self?.call?.reject("User denied access to camera") @@ -256,12 +302,12 @@ private extension CameraPlugin { } // we either already have permission or can prompt if authStatus == .authorized { - presentPhotoPicker() + presentSystemAppropriateImagePicker() } else { PHPhotoLibrary.requestAuthorization({ [weak self] (status) in if status == PHAuthorizationStatus.authorized { DispatchQueue.main.async { [weak self] in - self?.presentPhotoPicker() + self?.presentSystemAppropriateImagePicker() } } else { self?.call?.reject("User denied access to photos") @@ -269,19 +315,71 @@ private extension CameraPlugin { }) } } - - func presentPhotoPicker() { - guard let imagePicker = imagePicker else { - return + + func presentCameraPicker() { + let picker = UIImagePickerController() + picker.delegate = self + picker.allowsEditing = self.settings.allowEditing + // select the input + picker.sourceType = .camera + if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) { + picker.cameraDevice = .rear + } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) { + picker.cameraDevice = .front + } + // present + picker.modalPresentationStyle = settings.presentationStyle + if settings.presentationStyle == .popover { + picker.popoverPresentationController?.delegate = self + setCenteredPopover(picker) + } + bridge?.viewController?.present(picker, animated: true, completion: nil) + } + + func presentSystemAppropriateImagePicker() { + #if swift(>=5.3) + if #available(iOS 14, *) { + presentPhotoPicker() + } else { + presentImagePicker() + } + #else + presentImagePicker() + #endif + } + + func presentImagePicker() { + let picker = UIImagePickerController() + picker.delegate = self + picker.allowsEditing = self.settings.allowEditing + // select the input + picker.sourceType = .photoLibrary + // present + picker.modalPresentationStyle = settings.presentationStyle + if settings.presentationStyle == .popover { + picker.popoverPresentationController?.delegate = self + setCenteredPopover(picker) } + bridge?.viewController?.present(picker, animated: true, completion: nil) + } + + #if swift(>=5.3) + @available(iOS 14, *) + func presentPhotoPicker() { + var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) + configuration.selectionLimit = 1 + configuration.filter = .images + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + // present + picker.modalPresentationStyle = settings.presentationStyle if settings.presentationStyle == .popover { - imagePicker.modalPresentationStyle = .popover - imagePicker.popoverPresentationController?.delegate = self - setCenteredPopover(imagePicker) + picker.popoverPresentationController?.delegate = self + setCenteredPopover(picker) } - imagePicker.sourceType = .photoLibrary - self.bridge?.viewController?.present(imagePicker, animated: true, completion: nil) + bridge?.viewController?.present(picker, animated: true, completion: nil) } + #endif func saveTemporaryImage(_ data: Data) throws -> URL { var url: URL @@ -295,26 +393,39 @@ private extension CameraPlugin { } func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? { - // get the image - var result: ProcessedImage + var selectedImage: UIImage? var flags: PhotoFlags = [] - if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { - result = ProcessedImage(image: image, metadata: [:]) // use the edited version + // get the image + if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { + selectedImage = edited // use the edited version flags = flags.union([.edited]) - } else if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - result = ProcessedImage(image: image, metadata: [:]) // use the original version - } else { + } else if let original = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + selectedImage = original // use the original version + } + guard let image = selectedImage else { return nil } + var metadata: [String: Any] = [:] // get the image's metadata from the picker or from the photo album if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { - result.metadata = photoMetadata + metadata = photoMetadata } else { flags = flags.union([.gallery]) } if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset { - result.metadata = asset.imageData + metadata = asset.imageData + } + // get the result + let result = processedImage(from: image, with: metadata) + // conditionally save the image + if settings.saveToGallery && (flags.contains(.edited) == true || flags.contains(.gallery) == false) { + UIImageWriteToSavedPhotosAlbum(result.image, nil, nil, nil) } + return result + } + + func processedImage(from image: UIImage, with metadata: [String: Any]?) -> ProcessedImage { + var result = ProcessedImage(image: image, metadata: metadata ?? [:]) // resizing the image only makes sense if we have real values to which to constrain it if settings.shouldResize, settings.width > 0 || settings.height > 0 { result.image = result.image.reformat(to: CGSize(width: settings.width, height: settings.height)) @@ -324,11 +435,6 @@ private extension CameraPlugin { result.image = result.image.reformat() result.overwriteMetadataOrientation(to: 1) } - // conditionally save the image - if settings.saveToGallery && (flags.contains(.edited) == true || flags.contains(.gallery) == false) { - UIImageWriteToSavedPhotosAlbum(result.image, nil, nil, nil) - } - return result } } From 1fe4305aa6df1aacf63b2aec893f7df3382be86f Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 22:17:04 -0500 Subject: [PATCH 26/58] Fmt --- camera/ios/Plugin/CameraPlugin.swift | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 083135059..9934f4837 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -6,10 +6,10 @@ import PhotosUI /* Runtime detection of iOS 14 with @available and #available is straightforward but that code will fail to compile under Xcode 11. So we need to use statements in the form of - + #if swift(>=5.3) #endif - + as a poor proxy for Xcode 12 detection. The conditionals should be removed once Xcode 12 is required. */ @@ -177,8 +177,7 @@ extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerD picker.dismiss(animated: true, completion: nil) if let processedImage = processImage(from: info) { returnProcessedImage(processedImage) - } - else { + } else { self.call?.reject("Error processing image") } } @@ -198,7 +197,7 @@ extension CameraPlugin: PHPickerViewControllerDelegate { return } // extract the image - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, error) in + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in if let image = reading as? UIImage { var asset: PHAsset? if let assetId = result.assetIdentifier { @@ -248,7 +247,7 @@ private extension CameraPlugin { ]) } } - + func showPrompt() { // Build the action sheet let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet) @@ -315,7 +314,7 @@ private extension CameraPlugin { }) } } - + func presentCameraPicker() { let picker = UIImagePickerController() picker.delegate = self @@ -335,7 +334,7 @@ private extension CameraPlugin { } bridge?.viewController?.present(picker, animated: true, completion: nil) } - + func presentSystemAppropriateImagePicker() { #if swift(>=5.3) if #available(iOS 14, *) { @@ -347,7 +346,7 @@ private extension CameraPlugin { presentImagePicker() #endif } - + func presentImagePicker() { let picker = UIImagePickerController() picker.delegate = self @@ -362,7 +361,7 @@ private extension CameraPlugin { } bridge?.viewController?.present(picker, animated: true, completion: nil) } - + #if swift(>=5.3) @available(iOS 14, *) func presentPhotoPicker() { @@ -423,7 +422,7 @@ private extension CameraPlugin { } return result } - + func processedImage(from image: UIImage, with metadata: [String: Any]?) -> ProcessedImage { var result = ProcessedImage(image: image, metadata: metadata ?? [:]) // resizing the image only makes sense if we have real values to which to constrain it From f7361464e520da8cdb79d38c4eddcb457fa91b51 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 2 Dec 2020 22:19:44 -0500 Subject: [PATCH 27/58] Updating dependencies to alpha 7 --- camera/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/package.json b/camera/package.json index b0ff07f8a..63ce23241 100644 --- a/camera/package.json +++ b/camera/package.json @@ -36,10 +36,10 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "@capacitor/android": "file:../../capacitor/android", - "@capacitor/core": "file:../../capacitor/core", + "@capacitor/android": "^3.0.0-alpha.7", + "@capacitor/core": "^3.0.0-alpha.7", "@capacitor/docgen": "^0.0.10", - "@capacitor/ios": "file:../../capacitor/ios", + "@capacitor/ios": "^3.0.0-alpha.7", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^1.0.1", "@ionic/swiftlint-config": "^1.1.2", From 69a1fae75ab41f8ea5248e4d0f96b7a00e66ede8 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 3 Dec 2020 20:46:17 +0100 Subject: [PATCH 28/58] update gradle wrapper and plugin --- camera/android/build.gradle | 2 +- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 58695 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- camera/android/gradlew | 2 ++ camera/android/gradlew.bat | 4 ++++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/camera/android/build.gradle b/camera/android/build.gradle index dc9bef47a..eba258f74 100644 --- a/camera/android/build.gradle +++ b/camera/android/build.gradle @@ -11,7 +11,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.1' } } diff --git a/camera/android/gradle/wrapper/gradle-wrapper.jar b/camera/android/gradle/wrapper/gradle-wrapper.jar index f3d88b1c2faf2fc91d853cd5d4242b5547257070..62d4c053550b91381bbd28b1afc82d634bf73a8a 100644 GIT binary patch delta 12524 zcmY*pqN z|ERss*;ReIR@Lg&r_R7IT-GRDbpR4NMKNcwFgy&*A|eco1PnBHRth?SBneiw6$UT&^LVJ0?Naa`KfV(>F!Ez}~S z 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 delta 12163 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 diff --git a/camera/android/gradle/wrapper/gradle-wrapper.properties b/camera/android/gradle/wrapper/gradle-wrapper.properties index 4e1cc9db6..186b71557 100644 --- a/camera/android/gradle/wrapper/gradle-wrapper.properties +++ b/camera/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/camera/android/gradlew b/camera/android/gradlew index 2fe81a7d9..fbd7c5158 100755 --- a/camera/android/gradlew +++ b/camera/android/gradlew @@ -82,6 +82,7 @@ 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 @@ -129,6 +130,7 @@ fi 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 diff --git a/camera/android/gradlew.bat b/camera/android/gradlew.bat index 24467a141..a9f778a7a 100644 --- a/camera/android/gradlew.bat +++ b/camera/android/gradlew.bat @@ -29,6 +29,9 @@ 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" @@ -81,6 +84,7 @@ set CMD_LINE_ARGS=%* 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% From f973808055c5c02d76837e39d2cea1b06406f4b7 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 3 Dec 2020 20:47:13 +0100 Subject: [PATCH 29/58] remove bridge_layout_main.xml --- .../src/main/res/layout/bridge_layout_main.xml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 camera/android/src/main/res/layout/bridge_layout_main.xml diff --git a/camera/android/src/main/res/layout/bridge_layout_main.xml b/camera/android/src/main/res/layout/bridge_layout_main.xml deleted file mode 100644 index 56fec1546..000000000 --- a/camera/android/src/main/res/layout/bridge_layout_main.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - From 921496d5022a16284a2013444c971109465ef116 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 16:17:26 -0500 Subject: [PATCH 30/58] Removed contributing.md file. --- camera/CONTRIBUTING.md | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 camera/CONTRIBUTING.md diff --git a/camera/CONTRIBUTING.md b/camera/CONTRIBUTING.md deleted file mode 100644 index 019c41eef..000000000 --- a/camera/CONTRIBUTING.md +++ /dev/null @@ -1,42 +0,0 @@ -# Contributing - -This guide provides instructions for contributing to this Capacitor plugin. - -## Developing - -### Local Setup - -1. Fork and clone the repo. -1. Install the dependencies. - - ```shell - npm install - ``` - -1. Install SwiftLint if you're on macOS. - - ```shell - brew install swiftlint - ``` - -### Scripts - -#### `npm run build` - -Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen). - -It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported. - -Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`. - -#### `npm run verify` - -Build and validate the web and native projects. - -This is useful to run in CI to verify that the plugin builds for all platforms. - -#### `npm run lint` / `npm run fmt` - -Check formatting and code quality, autoformat/autofix if possible. - -This template is integrated with ESLint, Prettier, and SwiftLint. Using these tools is completely optional, but the [Capacitor Community](https://github.com/capacitor-community/) strives to have consistent code style and structure for easier cooperation. From 4b82a664ed5263b668976a0008cef136afb35eb6 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 16:30:41 -0500 Subject: [PATCH 31/58] Update camera/package.json Co-authored-by: Dan Imhoff --- camera/package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/camera/package.json b/camera/package.json index 63ce23241..64b4ddaee 100644 --- a/camera/package.json +++ b/camera/package.json @@ -5,6 +5,13 @@ "main": "dist/esm/index.js", "types": "dist/esm/index.d.ts", "unpkg": "dist/plugin.js", + "files": [ + "android/src/main/", + "android/build.gradle", + "dist/", + "ios/Plugin/", + "CapacitorCamera.podspec" + ], "author": "Ionic ", "license": "MIT", "repository": { From ca49f3ce6366faf6fd5e8028049928c65fb36e5e Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 16:31:18 -0500 Subject: [PATCH 32/58] Update camera/package.json Co-authored-by: Dan Imhoff --- camera/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/camera/package.json b/camera/package.json index 64b4ddaee..432d7ea75 100644 --- a/camera/package.json +++ b/camera/package.json @@ -73,5 +73,8 @@ "android": { "src": "android" } + }, + "publishConfig": { + "access": "public" } } From a9c2028e26ade9569be220df1dda2e14d93f410f Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 17:18:10 -0500 Subject: [PATCH 33/58] Updating gradle file to use version variables. --- camera/android/build.gradle | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/camera/android/build.gradle b/camera/android/build.gradle index eba258f74..4610e7ccb 100644 --- a/camera/android/build.gradle +++ b/camera/android/build.gradle @@ -1,7 +1,10 @@ ext { junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' - androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.2.0' + androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.1.0' androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' + androidxExifVersion = project.hasProperty('androidxExifVersion') ? rootProject.ext.androidxExifVersion : '1.2.0' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' androidxMaterialVersion = project.hasProperty('androidxMaterialVersion') ? rootProject.ext.androidxMaterialVersion : '1.1.0-rc02' } @@ -51,9 +54,9 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':capacitor-android') - implementation 'androidx.exifinterface:exifinterface:1.2.0' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' + implementation 'androidx.exifinterface:exifinterface:$androidxExifVersion' + implementation 'androidx.appcompat:appcompat:$androidxAppCompatVersion' + implementation 'androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion' implementation "com.google.android.material:material:$androidxMaterialVersion" testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" From 7a791f6b9d78c61d81a939e8fa91da12a35feb32 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 17:19:02 -0500 Subject: [PATCH 34/58] Fix error handling --- camera/src/web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/src/web.ts b/camera/src/web.ts index f3eed0de4..c7693b2c9 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -1,4 +1,4 @@ -import { WebPlugin } from '@capacitor/core'; +import { WebPlugin, CapacitorException } from '@capacitor/core'; import type { CameraPlugin, @@ -23,9 +23,9 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const photo = e.detail; if (photo === null) { - reject('User cancelled photos app'); + reject(new CapacitorException('User cancelled photos app')); } else if (photo instanceof Error) { - reject(photo.message); + reject(photo); } else { resolve(await this._getCameraPhoto(photo, options)); } From 4674a341723df1f815efcfaa47c533ff3768c8d7 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 17:20:11 -0500 Subject: [PATCH 35/58] Updating JSDoc comments --- camera/README.md | 52 ++++++++++++++++++------------------- camera/src/definitions.ts | 54 +++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/camera/README.md b/camera/README.md index f6369aa70..02654d93a 100644 --- a/camera/README.md +++ b/camera/README.md @@ -82,36 +82,36 @@ Request camera and photo album permissions #### CameraPhoto -| Prop | Type | Description | -| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | -| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | -| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | -| **`exif`** | any | Exif data, if any, retrieved from the image | -| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | +| Prop | Type | Description | Since | +| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | 1.0.0 | +| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | 1.0.0 | +| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | 1.0.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | 1.0.0 | +| **`exif`** | any | Exif data, if any, retrieved from the image | 1.0.0 | +| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | 1.0.0 | #### CameraOptions -| Prop | Type | Description | -| ------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`quality`** | number | The quality of image to return as JPEG, from 0-100 | -| **`allowEditing`** | boolean | Whether to allow the user to crop or make small edits (platform specific) | -| **`resultType`** | "uri" \| "base64" \| "dataUrl" | How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported | -| **`saveToGallery`** | boolean | Whether to save the photo to the gallery. If the photo was picked from the gallery, it will only be saved if edited. Default: false | -| **`width`** | number | The width of the saved image | -| **`height`** | number | The height of the saved image | -| **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. Default: false | -| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode Default: true | -| **`source`** | "camera" \| "photos" \| "prompt" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. Default: CameraSource.Prompt | -| **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. Default: CameraDirection.Rear | -| **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. Defaults to fullscreen. | -| **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | -| **`promptLabelHeader`** | string | If use CameraSource.Prompt only, can change Prompt label. default: promptLabelHeader : 'Photo' // iOS only promptLabelCancel : 'Cancel' // iOS only promptLabelPhoto : 'From Photos' promptLabelPicture : 'Take Picture' | -| **`promptLabelCancel`** | string | | -| **`promptLabelPhoto`** | string | | -| **`promptLabelPicture`** | string | | +| Prop | Type | Description | Default | Since | +| ------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----- | +| **`quality`** | number | The quality of image to return as JPEG, from 0-100 | | 1.0.0 | +| **`allowEditing`** | boolean | Whether to allow the user to crop or make small edits (platform specific) | | 1.0.0 | +| **`resultType`** | "uri" \| "base64" \| "dataUrl" | How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported | | 1.0.0 | +| **`saveToGallery`** | boolean | Whether to save the photo to the gallery. If the photo was picked from the gallery, it will only be saved if edited. | : false | 1.0.0 | +| **`width`** | number | The width of the saved image | | 1.0.0 | +| **`height`** | number | The height of the saved image | | 1.0.0 | +| **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. | : false | 1.0.0 | +| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode | : true | | +| **`source`** | "camera" \| "photos" \| "prompt" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. | : CameraSource.prompt | 1.0.0 | +| **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. | : CameraDirection.rear | 1.0.0 | +| **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. Defaults to fullscreen. | | 1.0.0 | +| **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | | 1.0.0 | +| **`promptLabelHeader`** | string | If use CameraSource.Prompt only, can change Prompt label. default: promptLabelHeader : 'Photo' // iOS only promptLabelCancel : 'Cancel' // iOS only promptLabelPhoto : 'From Photos' promptLabelPicture : 'Take Picture' | | 1.0.0 | +| **`promptLabelCancel`** | string | | | | +| **`promptLabelPhoto`** | string | | | | +| **`promptLabelPicture`** | string | | | | #### CameraPermissionStatus diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index 3a1300537..5fa711393 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -1,11 +1,3 @@ -import {} from '@capacitor/core'; - -declare module '@capacitor/core' { - interface PluginRegistry { - Camera: CameraPlugin; - } -} - export type CameraPermissionState = PermissionState | 'limited'; export type CameraPermissionType = 'camera' | 'photos'; @@ -48,28 +40,40 @@ export interface CameraPlugin { export interface CameraOptions { /** * The quality of image to return as JPEG, from 0-100 + * + * @since 1.0.0 */ quality?: number; /** * Whether to allow the user to crop or make small edits (platform specific) + * + * @since 1.0.0 */ allowEditing?: boolean; /** * How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported + * + * @since 1.0.0 */ resultType: CameraResultType; /** * Whether to save the photo to the gallery. * If the photo was picked from the gallery, it will only be saved if edited. - * Default: false + * @default: false + * + * @since 1.0.0 */ saveToGallery?: boolean; /** * The width of the saved image + * + * @since 1.0.0 */ width?: number; /** * The height of the saved image + * + * @since 1.0.0 */ height?: number; /** @@ -81,29 +85,37 @@ export interface CameraOptions { * * A future major version will change this behavior to be default, * and may also remove this option altogether. - * Default: false + * @default: false + * + * @since 1.0.0 */ preserveAspectRatio?: boolean; /** * Whether to automatically rotate the image "up" to correct for orientation * in portrait mode - * Default: true + * @default: true */ correctOrientation?: boolean; /** * The source to get the photo from. By default this prompts the user to select * either the photo album or take a photo. - * Default: CameraSource.Prompt + * @default: CameraSource.prompt + * + * @since 1.0.0 */ source?: CameraSource; /** * iOS and Web only: The camera direction. - * Default: CameraDirection.Rear + * @default: CameraDirection.rear + * + * @since 1.0.0 */ direction?: CameraDirection; /** * iOS only: The presentation style of the Camera. Defaults to fullscreen. + * + * @since 1.0.0 */ presentationStyle?: 'fullscreen' | 'popover'; @@ -113,6 +125,8 @@ export interface CameraOptions { * To always use file input, set this to `true`. * * Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements + * + * @since 1.0.0 */ webUseInput?: boolean; @@ -123,6 +137,8 @@ export interface CameraOptions { * promptLabelCancel : 'Cancel' // iOS only * promptLabelPhoto : 'From Photos' * promptLabelPicture : 'Take Picture' + * + * @since 1.0.0 */ promptLabelHeader?: string; promptLabelCancel?: string; @@ -133,24 +149,34 @@ export interface CameraOptions { export interface CameraPhoto { /** * The base64 encoded string representation of the image, if using CameraResultType.Base64. + * + * @since 1.0.0 */ base64String?: string; /** * The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. + * + * @since 1.0.0 */ dataUrl?: string; /** * If using CameraResultType.Uri, the path will contain a full, * platform-specific file URL that can be read later using the Filsystem API. + * + * @since 1.0.0 */ path?: string; /** * webPath returns a path that can be used to set the src attribute of an image for efficient * loading and rendering. + * + * @since 1.0.0 */ webPath?: string; /** * Exif data, if any, retrieved from the image + * + * @since 1.0.0 */ exif?: any; /** @@ -158,6 +184,8 @@ export interface CameraPhoto { * * iOS and Android only support jpeg. * Web supports jpeg and png. gif is only supported if using file input. + * + * @since 1.0.0 */ format: string; } From 85bc00945ae58fb9e6c3637357532f52811c305d Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 17:29:48 -0500 Subject: [PATCH 36/58] Removing unused file. --- .../main/java/com/capacitorjs/plugins/camera/Camera.java | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java deleted file mode 100644 index efc0918dc..000000000 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/Camera.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.capacitorjs.plugins.camera; - -public class Camera { - - public String echo(String value) { - return value; - } -} From 9d575d9d80f6bce7b28913d5ba3c0d98d42d7a4d Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Thu, 3 Dec 2020 17:30:01 -0500 Subject: [PATCH 37/58] Fixing build.gradle file. --- camera/android/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/camera/android/build.gradle b/camera/android/build.gradle index 4610e7ccb..2aa989cce 100644 --- a/camera/android/build.gradle +++ b/camera/android/build.gradle @@ -3,7 +3,7 @@ ext { androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.2.0' androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.1.0' androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' - androidxExifVersion = project.hasProperty('androidxExifVersion') ? rootProject.ext.androidxExifVersion : '1.2.0' + androidxExifInterfaceVersion = project.hasProperty('androidxExifInterfaceVersion') ? rootProject.ext.androidxExifInterfaceVersion : '1.2.0' androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' androidxMaterialVersion = project.hasProperty('androidxMaterialVersion') ? rootProject.ext.androidxMaterialVersion : '1.1.0-rc02' } @@ -54,9 +54,9 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':capacitor-android') - implementation 'androidx.exifinterface:exifinterface:$androidxExifVersion' - implementation 'androidx.appcompat:appcompat:$androidxAppCompatVersion' - implementation 'androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion' + implementation "androidx.exifinterface:exifinterface:$androidxExifInterfaceVersion" + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "com.google.android.material:material:$androidxMaterialVersion" testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" From 869f4121b7384a977030ef1ad52bf6eed55f3b51 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 15:45:52 -0500 Subject: [PATCH 38/58] Removing unneeded dependency. --- camera/android/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/camera/android/build.gradle b/camera/android/build.gradle index 2aa989cce..825e35bfa 100644 --- a/camera/android/build.gradle +++ b/camera/android/build.gradle @@ -1,7 +1,6 @@ ext { junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.2.0' - androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.1.0' androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' androidxExifInterfaceVersion = project.hasProperty('androidxExifInterfaceVersion') ? rootProject.ext.androidxExifInterfaceVersion : '1.2.0' androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' @@ -56,7 +55,6 @@ dependencies { implementation project(':capacitor-android') implementation "androidx.exifinterface:exifinterface:$androidxExifInterfaceVersion" implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" - implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "com.google.android.material:material:$androidxMaterialVersion" testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" From dda75aca840ef60aced58255620004fd92e0876e Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 15:46:07 -0500 Subject: [PATCH 39/58] Updating package.json --- camera/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/package.json b/camera/package.json index 432d7ea75..d9ddc8aef 100644 --- a/camera/package.json +++ b/camera/package.json @@ -51,15 +51,15 @@ "@ionic/prettier-config": "^1.0.1", "@ionic/swiftlint-config": "^1.1.2", "eslint": "^7.11.0", - "prettier": "^2.1.2", - "prettier-plugin-java": "^0.8.3", + "prettier": "~2.2.0", + "prettier-plugin-java": "~1.0.0", "rimraf": "^3.0.2", "rollup": "^2.32.0", "swiftlint": "^1.0.1", "typescript": "~4.0.3" }, "peerDependencies": { - "@capacitor/core": "next" + "@capacitor/core": "^3.0.0-alpha.7" }, "prettier": "@ionic/prettier-config", "swiftlint": "@ionic/swiftlint-config", From 1031f56c428c7c64d95a2befd1a998221867653a Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 15:47:16 -0500 Subject: [PATCH 40/58] Removing Xcode 11 support --- camera/ios/Plugin/CameraPlugin.swift | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 9934f4837..83de8140e 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -30,15 +30,11 @@ public class CameraPlugin: CAPPlugin { case .camera: state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState case .photos: - #if swift(>=5.3) if #available(iOS 14, *) { state = PHPhotoLibrary.authorizationStatus(for: .readWrite).authorizationState } else { state = PHPhotoLibrary.authorizationStatus().authorizationState } - #else - state = PHPhotoLibrary.authorizationStatus().authorizationState - #endif } result[permission.rawValue] = state } @@ -67,7 +63,6 @@ public class CameraPlugin: CAPPlugin { } case .photos: group.enter() - #if swift(>=5.3) if #available(iOS 14, *) { PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in result[permission.rawValue] = status.authorizationState @@ -79,12 +74,6 @@ public class CameraPlugin: CAPPlugin { group.leave() }) } - #else - PHPhotoLibrary.requestAuthorization({ (status) in - result[permission.rawValue] = status.authorizationState - group.leave() - }) - #endif } } group.notify(queue: DispatchQueue.main) { @@ -183,7 +172,6 @@ extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerD } } -#if swift(>=5.3) @available(iOS 14, *) extension CameraPlugin: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { @@ -212,7 +200,6 @@ extension CameraPlugin: PHPickerViewControllerDelegate { } } } -#endif private extension CameraPlugin { func returnProcessedImage(_ processedImage: ProcessedImage) { @@ -336,15 +323,11 @@ private extension CameraPlugin { } func presentSystemAppropriateImagePicker() { - #if swift(>=5.3) if #available(iOS 14, *) { presentPhotoPicker() } else { presentImagePicker() } - #else - presentImagePicker() - #endif } func presentImagePicker() { @@ -362,7 +345,6 @@ private extension CameraPlugin { bridge?.viewController?.present(picker, animated: true, completion: nil) } - #if swift(>=5.3) @available(iOS 14, *) func presentPhotoPicker() { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) @@ -378,7 +360,6 @@ private extension CameraPlugin { } bridge?.viewController?.present(picker, animated: true, completion: nil) } - #endif func saveTemporaryImage(_ data: Data) throws -> URL { var url: URL From bbdafb00877185570f5511f8d4c6518d1bfefdcb Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 17:30:10 -0500 Subject: [PATCH 41/58] Fixing argument name, adding centering for alert. --- camera/ios/Plugin/CameraPlugin.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 83de8140e..91e4be262 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -43,7 +43,7 @@ public class CameraPlugin: CAPPlugin { @objc override public func requestPermissions(_ call: CAPPluginCall) { // get the list of desired types, if passed - let typeList = call.getArray("types", String.self)?.compactMap({ (type) -> CameraPermissionType? in + let typeList = call.getArray("permissions", String.self)?.compactMap({ (type) -> CameraPermissionType? in return CameraPermissionType(rawValue: type) }) ?? [] // otherwise check everything @@ -249,7 +249,7 @@ private extension CameraPlugin { alert.addAction(UIAlertAction(title: settings.userPromptText.cancelAction, style: .cancel, handler: { [weak self] (_: UIAlertAction) in self?.call?.reject("User cancelled photos app") })) - + self.setCenteredPopover(alert) self.bridge?.viewController?.present(alert, animated: true, completion: nil) } From 4ef3fea6d4550e6f56cb62f084fdda4ce7b7ec06 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 17:30:48 -0500 Subject: [PATCH 42/58] Defaulting photo permissions to `granted` on the web. --- camera/src/web.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camera/src/web.ts b/camera/src/web.ts index c7693b2c9..b74df8e9c 100644 --- a/camera/src/web.ts +++ b/camera/src/web.ts @@ -164,7 +164,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); return { camera: permission.state, - photos: 'denied', + photos: 'granted', }; } catch { throw this.unavailable( From 0cc83106539c6aef5aa172f509d433d7dfb4d9f1 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 17:31:38 -0500 Subject: [PATCH 43/58] Making arguments optional on requestPermissions. --- camera/src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index 5fa711393..18f970274 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -33,7 +33,7 @@ export interface CameraPlugin { * @since 1.0.0 */ requestPermissions( - permissions: CameraPluginPermissions | null, + permissions?: CameraPluginPermissions, ): Promise; } From 835e646ed38d15a54ea319a39d1267ae3c3551a5 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 17:31:57 -0500 Subject: [PATCH 44/58] Updating doc comments. --- camera/README.md | 12 ++++++------ camera/src/definitions.ts | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/camera/README.md b/camera/README.md index 02654d93a..9f0809ff8 100644 --- a/camera/README.md +++ b/camera/README.md @@ -61,14 +61,14 @@ Check camera and photo album permissions ### requestPermissions(...) ```typescript -requestPermissions(permissions: CameraPluginPermissions | null) => Promise +requestPermissions(permissions?: CameraPluginPermissions | undefined) => Promise ``` Request camera and photo album permissions -| Param | Type | -| ----------------- | ----------------------------------------------------------------------------------- | -| **`permissions`** | CameraPluginPermissions \| null | +| Param | Type | +| ----------------- | --------------------------------------------------------------------------- | +| **`permissions`** | CameraPluginPermissions | **Returns:** Promise<CameraPermissionStatus> @@ -103,10 +103,10 @@ Request camera and photo album permissions | **`width`** | number | The width of the saved image | | 1.0.0 | | **`height`** | number | The height of the saved image | | 1.0.0 | | **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. | : false | 1.0.0 | -| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode | : true | | +| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode | : true | 1.0.0 | | **`source`** | "camera" \| "photos" \| "prompt" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. | : CameraSource.prompt | 1.0.0 | | **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. | : CameraDirection.rear | 1.0.0 | -| **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. Defaults to fullscreen. | | 1.0.0 | +| **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. | : 'fullscreen' | 1.0.0 | | **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | | 1.0.0 | | **`promptLabelHeader`** | string | If use CameraSource.Prompt only, can change Prompt label. default: promptLabelHeader : 'Photo' // iOS only promptLabelCancel : 'Cancel' // iOS only promptLabelPhoto : 'From Photos' promptLabelPicture : 'Take Picture' | | 1.0.0 | | **`promptLabelCancel`** | string | | | | diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index 18f970274..a4d14f5eb 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -94,6 +94,8 @@ export interface CameraOptions { * Whether to automatically rotate the image "up" to correct for orientation * in portrait mode * @default: true + * + * @since 1.0.0 */ correctOrientation?: boolean; /** @@ -113,7 +115,8 @@ export interface CameraOptions { direction?: CameraDirection; /** - * iOS only: The presentation style of the Camera. Defaults to fullscreen. + * iOS only: The presentation style of the Camera. + * @default: 'fullscreen' * * @since 1.0.0 */ From 4b5f1c0461259eaed185a7e1a94b25e2ae23cbae Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 4 Dec 2020 17:32:15 -0500 Subject: [PATCH 45/58] Updated deprecated call for bottom sheet. --- .../plugins/camera/CameraBottomSheetDialogFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java index de2fff134..254f1360e 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraBottomSheetDialogFragment.java @@ -116,7 +116,7 @@ public void setupDialog(Dialog dialog, int style) { CoordinatorLayout.Behavior behavior = params.getBehavior(); if (behavior != null && behavior instanceof BottomSheetBehavior) { - ((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback); + ((BottomSheetBehavior) behavior).addBottomSheetCallback(mBottomSheetBehaviorCallback); } } } From c6578519640aea017052a38af01e57c198e10b92 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Mon, 7 Dec 2020 11:48:14 -0500 Subject: [PATCH 46/58] Updating documentation comments. --- camera/src/definitions.ts | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index a4d14f5eb..b88cf08cb 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -134,18 +134,40 @@ export interface CameraOptions { webUseInput?: boolean; /** - * If use CameraSource.Prompt only, can change Prompt label. - * default: - * promptLabelHeader : 'Photo' // iOS only - * promptLabelCancel : 'Cancel' // iOS only - * promptLabelPhoto : 'From Photos' - * promptLabelPicture : 'Take Picture' + * Text value to use when displaying the prompt. + * iOS only: The title of the action sheet. + * @default: 'Photo' * * @since 1.0.0 + * */ promptLabelHeader?: string; + + /** + * Text value to use when displaying the prompt. + * iOS only: The label of the 'cancel' button. + * @default: 'Cancel' + * + * @since 1.0.0 + */ promptLabelCancel?: string; + + /** + * Text value to use when displaying the prompt. + * The label of the button to select a saved image. + * @default: 'From Photos' + * + * @since 1.0.0 + */ promptLabelPhoto?: string; + + /** + * Text value to use when displaying the prompt. + * The label of the button to open the camera. + * @default: 'Take Picture' + * + * @since 1.0.0 + */ promptLabelPicture?: string; } From aca99a85ce2493ca0d6468e008e11565605423b9 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Mon, 7 Dec 2020 11:57:13 -0500 Subject: [PATCH 47/58] Fmt --- camera/src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index b88cf08cb..3f1d3100d 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -94,7 +94,7 @@ export interface CameraOptions { * Whether to automatically rotate the image "up" to correct for orientation * in portrait mode * @default: true - * + * * @since 1.0.0 */ correctOrientation?: boolean; From 97dae9be5bfdc1f10c994fa7163377e83ab446da Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Mon, 7 Dec 2020 12:02:52 -0500 Subject: [PATCH 48/58] Update camera/ios/Plugin/CameraPlugin.swift Co-authored-by: jcesarmobile --- camera/ios/Plugin/CameraPlugin.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 91e4be262..585796a97 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -3,15 +3,6 @@ import Capacitor import Photos import PhotosUI -/* - Runtime detection of iOS 14 with @available and #available is straightforward but that code will fail to compile - under Xcode 11. So we need to use statements in the form of - - #if swift(>=5.3) - #endif - - as a poor proxy for Xcode 12 detection. The conditionals should be removed once Xcode 12 is required. - */ @objc(CAPCameraPlugin) public class CameraPlugin: CAPPlugin { From 582bda703ebf5dd44e3c4b1559b46033b3907c28 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Mon, 7 Dec 2020 13:53:11 -0500 Subject: [PATCH 49/58] Fix for conflicting mutations of a captured variable, incorrect result on requestPermissions. --- camera/ios/Plugin/CameraPlugin.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 585796a97..8aa36c1aa 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -3,7 +3,6 @@ import Capacitor import Photos import PhotosUI - @objc(CAPCameraPlugin) public class CameraPlugin: CAPPlugin { private var call: CAPPluginCall? @@ -40,35 +39,29 @@ public class CameraPlugin: CAPPlugin { // otherwise check everything let permissions: [CameraPermissionType] = (typeList.count > 0) ? typeList : CameraPermissionType.allCases // request the permissions - var result: [String: Any] = [:] let group = DispatchGroup() for permission in permissions { switch permission { case .camera: group.enter() AVCaptureDevice.requestAccess(for: .video) { granted in - result[permission.rawValue] = granted ? - AVAuthorizationStatus.authorized.authorizationState : - AVAuthorizationStatus.denied.authorizationState group.leave() } case .photos: group.enter() if #available(iOS 14, *) { PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in - result[permission.rawValue] = status.authorizationState group.leave() } } else { PHPhotoLibrary.requestAuthorization({ (status) in - result[permission.rawValue] = status.authorizationState group.leave() }) } } } - group.notify(queue: DispatchQueue.main) { - call.resolve(result) + group.notify(queue: DispatchQueue.main) { [weak self] in + self?.checkPermissions(call) } } From 5086434d59033371826f77c91ee6e2933423f8aa Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 8 Dec 2020 10:37:37 -0500 Subject: [PATCH 50/58] Updated to support permission override (requires latest capacitor code). --- .../plugins/camera/CameraPlugin.java | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index 77fb6f6fd..e67248b05 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -10,6 +10,8 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; + +import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import com.getcapacitor.FileUtils; import com.getcapacitor.JSObject; @@ -134,24 +136,34 @@ private void showPhotos(final PluginCall call) { } private boolean checkCameraPermissions(PluginCall call) { + // if the manifest does not contain the camera permissions key, we don't need to ask the user + boolean needCameraPerms = hasDefinedPermissions(new String[] { Manifest.permission.CAMERA }); + boolean hasCameraPerms = !needCameraPerms || hasPermission(Manifest.permission.CAMERA); + boolean hasPhotoPerms = hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + // If we want to save to the gallery, we need two permissions - if ( - settings.isSaveToGallery() && - !(hasPermission(Manifest.permission.CAMERA) && hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) - ) { - requestPermissions( - call, - new String[] { - Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE - }, - CAMERA_REQUEST_PERMISSIONS - ); + if (settings.isSaveToGallery() && !(hasCameraPerms && hasPhotoPerms)) { + if (needCameraPerms) { + requestPermissions( + call, + new String[] { + Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + }, + CAMERA_REQUEST_PERMISSIONS + ); + } else { + requestPermissions( + call, + new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, + CAMERA_REQUEST_PERMISSIONS + ); + } return false; } // If we don't need to save to the gallery, we can just ask for camera permissions - else if (!hasPermission(Manifest.permission.CAMERA)) { + else if (!hasCameraPerms) { requestPermission(call, Manifest.permission.CAMERA, CAMERA_REQUEST_PERMISSIONS); return false; } @@ -443,6 +455,22 @@ private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStre call.resolve(data); } + @Nullable + @Override + protected String overrideStateForPermission(Permission permission) { + if (permission.alias().equals("camera")) { + // If the camera permission is defined in the manifest, then we have to prompt the user + // or else we will get a security exception when trying to present the camera. If, however, + // it is not defined in the manifest then we don't need to prompt and it will just work. + if (hasDefinedPermissions(new Permission[]{ permission })) { + return null; + } else { + return "granted"; + } + } + return super.overrideStateForPermission(permission); + } + @Override protected void onRequestPermissionsResult(PluginCall savedCall, int requestCode, String[] permissions, int[] grantResults) { Logger.debug(getLogTag(), "handling request perms result"); From 3b7600bd4f5b38e4456a2186de98a0b60a12be41 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 9 Dec 2020 14:26:45 -0500 Subject: [PATCH 51/58] Fmt --- .../plugins/camera/CameraPlugin.java | 23 +++++++++---------- camera/ios/Plugin/CameraPlugin.swift | 6 ++--- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index e67248b05..f731e234a 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -10,7 +10,6 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; - import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import com.getcapacitor.FileUtils; @@ -145,19 +144,19 @@ private boolean checkCameraPermissions(PluginCall call) { if (settings.isSaveToGallery() && !(hasCameraPerms && hasPhotoPerms)) { if (needCameraPerms) { requestPermissions( - call, - new String[] { - Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE - }, - CAMERA_REQUEST_PERMISSIONS + call, + new String[] { + Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + }, + CAMERA_REQUEST_PERMISSIONS ); } else { requestPermissions( - call, - new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, - CAMERA_REQUEST_PERMISSIONS + call, + new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, + CAMERA_REQUEST_PERMISSIONS ); } return false; @@ -462,7 +461,7 @@ protected String overrideStateForPermission(Permission permission) { // If the camera permission is defined in the manifest, then we have to prompt the user // or else we will get a security exception when trying to present the camera. If, however, // it is not defined in the manifest then we don't need to prompt and it will just work. - if (hasDefinedPermissions(new Permission[]{ permission })) { + if (hasDefinedPermissions(new Permission[] { permission })) { return null; } else { return "granted"; diff --git a/camera/ios/Plugin/CameraPlugin.swift b/camera/ios/Plugin/CameraPlugin.swift index 8aa36c1aa..44196472b 100644 --- a/camera/ios/Plugin/CameraPlugin.swift +++ b/camera/ios/Plugin/CameraPlugin.swift @@ -44,17 +44,17 @@ public class CameraPlugin: CAPPlugin { switch permission { case .camera: group.enter() - AVCaptureDevice.requestAccess(for: .video) { granted in + AVCaptureDevice.requestAccess(for: .video) { _ in group.leave() } case .photos: group.enter() if #available(iOS 14, *) { - PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in + PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in group.leave() } } else { - PHPhotoLibrary.requestAuthorization({ (status) in + PHPhotoLibrary.requestAuthorization({ (_) in group.leave() }) } From 1351a8d5054e66758bd9619ee883a43d471973dd Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 11 Dec 2020 18:16:14 -0500 Subject: [PATCH 52/58] Updated readme --- camera/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/camera/README.md b/camera/README.md index 9f0809ff8..469c26253 100644 --- a/camera/README.md +++ b/camera/README.md @@ -108,10 +108,10 @@ Request camera and photo album permissions | **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. | : CameraDirection.rear | 1.0.0 | | **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. | : 'fullscreen' | 1.0.0 | | **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | | 1.0.0 | -| **`promptLabelHeader`** | string | If use CameraSource.Prompt only, can change Prompt label. default: promptLabelHeader : 'Photo' // iOS only promptLabelCancel : 'Cancel' // iOS only promptLabelPhoto : 'From Photos' promptLabelPicture : 'Take Picture' | | 1.0.0 | -| **`promptLabelCancel`** | string | | | | -| **`promptLabelPhoto`** | string | | | | -| **`promptLabelPicture`** | string | | | | +| **`promptLabelHeader`** | string | Text value to use when displaying the prompt. iOS only: The title of the action sheet. | : 'Photo' | 1.0.0 | +| **`promptLabelCancel`** | string | Text value to use when displaying the prompt. iOS only: The label of the 'cancel' button. | : 'Cancel' | 1.0.0 | +| **`promptLabelPhoto`** | string | Text value to use when displaying the prompt. The label of the button to select a saved image. | : 'From Photos' | 1.0.0 | +| **`promptLabelPicture`** | string | Text value to use when displaying the prompt. The label of the button to open the camera. | : 'Take Picture' | 1.0.0 | #### CameraPermissionStatus From b53456f068ba83ad0195e34a4de13f275b9ec306 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 11 Dec 2020 18:16:24 -0500 Subject: [PATCH 53/58] Update permissions handling --- .../plugins/camera/CameraPlugin.java | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index f731e234a..b815b6364 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -10,9 +10,9 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; -import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import com.getcapacitor.FileUtils; +import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; import com.getcapacitor.Logger; import com.getcapacitor.Plugin; @@ -21,6 +21,7 @@ import com.getcapacitor.PluginRequestCodes; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; +import org.json.JSONException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -454,20 +455,47 @@ private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStre call.resolve(data); } - @Nullable @Override - protected String overrideStateForPermission(Permission permission) { - if (permission.alias().equals("camera")) { - // If the camera permission is defined in the manifest, then we have to prompt the user - // or else we will get a security exception when trying to present the camera. If, however, - // it is not defined in the manifest then we don't need to prompt and it will just work. - if (hasDefinedPermissions(new Permission[] { permission })) { - return null; + @PluginMethod + public void requestPermissions(PluginCall call) { + // If the camera permission is defined in the manifest, then we have to prompt the user + // or else we will get a security exception when trying to present the camera. If, however, + // it is not defined in the manifest then we don't need to prompt and it will just work. + if (hasDefinedPermissions(new String[] { Manifest.permission.CAMERA })) { + // just request normally + super.requestPermissions(call); + } + else { + // the manifest does not define camera permissions, so we need to decide what to do + // first, extract the permissions being requested + JSArray providedPerms = call.getArray("permissions"); + List permsList = null; + try { + permsList = providedPerms.toList(); + } catch (JSONException e) { + } + + if (permsList != null && permsList.size() == 1 && permsList.contains("camera")) { + // the only thing being asked for was the camera so we can just return the current state + call.resolve(getPermissionStates()); } else { - return "granted"; + // we need to ask about photos so request storage permissions + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; + requestPermissions(call, perms, CameraPlugin.CAMERA_REQUEST_PERMISSIONS); } } - return super.overrideStateForPermission(permission); + } + + @Override + public JSObject getPermissionStates() { + JSObject permissionStates = super.getPermissionStates(); + + // If Camera is not in the manifest and therefore not required, say the permission is granted + if (!hasDefinedPermissions(new String[] { Manifest.permission.CAMERA })) { + permissionStates.put("camera", "granted"); + } + + return permissionStates; } @Override From fe0c3a0afbdb4a386a34873802562c28e179cb54 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Fri, 11 Dec 2020 18:17:04 -0500 Subject: [PATCH 54/58] Fmt --- .../com/capacitorjs/plugins/camera/CameraPlugin.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java index b815b6364..db31b918a 100644 --- a/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java +++ b/camera/android/src/main/java/com/capacitorjs/plugins/camera/CameraPlugin.java @@ -21,7 +21,6 @@ import com.getcapacitor.PluginRequestCodes; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; -import org.json.JSONException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -31,6 +30,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import org.json.JSONException; /** * The Camera plugin makes it easy to take a photo or have the user select a photo @@ -464,23 +464,21 @@ public void requestPermissions(PluginCall call) { if (hasDefinedPermissions(new String[] { Manifest.permission.CAMERA })) { // just request normally super.requestPermissions(call); - } - else { + } else { // the manifest does not define camera permissions, so we need to decide what to do // first, extract the permissions being requested JSArray providedPerms = call.getArray("permissions"); List permsList = null; try { permsList = providedPerms.toList(); - } catch (JSONException e) { - } + } catch (JSONException e) {} if (permsList != null && permsList.size() == 1 && permsList.contains("camera")) { // the only thing being asked for was the camera so we can just return the current state call.resolve(getPermissionStates()); } else { // we need to ask about photos so request storage permissions - String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; + String[] perms = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; requestPermissions(call, perms, CameraPlugin.CAMERA_REQUEST_PERMISSIONS); } } From e18c466d2679acbc2603f8614111b567d65dfbf4 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 15 Dec 2020 15:52:28 -0500 Subject: [PATCH 55/58] Updating to alpha 10 --- camera/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/camera/package.json b/camera/package.json index d9ddc8aef..eae66c7a6 100644 --- a/camera/package.json +++ b/camera/package.json @@ -43,10 +43,10 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "@capacitor/android": "^3.0.0-alpha.7", - "@capacitor/core": "^3.0.0-alpha.7", + "@capacitor/android": "^3.0.0-alpha.10", + "@capacitor/core": "^3.0.0-alpha.10", "@capacitor/docgen": "^0.0.10", - "@capacitor/ios": "^3.0.0-alpha.7", + "@capacitor/ios": "^3.0.0-alpha.10", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^1.0.1", "@ionic/swiftlint-config": "^1.1.2", @@ -59,7 +59,7 @@ "typescript": "~4.0.3" }, "peerDependencies": { - "@capacitor/core": "^3.0.0-alpha.7" + "@capacitor/core": "^3.0.0-alpha.10" }, "prettier": "@ionic/prettier-config", "swiftlint": "@ionic/swiftlint-config", From b123a616e9fa203843a614b0828b821f41eaf1b7 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Tue, 15 Dec 2020 16:14:49 -0500 Subject: [PATCH 56/58] Fixed alpha versions --- camera/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/package.json b/camera/package.json index eae66c7a6..83714e933 100644 --- a/camera/package.json +++ b/camera/package.json @@ -44,9 +44,9 @@ }, "devDependencies": { "@capacitor/android": "^3.0.0-alpha.10", - "@capacitor/core": "^3.0.0-alpha.10", + "@capacitor/core": "^3.0.0-alpha.9", "@capacitor/docgen": "^0.0.10", - "@capacitor/ios": "^3.0.0-alpha.10", + "@capacitor/ios": "^3.0.0-alpha.9", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^1.0.1", "@ionic/swiftlint-config": "^1.1.2", @@ -59,7 +59,7 @@ "typescript": "~4.0.3" }, "peerDependencies": { - "@capacitor/core": "^3.0.0-alpha.10" + "@capacitor/core": "^3.0.0-alpha.9" }, "prettier": "@ionic/prettier-config", "swiftlint": "@ionic/swiftlint-config", From e337b1a3d10dad88b0c73f38684fe16113ef4be8 Mon Sep 17 00:00:00 2001 From: Ian Keith Date: Wed, 16 Dec 2020 10:50:18 -0500 Subject: [PATCH 57/58] Imported PermissionState from @capacitor/core --- camera/src/definitions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/camera/src/definitions.ts b/camera/src/definitions.ts index 3f1d3100d..7c5906662 100644 --- a/camera/src/definitions.ts +++ b/camera/src/definitions.ts @@ -1,3 +1,5 @@ +import type { PermissionState } from '@capacitor/core'; + export type CameraPermissionState = PermissionState | 'limited'; export type CameraPermissionType = 'camera' | 'photos'; From b4cf552edca0d5240c17d69205755193c662240e Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 16 Dec 2020 14:02:50 -0800 Subject: [PATCH 58/58] docgen update --- camera/README.md | 94 ++++++++++++++++++++++++++++++--------------- camera/package.json | 8 ++-- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/camera/README.md b/camera/README.md index 469c26253..739db9f5f 100644 --- a/camera/README.md +++ b/camera/README.md @@ -17,6 +17,7 @@ npx cap sync * [`checkPermissions()`](#checkpermissions) * [`requestPermissions(...)`](#requestpermissions) * [Interfaces](#interfaces) +* [Type Aliases](#type-aliases) @@ -82,44 +83,44 @@ Request camera and photo album permissions #### CameraPhoto -| Prop | Type | Description | Since | -| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | 1.0.0 | -| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | 1.0.0 | -| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | 1.0.0 | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | 1.0.0 | -| **`exif`** | any | Exif data, if any, retrieved from the image | 1.0.0 | -| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | 1.0.0 | +| Prop | Type | Description | Since | +| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`base64String`** | string | The base64 encoded string representation of the image, if using CameraResultType.Base64. | 1.0.0 | +| **`dataUrl`** | string | The url starting with 'data:image/jpeg;base64,' and the base64 encoded string representation of the image, if using CameraResultType.DataUrl. | 1.0.0 | +| **`path`** | string | If using CameraResultType.Uri, the path will contain a full, platform-specific file URL that can be read later using the Filsystem API. | 1.0.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering. | 1.0.0 | +| **`exif`** | any | Exif data, if any, retrieved from the image | 1.0.0 | +| **`format`** | string | The format of the image, ex: jpeg, png, gif. iOS and Android only support jpeg. Web supports jpeg and png. gif is only supported if using file input. | 1.0.0 | #### CameraOptions -| Prop | Type | Description | Default | Since | -| ------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----- | -| **`quality`** | number | The quality of image to return as JPEG, from 0-100 | | 1.0.0 | -| **`allowEditing`** | boolean | Whether to allow the user to crop or make small edits (platform specific) | | 1.0.0 | -| **`resultType`** | "uri" \| "base64" \| "dataUrl" | How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported | | 1.0.0 | -| **`saveToGallery`** | boolean | Whether to save the photo to the gallery. If the photo was picked from the gallery, it will only be saved if edited. | : false | 1.0.0 | -| **`width`** | number | The width of the saved image | | 1.0.0 | -| **`height`** | number | The height of the saved image | | 1.0.0 | -| **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. | : false | 1.0.0 | -| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode | : true | 1.0.0 | -| **`source`** | "camera" \| "photos" \| "prompt" | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. | : CameraSource.prompt | 1.0.0 | -| **`direction`** | "rear" \| "front" | iOS and Web only: The camera direction. | : CameraDirection.rear | 1.0.0 | -| **`presentationStyle`** | "fullscreen" \| "popover" | iOS only: The presentation style of the Camera. | : 'fullscreen' | 1.0.0 | -| **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | | 1.0.0 | -| **`promptLabelHeader`** | string | Text value to use when displaying the prompt. iOS only: The title of the action sheet. | : 'Photo' | 1.0.0 | -| **`promptLabelCancel`** | string | Text value to use when displaying the prompt. iOS only: The label of the 'cancel' button. | : 'Cancel' | 1.0.0 | -| **`promptLabelPhoto`** | string | Text value to use when displaying the prompt. The label of the button to select a saved image. | : 'From Photos' | 1.0.0 | -| **`promptLabelPicture`** | string | Text value to use when displaying the prompt. The label of the button to open the camera. | : 'Take Picture' | 1.0.0 | +| Prop | Type | Description | Default | Since | +| ------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----- | +| **`quality`** | number | The quality of image to return as JPEG, from 0-100 | | 1.0.0 | +| **`allowEditing`** | boolean | Whether to allow the user to crop or make small edits (platform specific) | | 1.0.0 | +| **`resultType`** | CameraResultType | How the data should be returned. Currently, only 'Base64', 'DataUrl' or 'Uri' is supported | | 1.0.0 | +| **`saveToGallery`** | boolean | Whether to save the photo to the gallery. If the photo was picked from the gallery, it will only be saved if edited. | : false | 1.0.0 | +| **`width`** | number | The width of the saved image | | 1.0.0 | +| **`height`** | number | The height of the saved image | | 1.0.0 | +| **`preserveAspectRatio`** | boolean | Whether to preserve the aspect ratio of the image. If this flag is true, the width and height will be used as max values and the aspect ratio will be preserved. This is only relevant when both a width and height are passed. When only width or height is provided the aspect ratio is always preserved (and this option is a no-op). A future major version will change this behavior to be default, and may also remove this option altogether. | : false | 1.0.0 | +| **`correctOrientation`** | boolean | Whether to automatically rotate the image "up" to correct for orientation in portrait mode | : true | 1.0.0 | +| **`source`** | CameraSource | The source to get the photo from. By default this prompts the user to select either the photo album or take a photo. | : CameraSource.prompt | 1.0.0 | +| **`direction`** | CameraDirection | iOS and Web only: The camera direction. | : CameraDirection.rear | 1.0.0 | +| **`presentationStyle`** | 'fullscreen' \| 'popover' | iOS only: The presentation style of the Camera. | : 'fullscreen' | 1.0.0 | +| **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/pwa-elements | | 1.0.0 | +| **`promptLabelHeader`** | string | Text value to use when displaying the prompt. iOS only: The title of the action sheet. | : 'Photo' | 1.0.0 | +| **`promptLabelCancel`** | string | Text value to use when displaying the prompt. iOS only: The label of the 'cancel' button. | : 'Cancel' | 1.0.0 | +| **`promptLabelPhoto`** | string | Text value to use when displaying the prompt. The label of the button to select a saved image. | : 'From Photos' | 1.0.0 | +| **`promptLabelPicture`** | string | Text value to use when displaying the prompt. The label of the button to open the camera. | : 'Take Picture' | 1.0.0 | #### CameraPermissionStatus -| Prop | Type | -| ------------ | ---------------- | -| **`camera`** | any | -| **`photos`** | any | +| Prop | Type | +| ------------ | ----------------------------------------------------------------------- | +| **`camera`** | CameraPermissionState | +| **`photos`** | CameraPermissionState | #### CameraPluginPermissions @@ -128,4 +129,37 @@ Request camera and photo album permissions | ----------------- | ----------------------------------- | | **`permissions`** | CameraPermissionType[] | + +### Type Aliases + + +#### CameraResultType + +'uri' | 'base64' | 'dataUrl' + + +#### CameraSource + +'prompt' | 'camera' | 'photos' + + +#### CameraDirection + +'rear' | 'front' + + +#### CameraPermissionState + +PermissionState | 'limited' + + +#### PermissionState + +'prompt' | 'prompt-with-rationale' | 'granted' | 'denied' + + +#### CameraPermissionType + +'camera' | 'photos' + diff --git a/camera/package.json b/camera/package.json index 83714e933..73472a537 100644 --- a/camera/package.json +++ b/camera/package.json @@ -45,16 +45,16 @@ "devDependencies": { "@capacitor/android": "^3.0.0-alpha.10", "@capacitor/core": "^3.0.0-alpha.9", - "@capacitor/docgen": "^0.0.10", + "@capacitor/docgen": "0.0.14", "@capacitor/ios": "^3.0.0-alpha.9", "@ionic/eslint-config": "^0.3.0", - "@ionic/prettier-config": "^1.0.1", + "@ionic/prettier-config": "~1.0.1", "@ionic/swiftlint-config": "^1.1.2", "eslint": "^7.11.0", "prettier": "~2.2.0", "prettier-plugin-java": "~1.0.0", - "rimraf": "^3.0.2", - "rollup": "^2.32.0", + "rimraf": "^3.0.0", + "rollup": "^2.29.0", "swiftlint": "^1.0.1", "typescript": "~4.0.3" },