Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android setup #77

Merged
merged 4 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading