Skip to content

Commit

Permalink
android setup, single image handler
Browse files Browse the repository at this point in the history
android setup
  • Loading branch information
BrtqKr authored Mar 5, 2024
2 parents 46e18a6 + 0ffe479 commit 72f0fad
Show file tree
Hide file tree
Showing 17 changed files with 363 additions and 60 deletions.
10 changes: 10 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/send"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/money2020"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>

<meta-data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public List<NativeModule> createNativeModules(
List<NativeModule> modules = new ArrayList<>();

modules.add(new StartupTimer(reactContext));
modules.add(new ShareActionHandlerModule(reactContext));

return modules;
}
Expand Down
25 changes: 23 additions & 2 deletions android/app/src/main/java/com/expensify/chat/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package com.expensify.chat

import expo.modules.ReactActivityDelegateWrapper

import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsets
import com.expensify.chat.bootsplash.BootSplash
import com.expensify.chat.intentHandler.ImageIntentHandler
import com.expensify.chat.intentHandler.IntentHandlerFactory
import com.expensify.reactnativekeycommand.KeyCommandModule
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper

class MainActivity : ReactActivity() {
/**
Expand All @@ -34,6 +37,9 @@ class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
BootSplash.init(this)
super.onCreate(null)
Log.i("ImageIntentHandler", "On create")


if (resources.getBoolean(R.bool.portrait_only)) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
Expand All @@ -50,6 +56,21 @@ class MainActivity : ReactActivity() {
defaultInsets.systemWindowInsetBottom
)
}

if (intent != null) {
handleIntent(intent)
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // Must store the new intent unless getIntent() will return the old one
handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
val intentHandler = IntentHandlerFactory.getIntentHandler(this, intent.type)
intentHandler?.handle(intent)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class MainApplication : MultiDexApplication(), ReactApplication {
add(BootSplashPackage())
add(ExpensifyAppPackage())
add(RNTextInputResetPackage())
// add(ShareExtensionHandlerPackage())
}

override fun getJSMainModuleName() = ".expo/.virtual-metro-entry"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.expensify.chat

import android.content.Context
import android.util.Log
import com.expensify.chat.intentHandler.IntentHandlerConstants
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod

class ShareActionHandlerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "ShareActionHandlerModule"

@ReactMethod
fun processFiles(callback: Callback) {
try {
val sharedPreferences = reactApplicationContext.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE)
val fileSet = sharedPreferences.getStringSet(IntentHandlerConstants.fileArrayProperty, setOf())
val fileArray: ArrayList<String> = ArrayList(fileSet)

val resultArray = Arguments.createArray()
for (file in fileArray) {
resultArray.pushString(file)
}

callback.invoke(resultArray)
} catch (exception: Exception) {
Log.e("ImageIntentHandler", exception.toString())
callback.invoke(exception.toString(), null)
}
}

}
114 changes: 114 additions & 0 deletions android/app/src/main/java/com/expensify/chat/image/ImageUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.expensify.chat.image

import android.content.Context
import android.net.Uri
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream


object ImageUtils {
private const val tag = "ImageUtils"
private const val maxPictureWidth = 2400
private const val optimalPictureSize = maxPictureWidth * maxPictureWidth
private const val defaultImageExtension = ".jpg"

private fun getUniqueImageFilePrefix(): String {
return System.currentTimeMillis().toString()
}

/**
* Checks if external storage is available.
*
* @return true if external storage is available, mounted and writable, false otherwise.
*/
private fun isExternalStorageAvailable(): Boolean {
// Make sure the media storage is mounted (MEDIA_MOUNTED implies writable)
val state: String = Environment.getExternalStorageState()
if (state != Environment.MEDIA_MOUNTED) {
Log.w(tag, "External storage requested but not available" )
return false
}
return true
}

/**
* Synchronous method
*
* @param context
* @param imageUri
* @param destinationFile
* @throws IOException
*/
@Throws(IOException::class)
fun saveImageFromMediaProviderUri(imageUri: Uri?, destinationFile: File?, context: Context) {
val inputStream: InputStream? = imageUri?.let { context.contentResolver.openInputStream(it) }
val outputStream: OutputStream = FileOutputStream(destinationFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
}

/**
* Creates an image file into the internal or external storage.
*
* @param context
* @return
* @throws IOException
*/
@Throws(IOException::class)
fun createImageFile(context: Context): File {

// Create an image file name
val file: File = File.createTempFile(
getUniqueImageFilePrefix(),
defaultImageExtension,
getPhotoDirectory(context)
)
Log.i(tag, "Created a temporary file for the photo at" + file.absolutePath)
return file
}

/**
* Determines where the photo directory is based on if we can use external storage or not
*
* @param context
* @return File the directory
*/
private fun getPhotoDirectory(context: Context): File? {
val photoDirectory: File = if (isExternalStorageAvailable()) File(
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "Expensify"
) else File(context.filesDir.absolutePath, "Expensify")
if (!photoDirectory.exists()) {
photoDirectory.mkdir()
}
return photoDirectory
}

/**
* Copy the given Uri to storage
*
* @param uri
* @param context
* @return The absolute path of the image
*/
fun copyUriToStorage(uri: Uri?, context: Context): String? {
var resultingPath: String? = null
try {
val imageFile: File = createImageFile(context)
saveImageFromMediaProviderUri(uri, imageFile, context)
resultingPath = imageFile.absolutePath
Log.i("ImageIntentHandler", "save image$resultingPath")

} catch (ex: IOException) {
Log.e(tag, "Couldn't save image from intent", ex)
}
return resultingPath
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.expensify.chat.intentHandler

abstract class AbstractIntentHandler: IntentHandler {
override fun onCompleted() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.expensify.chat.intentHandler

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import com.expensify.chat.image.ImageUtils.copyUriToStorage


object IntentHandlerConstants {
const val preferencesFile = "shareActionHandler"
const val fileArrayProperty = "filePaths"
}


class ImageIntentHandler(private val context: Context) : AbstractIntentHandler() {
override fun handle(intent: Intent?): Boolean {
Log.i("ImageIntentHandler", "Handle intent" + intent.toString())
if (intent == null) {
return false
}

val action: String? = intent.action
val type: String = intent.type ?: return false

if(!type.startsWith("image/")) return false

when(action) {
Intent.ACTION_SEND -> {
Log.i("ImageIntentHandler", "Handle receive single image")
handleSingleImageIntent(intent, context)
onCompleted()
return true
}
Intent.ACTION_SEND_MULTIPLE -> {
handleMultipleImagesIntent(intent, context)
onCompleted()
return true
}
}
return false
}

private fun handleSingleImageIntent(intent: Intent, context: Context) {
(intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM))?.let { imageUri ->

Log.i("ImageIntentHandler", "handleSingleImageIntent$imageUri")
// Update UI to reflect image being shared
if (imageUri == null) {
return
}

val fileArrayList: ArrayList<String> = ArrayList()
val resultingPath: String? = copyUriToStorage(imageUri, context)
if (resultingPath != null) {
fileArrayList.add(resultingPath)
val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putStringSet(IntentHandlerConstants.fileArrayProperty, fileArrayList.toSet())
editor.apply()
}
}
}

private fun handleMultipleImagesIntent(intent: Intent, context: Context) {

val resultingImagePaths = ArrayList<String>()

(intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM))?.let { imageUris ->
for (imageUri in imageUris) {
val resultingPath: String? = copyUriToStorage(imageUri, context)
if (resultingPath != null) {
resultingImagePaths.add(resultingPath)
}
}
}
// Yapl.getInstance().callIntentCallback(resultingImagePaths)
}

override fun onCompleted() {
val uri: Uri = Uri.parse("new-expensify://share/root")
val deepLinkIntent = Intent(Intent.ACTION_VIEW, uri)
deepLinkIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(deepLinkIntent)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.expensify.chat.intentHandler

import android.content.Intent

interface IntentHandler {
fun handle(intent: Intent?): Boolean
fun onCompleted()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.expensify.chat.intentHandler

import android.content.Context

object IntentHandlerFactory {
fun getIntentHandler(context: Context, mimeType: String?): IntentHandler? {
if (mimeType == null) return null
return when {
mimeType.startsWith("image/") -> ImageIntentHandler(context)
// Add other cases like video/*, application/pdf etc.
else -> null
}
}
}
Loading

0 comments on commit 72f0fad

Please sign in to comment.