-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Share extension Android and iOS implementation #54354
base: main
Are you sure you want to change the base?
Changes from 87 commits
410748b
3cbb982
38ec094
b5e1641
7f82783
357b878
5871fa0
4562d4e
06ac232
3c6946c
f7d16af
7b13b38
b447164
47e83df
ee33b1e
3dd5657
f1b5f27
7dd0798
333b5a1
82579c1
ea0b0b8
7c3ee53
9cc6950
c73447f
a3022b9
ccb7120
caaac2e
83a92cc
b14584d
1b07248
a021fab
9cdeebb
de34abf
c639945
5f21f49
68db412
35325d6
286189a
4fd6101
b457bab
1930925
43d8f08
08c85e9
89bfbc4
7dd929f
b960843
c4b3d65
c354af4
1bbe686
ecca4b4
20cc5c1
ff3127e
5f6b097
a42a984
b4c7481
f04e90f
f5c754c
f4a9288
021a098
fd90d8a
71e3475
eb0590a
c67b558
2276bb6
f8a33c2
d68cf92
08c7040
11a8a52
05e21ae
9d1bf2d
8e60c27
bfe2682
769908d
8f309af
e23dc6e
f9ec294
10bacbf
4132cab
3770da6
6a61f98
5a048eb
ab547b5
b697ec8
9d50aad
813f5ac
869dc76
f74f3da
6ed8ad1
e4c96ec
776446e
b8ddb52
63bfe9e
55ef8fd
3bb13ca
b92ada3
5301ebd
bd4d916
3fa8535
a8af760
443f25e
693cc99
a77dbaa
e6f5ef1
4460ebf
f561b7e
1e5c644
72c4acc
8146a84
aab0c4d
a4f49f5
48fba1c
951a392
2594819
bc618ff
d649547
92a46e8
18defbf
a012e2b
12d4d7b
721e615
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package com.expensify.chat | ||
|
||
import android.content.Context | ||
import android.graphics.BitmapFactory | ||
import android.media.MediaMetadataRetriever | ||
import com.expensify.chat.intenthandler.IntentHandlerConstants | ||
import com.facebook.react.bridge.Callback | ||
import com.facebook.react.bridge.ReactApplicationContext | ||
import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
import android.util.Log | ||
import com.facebook.react.bridge.ReactMethod | ||
import org.json.JSONObject | ||
import java.io.File | ||
|
||
class ShareActionHandlerModule(reactContext: ReactApplicationContext) : | ||
ReactContextBaseJavaModule(reactContext) { | ||
|
||
override fun getName(): String { | ||
return "ShareActionHandlerModule" | ||
} | ||
|
||
@ReactMethod | ||
fun processFiles(callback: Callback) { | ||
try { | ||
val sharedPreferences = reactApplicationContext.getSharedPreferences( | ||
IntentHandlerConstants.preferencesFile, | ||
Context.MODE_PRIVATE | ||
) | ||
|
||
val shareObjectString = sharedPreferences.getString(IntentHandlerConstants.shareObjectProperty, null) | ||
if (shareObjectString == null) { | ||
callback.invoke("No data found", null) | ||
return | ||
} | ||
|
||
val shareObject = JSONObject(shareObjectString) | ||
val content = shareObject.optString("content") | ||
val mimeType = shareObject.optString("mimeType") | ||
val fileUriPath = "file://$content" | ||
val timestamp = System.currentTimeMillis() | ||
|
||
val file = File(content) | ||
if (!file.exists()) { | ||
val textObject = JSONObject().apply { | ||
put("id", "text") | ||
put("content", content) | ||
put("mimeType", "txt") | ||
put("processedAt", timestamp) | ||
} | ||
callback.invoke(textObject.toString()) | ||
return | ||
} | ||
|
||
val identifier = file.name | ||
var aspectRatio = 0.0f | ||
|
||
if (mimeType.startsWith("image/")) { | ||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } | ||
BitmapFactory.decodeFile(content, options) | ||
aspectRatio = if (options.outHeight != 0) options.outWidth.toFloat() / options.outHeight else 1.0f | ||
} else if (mimeType.startsWith("video/")) { | ||
val retriever = MediaMetadataRetriever() | ||
try { | ||
retriever.setDataSource(content) | ||
val videoWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toFloatOrNull() ?: 1f | ||
val videoHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toFloatOrNull() ?: 1f | ||
if (videoHeight != 0f) aspectRatio = videoWidth / videoHeight | ||
} catch (e: Exception) { | ||
Log.e("ShareActionHandlerModule", "Error retrieving video metadata: ${e.message}") | ||
} finally { | ||
retriever.release() | ||
} | ||
} | ||
|
||
val fileData = JSONObject().apply { | ||
put("id", identifier) | ||
put("content", fileUriPath) | ||
put("mimeType", mimeType) | ||
put("processedAt", timestamp) | ||
put("aspectRatio", aspectRatio) | ||
} | ||
|
||
callback.invoke(fileData.toString()) | ||
|
||
} catch (e: Exception) { | ||
callback.invoke(e.toString(), null) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.expensify.chat.intenthandler | ||
|
||
import android.content.Context | ||
import com.expensify.chat.utils.FileUtils.clearInternalStorageDirectory | ||
|
||
abstract class AbstractIntentHandler: IntentHandler { | ||
override fun onCompleted() {} | ||
|
||
protected fun clearTemporaryFiles(context: Context) { | ||
// Clear data present in the shared preferences | ||
val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) | ||
val editor = sharedPreferences.edit() | ||
editor.clear() | ||
editor.apply() | ||
|
||
// Clear leftover temporary files from previous share attempts | ||
clearInternalStorageDirectory(context) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package com.expensify.chat.intenthandler | ||
|
||
import android.content.Context | ||
import android.content.Intent | ||
import android.net.Uri | ||
import com.expensify.chat.utils.FileUtils | ||
|
||
class FileIntentHandler(private val context: Context) : AbstractIntentHandler() { | ||
override fun handle(intent: Intent): Boolean { | ||
super.clearTemporaryFiles(context) | ||
when(intent.action) { | ||
Intent.ACTION_SEND -> { | ||
handleSingleFileIntent(intent, context) | ||
onCompleted() | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
private fun handleSingleFileIntent(intent: Intent, context: Context) { | ||
(intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM))?.let { fileUri -> | ||
if (fileUri == null) { | ||
return | ||
} | ||
|
||
val resultingPath: String? = FileUtils.copyUriToStorage(fileUri, context) | ||
|
||
if (resultingPath != null) { | ||
val shareFileObject = ShareFileObject(resultingPath, intent.type) | ||
|
||
val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) | ||
val editor = sharedPreferences.edit() | ||
editor.putString(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) | ||
editor.apply() | ||
} | ||
} | ||
} | ||
|
||
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,12 @@ | ||
package com.expensify.chat.intenthandler | ||
|
||
import android.content.Intent | ||
|
||
object IntentHandlerConstants { | ||
const val preferencesFile = "shareActionHandler" | ||
const val shareObjectProperty = "shareObject" | ||
} | ||
interface IntentHandler { | ||
fun handle(intent: Intent): Boolean | ||
fun onCompleted() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.expensify.chat.intenthandler | ||
|
||
import android.content.Context | ||
|
||
object IntentHandlerFactory { | ||
fun getIntentHandler(context: Context, mimeType: String?, rest: String?): IntentHandler? { | ||
if (mimeType == null) return null | ||
|
||
return when { | ||
mimeType.matches(Regex("(image|application|audio|video)/.*")) -> FileIntentHandler(context) | ||
mimeType.startsWith("text/") -> TextIntentHandler(context) | ||
else -> throw UnsupportedOperationException("Unsupported MIME type: $mimeType") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.expensify.chat.intenthandler | ||
|
||
import com.google.gson.Gson | ||
|
||
data class ShareFileObject(val content: String, val mimeType: String?) { | ||
override fun toString(): String { | ||
return Gson().toJson(this) | ||
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to add library for handling json for this line? Wouldn't it be easier just to do something like: override fun toString(): String {
return "{\"content\": ${content}, \"mimeType\": ${mimeType}}" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might work partially, but in case we don't want to add this library we have to handle a few more issues like escaping eg. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And what about using |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package com.expensify.chat.intenthandler | ||
|
||
import android.content.Context | ||
import android.content.Intent | ||
import android.net.Uri | ||
import com.expensify.chat.utils.FileUtils | ||
|
||
|
||
class TextIntentHandler(private val context: Context) : AbstractIntentHandler() { | ||
override fun handle(intent: Intent): Boolean { | ||
super.clearTemporaryFiles(context) | ||
when(intent.action) { | ||
Intent.ACTION_SEND -> { | ||
handleTextIntent(intent, context) | ||
onCompleted() | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
private fun handleTextIntent(intent: Intent, context: Context) { | ||
when { | ||
intent.type == "text/plain" -> { | ||
val extras = intent.extras | ||
if (extras != null) { | ||
when { | ||
extras.containsKey(Intent.EXTRA_STREAM) -> { | ||
handleTextFileIntent(intent, context) | ||
} | ||
extras.containsKey(Intent.EXTRA_TEXT) -> { | ||
handleTextPlainIntent(intent, context) | ||
} | ||
else -> { | ||
throw UnsupportedOperationException("Unknown text/plain content") | ||
} | ||
} | ||
} | ||
} | ||
Regex("text/.*").matches(intent.type ?: "") -> handleTextFileIntent(intent, context) | ||
else -> throw UnsupportedOperationException("Unsupported MIME type: ${intent.type}") | ||
} | ||
} | ||
|
||
private fun saveToSharedPreferences(key: String, value: String) { | ||
val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE) | ||
val editor = sharedPreferences.edit() | ||
editor.putString(key, value) | ||
editor.apply() | ||
} | ||
|
||
private fun handleTextFileIntent(intent: Intent, context: Context) { | ||
(intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM))?.let { fileUri -> | ||
val resultingPath: String? = FileUtils.copyUriToStorage(fileUri, context) | ||
if (resultingPath != null) { | ||
val shareFileObject = ShareFileObject(resultingPath, intent.type) | ||
saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) | ||
} | ||
} | ||
} | ||
|
||
private fun handleTextPlainIntent(intent: Intent, context: Context) { | ||
var intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) | ||
if(intentTextContent != null) { | ||
val shareFileObject = ShareFileObject(intentTextContent, intent.type) | ||
saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) | ||
} | ||
} | ||
|
||
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for SNYK to pass we need to use
com.google.code.gson:[email protected]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done