Skip to content

Commit

Permalink
[_]: Generate video thumbnails correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
PixoDev committed Aug 7, 2023
1 parent 42face1 commit de73ac0
Show file tree
Hide file tree
Showing 31 changed files with 1,030 additions and 317 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,6 @@ android/keystores/debug.keystore
# generated by bob
lib/

.npmrc
.npmrc

config.json
6 changes: 6 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ android {
def kotlin_version = getExtOrDefault("kotlinVersion")
def spongycastle_version = "1.58.0.0"
def work_version = "2.8.0"
def room_version = "2.5.0"

dependencies {

Expand All @@ -82,6 +83,11 @@ dependencies {
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion



implementation "androidx.room:room-runtime:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation("androidx.work:work-runtime:$work_version")
implementation("androidx.work:work-runtime-ktx:$work_version")
implementation 'com.facebook.react:react-native:+'
Expand Down
110 changes: 106 additions & 4 deletions android/src/main/java/com/internxt/mobilesdk/MobileSdkModule.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.internxt.mobilesdk


import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.PackageManagerCompat.LOG_TAG
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.work.*
Expand All @@ -11,25 +13,36 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
import com.internxt.mobilesdk.config.MobileSdkAuthTokens
import com.internxt.mobilesdk.config.MobileSdkConfigLoader
import com.internxt.mobilesdk.core.*
import com.internxt.mobilesdk.data.photos.CreatePhotoPayload
import com.internxt.mobilesdk.data.photos.DevicePhotosItemType
import com.internxt.mobilesdk.data.photos.SyncedPhoto
import com.internxt.mobilesdk.services.FS
import com.internxt.mobilesdk.services.photos.DevicePhotosScanner
import com.internxt.mobilesdk.services.photos.DevicePhotosSyncChecker
import com.internxt.mobilesdk.services.photos.PhotosProcessingWorker
import com.internxt.mobilesdk.utils.CryptoUtils
import com.internxt.mobilesdk.utils.InvalidArgumentException
import com.internxt.mobilesdk.utils.JsonUtils
import com.internxt.mobilesdk.utils.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.Executors


class MobileSdkModule(val reactContext: ReactApplicationContext) :
class MobileSdkModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

private val devicePhotosScanner = DevicePhotosScanner(reactContext)
private val devicePhotosSyncChecker = DevicePhotosSyncChecker(reactContext)
private val upload = Upload()
private val encrypt = Encrypt()

private val photosEnqueuer = Executors.newFixedThreadPool(3)
private var scheduledPhotosToProcess = 0
private var processedPhotos = 0
Expand Down Expand Up @@ -166,10 +179,10 @@ class MobileSdkModule(val reactContext: ReactApplicationContext) :
val deviceId = config.getString("deviceId") ?: throw InvalidArgumentException("Missing deviceId")
Logger.info("Scheduling photo processing")


val events =
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
photosEnqueuer.execute {
val events =
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)


val requestBuilder = OneTimeWorkRequestBuilder<PhotosProcessingWorker>()

Expand Down Expand Up @@ -222,10 +235,99 @@ class MobileSdkModule(val reactContext: ReactApplicationContext) :
fun saveToDownloads(originUri: String, promise: Promise) {
try {
FS.saveFileToDownloadsDirectory(reactApplicationContext, originUri)
promise.resolve(true)
} catch (exception: Exception) {
exception.printStackTrace()
promise.reject(exception)
}
}

@ReactMethod
fun initPhotosProcessor(config: ReadableMap, promise: Promise) {

try {
val mnemonic = config.getString("mnemonic") ?: throw InvalidArgumentException("Missing mnemonic")
val bucketId = config.getString("bucketId") ?: throw InvalidArgumentException("Missing bucketId")
val photosUserId = config.getString("photosUserId") ?: throw InvalidArgumentException("Missing photosUserId")
val deviceId = config.getString("deviceId") ?: throw InvalidArgumentException("Missing deviceId")

// Resolve the initialization
promise.resolve(true)

// Get the React Event emitter, we'll send updates from this
val events =
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)

val allPhotos = devicePhotosScanner.getDevicePhotos()
val allVideos = devicePhotosScanner.getDeviceVideos()

val allItems = allPhotos.plus(allVideos)

val workManager = WorkManager
.getInstance(reactContext)

Logger.info("Found ${allItems.size} photos in the device")

Logger.info("Content ${allItems.contentToString()}")
allItems.forEach {
photosEnqueuer.execute {

val isSynced = devicePhotosSyncChecker.isSynced(it)

if(isSynced) {
Logger.info("${it.displayName} is already synced, skipping processing")
} else {
val requestBuilder = OneTimeWorkRequestBuilder<PhotosProcessingWorker>()
val data = Data.Builder()


data.putString("displayName", it.displayName)
data.putString("bucketId", bucketId)
data.putString("plainFilePath", it.uri.toString())
data.putString("mnemonic", mnemonic)
data.putString("photosUserId", photosUserId)
data.putString("deviceId", deviceId)
data.putString("type", it.type.name)
data.putString("takenAtISO", it.takenAt.format(DateTimeFormatter.ISO_DATE_TIME))

requestBuilder.setInputData(data.build())
requestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)


val request = requestBuilder.build()

workManager.enqueueUniqueWork(it.displayName, ExistingWorkPolicy.REPLACE, request)

Logger.info("Photos processing task enqueued correctly")

val workInfoLiveData =workManager.getWorkInfoByIdLiveData(request.id)
val observer = ((reactContext as ReactContext).currentActivity as AppCompatActivity)

GlobalScope.launch(Dispatchers.Main) {
workInfoLiveData.observe(observer) {

if (it !== null && (it.state == WorkInfo.State.SUCCEEDED || it.state == WorkInfo.State.FAILED || it.state == WorkInfo.State.CANCELLED)) {
Logger.info("Received work info update for work ${it.id}")
if(it.state == WorkInfo.State.SUCCEEDED) {
val encodedPhoto = it.outputData.getString("result")
if(encodedPhoto != null) {
val map = Arguments.createMap()

map.putString("result", encodedPhoto)
events.emit("onPhotoProcessed", map)
}
}
}
}
}
}

}
}
} catch (exception: Exception) {
Logger.error(exception)
promise.reject(exception)
}

}

Expand Down
48 changes: 48 additions & 0 deletions android/src/main/java/com/internxt/mobilesdk/data/photos/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,54 @@ data class CreatePhotoPayload(
val takenAt: String,
@Json()
val networkBucketId: String,
@Json()
val duration: Long?,
)






@JsonClass(generateAdapter = true)
data class SyncedPhoto(
@Json()
val id: String,
@Json()
val name: String,
@Json()
val type: String,
@Json()
val size: Long,
@Json()
val width: Int,
@Json()
val height: Int,
@Json()
val fileId: String,
@Json()
val previewId: String,
@Json()
val previews: List<PhotoPreview>,
@Json()
val deviceId: String,
@Json()
val userId: String,
@Json()
val hash: String,
@Json()
val itemType: String,
@Json()
val takenAt: String,
@Json()
val createdAt: String,
@Json()
val updatedAt: String,
@Json()
val status: String,
@Json()
val networkBucketId: String,
@Json()
val duration: Long?,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.internxt.mobilesdk.data.photos


import android.net.Uri
import java.io.File
import java.time.OffsetDateTime

enum class DevicePhotosItemType {
IMAGE,
VIDEO
}
data class DevicePhotosItem(
val displayName: String,
val takenAt: OffsetDateTime,
val uri: Uri,
val type: DevicePhotosItemType
)

data class DisplayablePhotosItem(
val name: String,
val type: DevicePhotosItemType,
val takenAt: OffsetDateTime,
val updatedAt: OffsetDateTime,
val size: Long,
val width: Int,
val height: Int
)
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package com.internxt.mobilesdk.services

import android.content.ContentResolver
import android.content.ContentValues
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import com.facebook.react.bridge.ReactApplicationContext
import com.internxt.mobilesdk.utils.FileAccessRejectionException
import java.io.*
import com.internxt.mobilesdk.utils.Logger
import kotlin.io.path.Path
import java.io.*


// This class is called FS to avoid naming conflicts with Java FileSystem class
Expand Down Expand Up @@ -77,6 +77,44 @@ object FS {
if(index == -1 ) throw Exception("This file does not have an extension")
return filename.substring(index+1)
}

@Throws(IOException::class)
fun copyExif(oldFileStream: InputStream, newFileStream: InputStream) {
val oldExif = ExifInterface(oldFileStream)
val attributes = arrayOf(
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_ISO_SPEED_RATINGS,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_ORIENTATION,
ExifInterface.TAG_SUBSEC_TIME,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
ExifInterface.TAG_WHITE_BALANCE
)
val newExif = ExifInterface(newFileStream)
for (i in attributes.indices) {
val value = oldExif.getAttribute(attributes[i])
if (value != null) newExif.setAttribute(attributes[i], value)
}
newExif.saveAttributes()
}
fun fileIsEmpty(path: String): Boolean {
val file = File(path)
try {
Expand All @@ -101,7 +139,7 @@ object FS {
?: throw Exception("Cannot open Input stream at path $originalFilePath")


if (false && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

val contentValues = ContentValues();

Expand Down Expand Up @@ -148,4 +186,15 @@ object FS {
fun getMimeType(contentResolver: ContentResolver, uri: String): String? {
return contentResolver.getType(getFileUri(uri, true))
}

fun getExtension(filePath: String): String? {
val file = File(filePath)
val fileName = file.name
val dotIndex = fileName.lastIndexOf('.')
return if (dotIndex > 0 && dotIndex < fileName.length - 1) {
fileName.substring(dotIndex + 1)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.internxt.mobilesdk.services.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.internxt.mobilesdk.services.database.photos.PhotosDBItem
import com.internxt.mobilesdk.services.database.photos.PhotosDao
import com.internxt.mobilesdk.services.database.photos.SyncedPhotosDao
import com.internxt.mobilesdk.services.database.photos.SyncedPhotosItem


@Database(entities = [PhotosDBItem::class, SyncedPhotosItem::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun photosDao(): PhotosDao?

abstract fun syncedPhotosDao(): SyncedPhotosDao?
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null

fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"internxt_app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Loading

0 comments on commit de73ac0

Please sign in to comment.