Share Extension/Intents (#2587)
* add native ios code outside of ios project * helper script * going to be a lot of these commits to squash...backing up * save * start of an expo plugin * create info.plist * copy the view controller * maybe working * working * wait working now * working plugin * use current scheme * update intent path * use better params * support text in uri * build * use better encoding * handle images * cleanup ios plugin * android * move bash script to /scripts * handle cases where loaded data is uiimage rather than uri * remove unnecessary logic, allow more than 4 images and just take first 4 * android build plugin * limit images to four on android * use js for plugins, no need to build * revert changes to app config * use correct scheme on android * android readme * move ios extension to /modules * remove unnecessary event * revert typo * plugin readme * scripts readme * add configurable scheme to .env, default to `bluesky` * remove debug * revert .gitignore change * add comment about updating .env to app.config.js for those modifying scheme * modify .env * update android module to use the proper url * update ios extension * remove comment * parse and validate incoming image uris * fix types * rm oops * fix a few typos
This commit is contained in:
parent
ac726497a4
commit
d451f82f54
27 changed files with 860 additions and 12 deletions
8
modules/expo-receive-android-intents/README.md
Normal file
8
modules/expo-receive-android-intents/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Expo Receive Android Intents
|
||||
|
||||
This module handles incoming intents on Android. Handled intents are `text/plain` and `image/*` (single or multiple).
|
||||
The module handles saving images to the app's filesystem for access within the app, limiting the selection of images
|
||||
to a max of four, and handling intent types. No JS code is required for this module, and it is no-op on non-android
|
||||
platforms.
|
||||
|
||||
No installation is required. Gradle will automatically add this module on build.
|
15
modules/expo-receive-android-intents/android/.gitignore
vendored
Normal file
15
modules/expo-receive-android-intents/android/.gitignore
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
92
modules/expo-receive-android-intents/android/build.gradle
Normal file
92
modules/expo-receive-android-intents/android/build.gradle
Normal file
|
@ -0,0 +1,92 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
group = 'xyz.blueskyweb.app.exporeceiveandroidintents'
|
||||
version = '0.4.1'
|
||||
|
||||
buildscript {
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
if (expoModulesCorePlugin.exists()) {
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
}
|
||||
|
||||
// Simple helper that allows the root project to override versions declared by this library.
|
||||
ext.safeExtGet = { prop, fallback ->
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
||||
// Ensures backward compatibility
|
||||
ext.getKotlinVersion = {
|
||||
if (ext.has("kotlinVersion")) {
|
||||
ext.kotlinVersion()
|
||||
} else {
|
||||
ext.safeExtGet("kotlinVersion", "1.8.10")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url = mavenLocal().url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 33)
|
||||
|
||||
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
|
||||
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.majorVersion
|
||||
}
|
||||
}
|
||||
|
||||
namespace "xyz.blueskyweb.app.exporeceiveandroidintents"
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
versionCode 1
|
||||
versionName "0.4.1"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
publishing {
|
||||
singleVariant("release") {
|
||||
withSourcesJar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':expo-modules-core')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<manifest>
|
||||
</manifest>
|
|
@ -0,0 +1,119 @@
|
|||
package xyz.blueskyweb.app.exporeceiveandroidintents
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.net.toUri
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URLEncoder
|
||||
|
||||
class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoReceiveAndroidIntents")
|
||||
|
||||
OnNewIntent {
|
||||
handleIntent(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
if(appContext.currentActivity == null || intent == null) return
|
||||
|
||||
if (intent.action == Intent.ACTION_SEND) {
|
||||
if (intent.type == "text/plain") {
|
||||
handleTextIntent(intent)
|
||||
} else if (intent.type.toString().startsWith("image/")) {
|
||||
handleImageIntent(intent)
|
||||
}
|
||||
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||
if (intent.type.toString().startsWith("image/")) {
|
||||
handleImagesIntent(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTextIntent(intent: Intent) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
val encoded = URLEncoder.encode(it, "UTF-8")
|
||||
"bluesky://intent/compose?text=${encoded}".toUri().let { uri ->
|
||||
val newIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||
appContext.currentActivity?.startActivity(newIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleImageIntent(intent: Intent) {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
if (uri == null) return
|
||||
|
||||
handleImageIntents(listOf(uri))
|
||||
}
|
||||
|
||||
private fun handleImagesIntent(intent: Intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.let {
|
||||
handleImageIntents(it.filterIsInstance<Uri>().take(4))
|
||||
}
|
||||
} else {
|
||||
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let {
|
||||
handleImageIntents(it.filterIsInstance<Uri>().take(4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleImageIntents(uris: List<Uri>) {
|
||||
var allParams = ""
|
||||
|
||||
uris.forEachIndexed { index, uri ->
|
||||
val info = getImageInfo(uri)
|
||||
val params = buildUriData(info)
|
||||
allParams = "${allParams}${params}"
|
||||
|
||||
if (index < uris.count() - 1) {
|
||||
allParams = "${allParams},"
|
||||
}
|
||||
}
|
||||
|
||||
val encoded = URLEncoder.encode(allParams, "UTF-8")
|
||||
|
||||
"bluesky://intent/compose?imageUris=${encoded}".toUri().let {
|
||||
val newIntent = Intent(Intent.ACTION_VIEW, it)
|
||||
appContext.currentActivity?.startActivity(newIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getImageInfo(uri: Uri): Map<String, Any> {
|
||||
val bitmap = MediaStore.Images.Media.getBitmap(appContext.currentActivity?.contentResolver, uri)
|
||||
// We have to save this so that we can access it later when uploading the image.
|
||||
// createTempFile will automatically place a unique string between "img" and "temp.jpeg"
|
||||
val file = File.createTempFile("img", "temp.jpeg", appContext.currentActivity?.cacheDir)
|
||||
val out = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
|
||||
return mapOf(
|
||||
"width" to bitmap.width,
|
||||
"height" to bitmap.height,
|
||||
"path" to file.path.toString()
|
||||
)
|
||||
}
|
||||
|
||||
// We will pas the width and height to the app here, since getting measurements
|
||||
// on the RN side is a bit more involved, and we already have them here anyway.
|
||||
private fun buildUriData(info: Map<String, Any>): String {
|
||||
val path = info.getValue("path")
|
||||
val width = info.getValue("width")
|
||||
val height = info.getValue("height")
|
||||
return "file://${path}|${width}|${height}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["xyz.blueskyweb.app.exporeceiveandroidintents.ExpoReceiveAndroidIntentsModule"]
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue