diff --git a/.github/workflows/github_actions.yml b/.github/workflows/github_actions.yml index 02cce2b..b445f27 100644 --- a/.github/workflows/github_actions.yml +++ b/.github/workflows/github_actions.yml @@ -31,11 +31,12 @@ jobs: - name: Code Coverage run: bundle exec fastlane coverage - - name: Setup sonarqube - uses: warchant/setup-sonar-scanner@v8 - - - name: Send to Sonarcloud - run: bundle exec fastlane sonarqube - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file + # Commenting Sonarqube steps for now, until we are able to configure Sonarqube in Ionic repos + #- name: Setup sonarqube + # uses: warchant/setup-sonar-scanner@v8 + + #- name: Send to Sonarcloud + # run: bundle exec fastlane sonarqube + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5e63623..5d73112 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,10 @@ android { "outputs/code-coverage/connected/*coverage.ec" ])) } + + testOptions { + unitTests.returnDefaultValues = true + } } repositories { @@ -88,7 +92,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.mockito:mockito-inline:5.2.0' - testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" } diff --git a/src/androidTest/java/io.ionic.libs/ioncameralib/ExampleInstrumentedTest.kt b/src/androidTest/java/io.ionic.libs/ioncameralib/ExampleInstrumentedTest.kt deleted file mode 100644 index c554bef..0000000 --- a/src/androidTest/java/io.ionic.libs/ioncameralib/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.ionic.libs.ioncameralib - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("io.ionic.libs.ioncameralib", appContext.packageName) - } -} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 4cc2068..937d80d 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -5,15 +5,15 @@ diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRExifHelper.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRExifHelper.kt similarity index 99% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRExifHelper.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRExifHelper.kt index cfae968..8c6ebc5 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRExifHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRExifHelper.kt @@ -23,7 +23,7 @@ import android.media.ExifInterface import android.net.Uri import java.io.IOException -class OSCAMRExifHelper : OSCAMRExifHelperInterface { +class IONCAMRExifHelper : IONCAMRExifHelperInterface { private var aperture: String? = null private var datetime: String? = null diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRExifHelperInterface.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRExifHelperInterface.kt similarity index 90% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRExifHelperInterface.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRExifHelperInterface.kt index 26b5b65..30a710b 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRExifHelperInterface.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRExifHelperInterface.kt @@ -3,7 +3,7 @@ package io.ionic.libs.ioncameralib.helper import android.media.ExifInterface import android.net.Uri -interface OSCAMRExifHelperInterface { +interface IONCAMRExifHelperInterface { fun createInFile(filePath: String?) fun createOutFile(filePath: String?) diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRFileHelper.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRFileHelper.kt similarity index 99% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRFileHelper.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRFileHelper.kt index 4d5284b..bd82f4e 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRFileHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRFileHelper.kt @@ -36,10 +36,10 @@ import java.nio.file.Files import java.nio.file.attribute.BasicFileAttributes import java.util.* -class OSCAMRFileHelper: OSCAMRFileHelperInterface { +class IONCAMRFileHelper: IONCAMRFileHelperInterface { companion object { - private const val LOG_TAG = "OSCAMRFileHelper" + private const val LOG_TAG = "IONCAMRFileHelper" private const val EXTERNAL_STORAGE = "com.android.externalstorage.documents" private const val DOWNLOADS_DOCUMENTS = "com.android.providers.downloads.documents" private const val PROVIDERS_MEDIA = "com.android.providers.media.documents" diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRFileHelperInterface.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRFileHelperInterface.kt similarity index 97% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRFileHelperInterface.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRFileHelperInterface.kt index 248b30d..24b0bed 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRFileHelperInterface.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRFileHelperInterface.kt @@ -7,7 +7,7 @@ import android.net.Uri import java.io.File import java.io.InputStream -interface OSCAMRFileHelperInterface { +interface IONCAMRFileHelperInterface { fun getRealPath(uri: Uri?, context: Context?): String? fun getRealPath(uriString: String, context: Context): String? fun getUriFromString(uriString: String): Uri diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRGalleryHelper.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRGalleryHelper.kt similarity index 56% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRGalleryHelper.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRGalleryHelper.kt index e7f8edb..df1890e 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRGalleryHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRGalleryHelper.kt @@ -1,6 +1,6 @@ package io.ionic.libs.ioncameralib.helper -class OSCAMRGalleryHelper(val picturesDirectory: String?, val galleryFileName: String?) { +class IONCAMRGalleryHelper(val picturesDirectory: String?, val galleryFileName: String?) { var galleryPath = this.picturesDirectory + "/" + this.galleryFileName diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRImageHelper.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRImageHelper.kt similarity index 92% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRImageHelper.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRImageHelper.kt index 723f6c2..8f1b2ad 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRImageHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRImageHelper.kt @@ -10,10 +10,10 @@ import android.graphics.Matrix import android.net.Uri import android.util.Base64 import androidx.core.graphics.scale -import io.ionic.libs.ioncameralib.model.IONError +import io.ionic.libs.ioncameralib.model.IONCAMRError import java.io.* -class OSCAMRImageHelper: OSCAMRImageHelperInterface { +class IONCAMRImageHelper: IONCAMRImageHelperInterface { companion object { private const val JPEG = 0 @@ -50,9 +50,9 @@ class OSCAMRImageHelper: OSCAMRImageHelperInterface { return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) } - override fun compressImage(activity: Activity?, uri: Uri?, bitmap: Bitmap?, compressFormat: CompressFormat, mQuality: Int, onError : (IONError) -> Unit){ + override fun compressImage(activity: Activity?, uri: Uri?, bitmap: Bitmap?, compressFormat: CompressFormat, mQuality: Int, onError : (IONCAMRError) -> Unit){ if(bitmap == null || uri == null || activity == null){ - onError(IONError.PROCESS_IMAGE_ERROR) + onError(IONCAMRError.PROCESS_IMAGE_ERROR) return } val os: OutputStream? = @@ -68,7 +68,7 @@ class OSCAMRImageHelper: OSCAMRImageHelperInterface { * * @param bitmap */ - override fun processPicture(bitmap: Bitmap?, encodingType: Int, mQuality: Int, onSuccess : (String) -> Unit, onError : (IONError) -> Unit) { + override fun processPicture(bitmap: Bitmap?, encodingType: Int, mQuality: Int, onSuccess : (String) -> Unit, onError : (IONCAMRError) -> Unit) { val jpegData = ByteArrayOutputStream() val compressFormat: CompressFormat = if (encodingType == JPEG) CompressFormat.JPEG else CompressFormat.PNG @@ -80,7 +80,7 @@ class OSCAMRImageHelper: OSCAMRImageHelperInterface { onSuccess(jsOut) } } catch (e: Exception) { - onError(IONError.PROCESS_IMAGE_ERROR) + onError(IONCAMRError.PROCESS_IMAGE_ERROR) } } @@ -92,7 +92,7 @@ class OSCAMRImageHelper: OSCAMRImageHelperInterface { return BitmapFactory.decodeFile(resultImagePath) } - override fun bitmapToBase64(result: Bitmap?, resolution: Int, quality: Int, onSuccess : (String) -> Unit, onError : (IONError) -> Unit) { + override fun bitmapToBase64(result: Bitmap?, resolution: Int, quality: Int, onSuccess : (String) -> Unit, onError : (IONCAMRError) -> Unit) { val byteArrayOutputStream = ByteArrayOutputStream() result?.let { val resizedImage = this.downsizeBitmapIfNeeded(it, resolution) @@ -104,7 +104,7 @@ class OSCAMRImageHelper: OSCAMRImageHelperInterface { } } - onError(IONError.EDIT_IMAGE_ERROR) + onError(IONCAMRError.EDIT_IMAGE_ERROR) return } diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRImageHelperInterface.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRImageHelperInterface.kt similarity index 87% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRImageHelperInterface.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRImageHelperInterface.kt index 1ba802b..bf1d4e2 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRImageHelperInterface.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRImageHelperInterface.kt @@ -6,19 +6,19 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.net.Uri -import io.ionic.libs.ioncameralib.model.IONError +import io.ionic.libs.ioncameralib.model.IONCAMRError import java.io.File import java.io.InputStream -interface OSCAMRImageHelperInterface { +interface IONCAMRImageHelperInterface { fun getBitmapForInputStream(fileStream: InputStream?): Bitmap? fun downsizeBitmapIfNeeded(bitmap: Bitmap, resolution: Int): Bitmap fun compressBitmap(bitmap: Bitmap, quality: Int): Bitmap - fun compressImage(activity: Activity?, uri: Uri?, bitmap: Bitmap?, compressFormat: Bitmap.CompressFormat, mQuality: Int, onError : (IONError) -> Unit) - fun processPicture(bitmap: Bitmap?, encodingType: Int, mQuality: Int, onSuccess : (String) -> Unit, onError : (IONError) -> Unit) + fun compressImage(activity: Activity?, uri: Uri?, bitmap: Bitmap?, compressFormat: Bitmap.CompressFormat, mQuality: Int, onError : (IONCAMRError) -> Unit) + fun processPicture(bitmap: Bitmap?, encodingType: Int, mQuality: Int, onSuccess : (String) -> Unit, onError : (IONCAMRError) -> Unit) fun decodeStream(fileStream: InputStream?, options: BitmapFactory.Options): Bitmap? fun decodeFile(resultImagePath: String?): Bitmap? - fun bitmapToBase64(result: Bitmap?, resolution: Int, quality: Int, onSuccess : (String) -> Unit, onError : (IONError) -> Unit) + fun bitmapToBase64(result: Bitmap?, resolution: Int, quality: Int, onSuccess : (String) -> Unit, onError : (IONCAMRError) -> Unit) fun base64toBitmap(imageByteArray: ByteArray): Bitmap? fun writeBitmapToFile(imageBitmap: Bitmap?, inputFile: File?) fun getScaledBitmap(unscaledBitmap: Bitmap?, scaledWidth: Int, scaledHeight: Int): Bitmap? diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRMediaHelper.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRMediaHelper.kt similarity index 97% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRMediaHelper.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRMediaHelper.kt index 6908796..37ac171 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRMediaHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRMediaHelper.kt @@ -16,11 +16,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream -class OSCAMRMediaHelper : OSCAMRMediaHelperInterface { +class IONCAMRMediaHelper : IONCAMRMediaHelperInterface { companion object { private const val CAMERA = 1 - private const val LOG_TAG = "OSCAMRMediaHelper" + private const val LOG_TAG = "IONCAMRMediaHelper" const val REQUEST_VIDEO_CAPTURE = 1 const val REQUEST_VIDEO_CAPTURE_SAVE_TO_GALLERY = 2 } @@ -111,7 +111,7 @@ class OSCAMRMediaHelper : OSCAMRMediaHelperInterface { return intent.resolveActivity(packageManager) != null } - /*override fun openDeviceVideo( + override fun openDeviceVideo( activity: Activity?, intent: Intent, videoFileUri: Uri?, @@ -122,7 +122,7 @@ class OSCAMRMediaHelper : OSCAMRMediaHelperInterface { intent, if (!saveToGallery) REQUEST_VIDEO_CAPTURE else REQUEST_VIDEO_CAPTURE_SAVE_TO_GALLERY ) - }*/ + } override fun createDeviceVideoIntent(activity: Activity?, intent: Intent, videoFileUri: Uri?, saveToGallery: Boolean, ): Intent? { val safeActivity = activity ?: return null @@ -154,13 +154,14 @@ class OSCAMRMediaHelper : OSCAMRMediaHelperInterface { return null } - override fun getVideoDuration(activity: Activity, uri: Uri): Long { + override fun getVideoDuration(activity: Activity, uri: Uri): Int { val mediaPlayer = MediaPlayer.create(activity, uri) - val duration = mediaPlayer?.duration?.toLong() ?: 0L + val duration = mediaPlayer?.duration ?: 0 mediaPlayer?.release() return duration } + override fun getVideoResolution(activity: Activity?, uri: Uri): Pair { val metaRetriever = MediaMetadataRetriever() metaRetriever.setDataSource(activity, uri) diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRMediaHelperInterface.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRMediaHelperInterface.kt similarity index 84% rename from src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRMediaHelperInterface.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRMediaHelperInterface.kt index 17131c6..3189dcb 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/helper/OSCAMRMediaHelperInterface.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/helper/IONCAMRMediaHelperInterface.kt @@ -5,7 +5,7 @@ import android.content.Intent import android.database.Cursor import android.net.Uri -interface OSCAMRMediaHelperInterface { +interface IONCAMRMediaHelperInterface { fun openDeviceCamera(activity: Activity?, imageUri: Uri?, returnType: Int) fun createCameraIntent(activity: Activity?, imageUri: Uri?): Intent? fun getCursor(activity: Activity?, contentStore: Uri): Cursor? @@ -14,10 +14,11 @@ interface OSCAMRMediaHelperInterface { fun getStringNumber(cursor: Cursor?, int: Int?): String? fun equalsDifference(currentNumOfImages: Int, numPics: Int?, diff: Int): Boolean fun existsActivity(activity: Activity?, intent: Intent): Boolean + fun openDeviceVideo(activity: Activity?, intent: Intent, videoFileUri: Uri?, saveToGallery: Boolean) fun createDeviceVideoIntent(activity: Activity?, intent: Intent, videoFileUri: Uri?, saveToGallery: Boolean): Intent? fun getVideoPathFromUri(activity: Activity, uri: Uri): String? suspend fun getThumbnailBase64String(activity: Activity, videoUri: Uri, targetDimension: Int): String? - fun getVideoDuration(activity: Activity, uri: Uri): Long + fun getVideoDuration(activity: Activity, uri: Uri): Int fun getVideoResolution(activity: Activity?, uri: Uri): Pair fun getImageResolution(imagePath: String): Pair } \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/OSCAMRImageEditorController.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/IONCAMRImageEditorController.kt similarity index 94% rename from src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/OSCAMRImageEditorController.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/IONCAMRImageEditorController.kt index 87f7edb..c966c05 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/OSCAMRImageEditorController.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/IONCAMRImageEditorController.kt @@ -4,7 +4,7 @@ import android.graphics.Bitmap import android.graphics.Matrix import android.graphics.Rect -class OSCAMRImageEditorController : OSCAMRImageEditorControllerInterface { +class IONCAMRImageEditorController : IONCAMRImageEditorControllerInterface { override suspend fun rotateLeft(image: Bitmap): Bitmap { val rotationMatrix = Matrix().apply { diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/OSCAMRImageEditorControllerInterface.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/IONCAMRImageEditorControllerInterface.kt similarity index 83% rename from src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/OSCAMRImageEditorControllerInterface.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/IONCAMRImageEditorControllerInterface.kt index 4daf83f..9a7cc5b 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/OSCAMRImageEditorControllerInterface.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/imageeditor/IONCAMRImageEditorControllerInterface.kt @@ -3,7 +3,7 @@ package io.ionic.libs.ioncameralib.imageeditor import android.graphics.Bitmap import android.graphics.Rect -interface OSCAMRImageEditorControllerInterface { +interface IONCAMRImageEditorControllerInterface { suspend fun rotateLeft(image: Bitmap): Bitmap suspend fun crop(image: Bitmap, rect: Rect) : Bitmap suspend fun flip(image: Bitmap): Bitmap diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/EditManager.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/EditManager.kt deleted file mode 100644 index 55f4833..0000000 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/EditManager.kt +++ /dev/null @@ -1,392 +0,0 @@ -package io.ionic.libs.ioncameralib.manager - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentValues -import android.content.Intent -import android.graphics.drawable.Drawable -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import android.util.Base64 -import android.util.Log -import androidx.activity.result.ActivityResultLauncher -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRImageHelperInterface -import io.ionic.libs.ioncameralib.model.IONError -import io.ionic.libs.ioncameralib.model.IONMediaMetadata -import io.ionic.libs.ioncameralib.model.IONMediaResult -import io.ionic.libs.ioncameralib.model.IONMediaType -import io.ionic.libs.ioncameralib.model.IONEditParameters -import io.ionic.libs.ioncameralib.view.ImageEditorActivity -import java.io.File -import androidx.core.net.toUri -import io.ionic.libs.ioncameralib.helper.OSCAMRGalleryHelper -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream -import java.text.SimpleDateFormat -import java.util.Date - -/** - * Contains edit functions - */ -class EditManager( - private var applicationId: String, - private var authority: String, - private var fileHelper: OSCAMRFileHelperInterface, - private var imageHelper: OSCAMRImageHelperInterface, -) { - - private var croppedUri: Uri? = null - private var croppedFilePath: String? = null - - companion object { - private const val JPEG = 0 - private const val PNG = 1 - private const val JPEG_TYPE = "jpg" - private const val PNG_TYPE = "png" - private const val JPEG_EXTENSION = ".$JPEG_TYPE" - private const val PNG_EXTENSION = ".$PNG_TYPE" - private const val IMAGE_MAX_RESOLUTION = 1080 - private const val IMAGE_MAX_QUALITY = 100 - private const val LOG_TAG = "EditManager" - private const val TIME_FORMAT = "yyyyMMdd_HHmmss" - - private const val PNG_MIME_TYPE = "image/png" - private const val JPEG_MIME_TYPE = "image/jpeg" - } - - /** - * Opens an activity with a UI to edit the provided image. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param image String representing the image in Base64. - * @param launcher ActivityResultLauncher to use when launching the edit activity - */ - fun editImage( - activity: Activity?, - image: String, - launcher: ActivityResultLauncher - ) { - // put the following code inside the OSCAMRImageHelper which deals with Bitmap related stuff - val imageByteArray: ByteArray = Base64.decode(image, Base64.NO_WRAP) - val imageBitmap = imageHelper.base64toBitmap(imageByteArray) - - //Creates temp file - val inputFilePath = - createCaptureFile( - activity, - JPEG, - System.currentTimeMillis().toString() + "" - ).absolutePath - val inputFileUri = inputFilePath.toUri() - val inputFile = File(inputFilePath) - - try { - //Writes bitmap in temp file - imageHelper.writeBitmapToFile(imageBitmap, inputFile) - openCropActivity(activity, inputFileUri, launcher) - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * Opens an activity with a UI to edit the provided image. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param pictureFilePath File path of the image to edit. - * @param launcher ActivityResultLauncher to use when launching the edit activity - */ - fun editURIPicture( - activity: Activity?, - pictureFilePath: String, - launcher: ActivityResultLauncher, - onError: (IONError) -> Unit - ) { - val imageFile = File(pictureFilePath) - if (!fileHelper.fileExists(imageFile)) { - onError(IONError.FILE_DOES_NOT_EXIST_ERROR) - return - } - val drawable: Drawable? = try { - Drawable.createFromPath(pictureFilePath) - } catch (ex: Exception) { - ex.printStackTrace() - null - } - if (drawable == null) { - // provided file path does not seem to belong to an actual picture - // .e.g could be video, for which edit is not supported - onError(IONError.FETCH_IMAGE_FROM_URI_ERROR) - return - } - val pictureUri = fileHelper.getUriForFile( - activity, - "$applicationId$authority", - imageFile - ) - try { - openCropActivity(activity, pictureUri, launcher) - } catch (e: Exception) { - e.printStackTrace() - onError(IONError.EDIT_IMAGE_ERROR) - } - } - - /** - * Opens the crop/edit activity with the provided image URI. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param picUri URI of the picture to edit. - * @param launcher ActivityResultLauncher to use when launching the edit activity - */ - fun openCropActivity( - activity: Activity?, - picUri: Uri?, - launcher: ActivityResultLauncher - ) { - val cropIntent = Intent(activity, ImageEditorActivity::class.java) - - // creates output file - croppedFilePath = createCaptureFile( - activity, - JPEG, - System.currentTimeMillis().toString() + "" - ).absolutePath - croppedUri = croppedFilePath?.toUri() - - cropIntent.putExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) - cropIntent.putExtra(ImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) - - launcher.launch(cropIntent) - } - - /** - * Applies all needed transformation to the image received from the Edit screen. - * - * @param activity Activity object necessary to process the result. - * @param intent An intent, containing the file path of the image that was just edited. - * @param editParameters IONEditParameters object with parameters for edit - * @param onImage callback that will be used when base64 image should be returned. - * @param onMediaResult callback that will be used when MediaResult object should be returned. - * @param onError callback that will be used when an error occurs. - */ - fun processResultFromEdit( - activity: Activity, - intent: Intent?, - editParameters: IONEditParameters, - onImage: (String) -> Unit, - onMediaResult: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit - ) { - val resultImagePath = intent?.getStringExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) - if (resultImagePath.isNullOrEmpty()) { - Log.d(LOG_TAG, "Image file path is null or empty") - onError(IONError.EDIT_IMAGE_ERROR) - return - } - if (editParameters.fromUri) { - val imageFile = File(resultImagePath) - val resultImageUri = fileHelper.getUriForFile( - activity, - "$applicationId$authority", - imageFile - ) - if (resultImageUri == null) { - Log.d(LOG_TAG, "Image URI is null") - onError(IONError.EDIT_IMAGE_ERROR) - return - } - val mediaResult = createImageMediaResult( - activity, - resultImagePath, - resultImageUri, - editParameters.includeMetadata - ) - if (mediaResult == null) { - Log.d(LOG_TAG, "MediaResult is null") - onError(IONError.EDIT_IMAGE_ERROR) - return - } - if (editParameters.saveToGallery) { - savePictureInGallery( - activity, - if (fileHelper.getFileExtension(resultImagePath) == JPEG_TYPE) 0 else 1, - resultImageUri) - } - onMediaResult(mediaResult) - } else { - val result = imageHelper.decodeFile(resultImagePath) - imageHelper.bitmapToBase64( - result = result, - resolution = IMAGE_MAX_RESOLUTION, - quality = IMAGE_MAX_QUALITY, - onSuccess = { onImage(it) }, - onError = { onError(it) } - ) - } - } - - /** - * Create a file in the applications temporary directory based upon the supplied encoding. - * - * @param encodingType of the image to be taken - * @param fileName or resultant File object. - * @return a File object pointing to the temporary picture - */ - private fun createCaptureFile( - activity: Activity?, - encodingType: Int, - fileName: String = "" - ): File { - var fileName = fileName - if (fileName.isEmpty()) { - fileName = ".Pic" - } - fileName = if (encodingType == JPEG) { - fileName + JPEG_EXTENSION - } else if (encodingType == PNG) { - fileName + PNG_EXTENSION - } else { - throw IllegalArgumentException("Invalid Encoding Type: $encodingType") - } - return fileHelper.createCaptureFile(activity, fileName) - } - - /** - * Transforms the image media item uri into a media result object. - * @param imagePath A string with the path for the image media item. - * @return An object containing relevant information for the media item. - * Null if an error occurred. - */ - private fun createImageMediaResult( - activity: Activity, - imagePath: String, - mediaUri: Uri, - includeMetadata: Boolean - ): IONMediaResult? { - var base64Image = "" - var error: IONError? = null - - val file = File(imagePath) - if (!fileHelper.fileExists(file)) return null - - val decodedImage = imageHelper.decodeFile(imagePath) - - if(decodedImage == null) return null - - val downsizedImage = imageHelper.downsizeBitmapIfNeeded(decodedImage, IMAGE_MAX_RESOLUTION) - val compressedImage = imageHelper.compressBitmap(downsizedImage, 100) - - imageHelper.bitmapToBase64(compressedImage, - resolution = IMAGE_MAX_RESOLUTION, - quality = IMAGE_MAX_QUALITY, - onSuccess = { base64Image = it }, - onError = { error = it } - ) - - if (error != null) { - return null - } - - var metadata: IONMediaMetadata? = null - if (includeMetadata) { - metadata = IONMediaMetadata( - fileHelper.getFileSizeFromUri(activity, mediaUri), - null, - fileHelper.getFileExtension(imagePath), - "${decodedImage.height}x${decodedImage.width}", - fileHelper.getFileCreationDate(file), - ) - } - - return IONMediaResult(IONMediaType.PICTURE.type, imagePath, base64Image, metadata, true) - } - - private fun savePictureInGallery(activity: Activity, encodingType: Int, srcUri: Uri?) { - val galleryPathVO: OSCAMRGalleryHelper = getPicturesPath(encodingType) - val fileFromGalleryPath = File(galleryPathVO.galleryPath) - val galleryUri = Uri.fromFile(fileFromGalleryPath) - - if (Build.VERSION.SDK_INT <= 28) { - writeTakenPictureToGalleryLowerThanAndroidQ(activity, srcUri, galleryUri) - } else { - writeTakenPictureToGalleryStartingFromAndroidQ( - activity, - srcUri, - galleryPathVO, - encodingType - ) - } - } - - private fun getPicturesPath(encodingType: Int): OSCAMRGalleryHelper { - val timeStamp = - SimpleDateFormat(TIME_FORMAT).format( - Date() - ) - val imageFileName = - "IMG_" + timeStamp + if (encodingType == JPEG) JPEG_EXTENSION else PNG_EXTENSION - val storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES - ) - storageDir.mkdirs() - return OSCAMRGalleryHelper(storageDir.absolutePath, imageFileName) - } - - @Throws(IOException::class) - private fun writeTakenPictureToGalleryLowerThanAndroidQ(activity: Activity?, srcUri: Uri?, galleryUri: Uri?) { - writeUncompressedImage(activity, srcUri, galleryUri) - fileHelper.refreshGallery(activity, galleryUri) - } - - @Throws(IOException::class) - private fun writeTakenPictureToGalleryStartingFromAndroidQ( - activity: Activity?, - srcUri: Uri?, - galleryPathVO: OSCAMRGalleryHelper, - encodingType: Int - ) { - // Starting from Android Q, working with the ACTION_MEDIA_SCANNER_SCAN_FILE intent is deprecated - // https://developer.android.com/reference/android/content/Intent#ACTION_MEDIA_SCANNER_SCAN_FILE - // we must start working with the MediaStore from Android Q on. - val resolver: ContentResolver? = activity?.contentResolver - val contentValues = ContentValues() - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, galleryPathVO.galleryFileName) - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, getMimetypeForFormat(encodingType)) - val galleryOutputUri = - resolver?.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) - val fileStream: InputStream? = - fileHelper.getInputStreamFromUriString( - srcUri.toString(), - activity - ) - fileHelper.writeUncompressedImage(activity, fileStream, galleryOutputUri) - } - - /** - * In the special case where the default width, height and quality are unchanged - * we just write the file out to disk saving the expensive Bitmap.compress function. - * - * @param src - * @throws FileNotFoundException - * @throws IOException - */ - @Throws(FileNotFoundException::class, IOException::class) - private fun writeUncompressedImage(activity: Activity?, src: Uri?, dest: Uri?) { - val fis: InputStream? = fileHelper.getInputStreamFromUriString(src.toString(), activity) - fileHelper.writeUncompressedImage(activity, fis, dest) - } - - /** - * Converts output image format int value to string value of mime type. - * @param outputFormat int Output format of camera API. - * Must be value of either JPEG or PNG constant - * @return String String value of mime type or empty string if mime type is not supported - */ - private fun getMimetypeForFormat(outputFormat: Int): String? { - if (outputFormat == PNG) return PNG_MIME_TYPE - return if (outputFormat == JPEG) JPEG_MIME_TYPE else "" - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/GalleryManager.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/GalleryManager.kt deleted file mode 100644 index 61d5102..0000000 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/GalleryManager.kt +++ /dev/null @@ -1,276 +0,0 @@ -package io.ionic.libs.ioncameralib.manager - -import android.app.Activity -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.net.Uri -import android.util.Log -import androidx.activity.result.ActivityResultLauncher -import io.ionic.libs.ioncameralib.helper.OSCAMRExifHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRImageHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRMediaHelperInterface -import io.ionic.libs.ioncameralib.model.IONError -import io.ionic.libs.ioncameralib.model.IONMediaResult -import io.ionic.libs.ioncameralib.model.IONMediaType -import io.ionic.libs.ioncameralib.processor.IONMediaProcessor -import io.ionic.libs.ioncameralib.view.IONOpenPhotoPickerActivity -import io.ionic.libs.ioncameralib.view.IONLoadingActivity -import io.ionic.libs.ioncameralib.view.ImageEditorActivity -import java.io.File - -class GalleryManager( - private var exif: OSCAMRExifHelperInterface, - private var fileHelper: OSCAMRFileHelperInterface, - private var mediaHelper: OSCAMRMediaHelperInterface, - private var imageHelper: OSCAMRImageHelperInterface -) { - private var croppedUri: Uri? = null - private var croppedFilePath: String? = null - - private val mediaProcessor = IONMediaProcessor( - exif = exif, - fileHelper = fileHelper, - mediaHelper = mediaHelper, - imageHelper = imageHelper - ) - - companion object { - private const val JPEG = 0 - private const val PNG = 1 - private const val JPEG_TYPE = "jpg" - private const val PNG_TYPE = "png" - private const val JPEG_EXTENSION = ".$JPEG_TYPE" - private const val PNG_EXTENSION = ".${PNG_TYPE}" - private const val LOG_TAG = "GalleryManager" - private const val ALLOW_MULTIPLE = "allowMultiple" - private const val MEDIA_TYPE = "mediaType" - } - - /** - * Opens a screen that allows users to select media from device gallery - * @param activity Activity object that will be necessary to launch the edit activity. - * @param mediaType The type of content the user is allowed to select. - * @param allowMultiSelect Whether or not the user should be allowed to select multiple items - * from gallery. - */ - fun chooseFromGallery( - activity: Activity, - mediaType: IONMediaType, - allowMultiSelect: Boolean, - launcher: ActivityResultLauncher - ) { - try { - val intent = Intent(activity, IONOpenPhotoPickerActivity::class.java).apply { - putExtra(ALLOW_MULTIPLE, allowMultiSelect) - putExtra(MEDIA_TYPE, mediaType.mimeType) - } - - launcher.launch(intent) - } catch (e: Exception) { - Log.e(LOG_TAG, e.message.toString()) - } - } - - /** - * Handles the result after users have selected media from device gallery. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param resultCode The code resulting from the operation. - * @param intent The intent resulting from the operation - * @param onSuccess The code the be executed if the operation was successfully. - * Returns a list of media item results. - * @param onError he code the be executed if the operation was not successfully. - */ - suspend fun onChooseFromGalleryResult( - activity: Activity, - resultCode: Int, - intent: Intent?, - includeMetadata: Boolean = false, - onSuccess: (List) -> Unit, - onError: (IONError) -> Unit - ) { - - if (intent == null) { - onError(IONError.CHOOSE_MULTIMEDIA_CANCELLED_ERROR) - return - } - - when (resultCode) { - - RESULT_OK -> { - - val uris = imageHelper.getResultUriFromIntent(intent) - - showLoadingScreen(activity) - - val results: MutableList = mutableListOf() - for (uri in uris) { - - var fileLocation = fileHelper.getRealPath(uri, activity) - - // when fileLocation = null means the file isn't available on the local filesystem. - - if (fileLocation == null) { - fileLocation = - fileHelper.getImagePathFromInputStreamUri(activity, uri) ?: continue - } - - val mediaResult = - createMediaResult(activity, fileLocation, uri, includeMetadata) - - if (mediaResult == null) { - onError(IONError.GENERIC_CHOOSE_MULTIMEDIA_ERROR) - dismissLoadingScreen(activity) - return - } - results.add(mediaResult) - } - - onSuccess(results) - dismissLoadingScreen(activity) - } - - RESULT_CANCELED -> { - onError(IONError.CHOOSE_MULTIMEDIA_CANCELLED_ERROR) - } - - else -> { - onError(IONError.GENERIC_CHOOSE_MULTIMEDIA_ERROR) - } - } - } - - - /** - * Handles the result after users have edited media from device gallery. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param resultCode The code resulting from the operation. - * @param intent The intent resulting from the operation - * @param onSuccess The code the be executed if the operation was successfully. - * Returns a list of media item results. - * @param onError he code the be executed if the operation was not successfully. - */ - suspend fun onChooseFromGalleryEditResult( - activity: Activity, - resultCode: Int, - intent: Intent?, - includeMetadata: Boolean = false, - onSuccess: (List) -> Unit, - onError: (IONError) -> Unit - ) { - when (resultCode) { - - RESULT_OK -> { - if (intent == null) { - onError(IONError.EDIT_IMAGE_ERROR) - return - } - - // An empty string here will trigger EDIT_IMAGE_ERROR later - val fileLocation = - intent.getStringExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) ?: "" - - val mediaResult = createMediaResult( - activity, - fileLocation, - fileHelper.getUriFromString(fileLocation), - includeMetadata - ) - if (mediaResult == null) { - onError(IONError.EDIT_IMAGE_ERROR) - return - } - onSuccess(listOf(mediaResult)) - } - - RESULT_CANCELED -> { - onError(IONError.EDIT_CANCELLED_ERROR) - } - - else -> { - onError(IONError.EDIT_IMAGE_ERROR) - } - } - } - - fun openCropActivity( - activity: Activity?, - picUri: Uri?, - launcher: ActivityResultLauncher - ) { - val cropIntent = Intent(activity, ImageEditorActivity::class.java) - - // creates output file - croppedFilePath = createCaptureFile( - activity, - JPEG, - System.currentTimeMillis().toString() + "" - ).absolutePath - croppedUri = Uri.parse(croppedFilePath) - - cropIntent.putExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) - cropIntent.putExtra(ImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) - - launcher.launch(cropIntent) - } - - /** - * Create a file in the applications temporary directory based upon the supplied encoding. - * - * @param encodingType of the image to be taken - * @param fileName or resultant File object. - * @return a File object pointing to the temporary picture - */ - fun createCaptureFile(activity: Activity?, encodingType: Int, fileName: String = ""): File { - var fileName = fileName - if (fileName.isEmpty()) { - fileName = ".Pic" - } - fileName = if (encodingType == JPEG) { - fileName + JPEG_EXTENSION - } else if (encodingType == PNG) { - fileName + PNG_EXTENSION - } else { - throw IllegalArgumentException("Invalid Encoding Type: $encodingType") - } - return fileHelper.createCaptureFile(activity, fileName) - } - - /** - * Transforms the media item uri into a media result object. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param filePath The uri for the media item. - * @return An object containing relevant information for the media item. - * Null if an error occurred. - */ - private suspend fun createMediaResult( - activity: Activity, - filePath: String, - uri: Uri, - includeMetadata: Boolean, - ): IONMediaResult? { - - val mimeType = fileHelper.getMimeType(filePath, activity) - val isImage = mimeType != null && mimeType.startsWith("image") - - return if (isImage) { - mediaProcessor.createImageMediaResult(activity, filePath, uri, includeMetadata, null) - } else { - mediaProcessor.createVideoMediaResult(activity, filePath, uri, includeMetadata) - } - } - - fun extractUris(intent: Intent?): List { - if (intent == null) return emptyList() - return imageHelper.getResultUriFromIntent(intent) - } - - private fun showLoadingScreen(activity: Activity) { - activity.startActivity(Intent(activity, IONLoadingActivity::class.java)) - } - - private fun dismissLoadingScreen(activity: Activity) { - activity.sendBroadcast(Intent(IONLoadingActivity.DISMISS_INTENT_FILTER)) - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/CameraManager.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRCameraManager.kt similarity index 51% rename from src/main/kotlin/io/ionic/libs/ioncameralib/manager/CameraManager.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRCameraManager.kt index 8a184cc..f250d5f 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/CameraManager.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRCameraManager.kt @@ -2,44 +2,35 @@ package io.ionic.libs.ioncameralib.manager import android.app.Activity import androidx.activity.result.ActivityResultLauncher -import android.content.ContentResolver -import android.content.ContentValues import android.content.Intent -import android.media.ExifInterface import android.media.MediaScannerConnection import android.media.MediaScannerConnection.MediaScannerConnectionClient import android.net.Uri -import android.os.Build -import android.os.Environment import android.provider.MediaStore import android.util.Log -import io.ionic.libs.ioncameralib.helper.OSCAMRExifHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRImageHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRMediaHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRGalleryHelper -import io.ionic.libs.ioncameralib.model.IONError -import io.ionic.libs.ioncameralib.model.IONMediaResult -import io.ionic.libs.ioncameralib.model.IONMediaType -import io.ionic.libs.ioncameralib.model.IONCameraParameters -import io.ionic.libs.ioncameralib.view.ImageEditorActivity -import io.ionic.libs.ioncameralib.processor.IONMediaProcessor -import kotlinx.coroutines.Job +import io.ionic.libs.ioncameralib.helper.IONCAMRExifHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRImageHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRMediaHelperInterface +import io.ionic.libs.ioncameralib.model.IONCAMRError +import io.ionic.libs.ioncameralib.model.IONCAMRMediaResult +import io.ionic.libs.ioncameralib.model.IONCAMRMediaType +import io.ionic.libs.ioncameralib.model.IONCAMRCameraParameters +import io.ionic.libs.ioncameralib.view.IONCAMRImageEditorActivity +import io.ionic.libs.ioncameralib.processor.IONCAMRMediaProcessor import java.io.File -import java.io.FileNotFoundException import java.io.IOException -import java.io.InputStream import java.text.SimpleDateFormat import java.util.Date -class CameraManager( +class IONCAMRCameraManager( private var applicationId: String, private var authority: String, - private var exif: OSCAMRExifHelperInterface, - private var fileHelper: OSCAMRFileHelperInterface, - private var mediaHelper: OSCAMRMediaHelperInterface, - private var imageHelper: OSCAMRImageHelperInterface + private var exif: IONCAMRExifHelperInterface, + private var fileHelper: IONCAMRFileHelperInterface, + private var mediaHelper: IONCAMRMediaHelperInterface, + private var imageHelper: IONCAMRImageHelperInterface ) : MediaScannerConnectionClient { private var imageFilePath: String? = null private var imageUri: Uri? = null @@ -47,8 +38,7 @@ class CameraManager( private var croppedFilePath: String? = null private var conn: MediaScannerConnection? = null private var scanMe: Uri? = null - - private val mediaProcessor = IONMediaProcessor( + private val mediaProcessor = IONCAMRMediaProcessor( exif = exif, fileHelper = fileHelper, mediaHelper = mediaHelper, @@ -57,33 +47,14 @@ class CameraManager( companion object { private const val JPEG = 0 - private const val PNG = 1 - private const val JPEG_TYPE = "jpg" - private const val PNG_TYPE = "png" - private const val JPEG_EXTENSION = ".$JPEG_TYPE" - private const val PNG_EXTENSION = ".$PNG_TYPE" - private const val PNG_MIME_TYPE = "image/png" - private const val JPEG_MIME_TYPE = "image/jpeg" - - private const val GET_PICTURE = "Get Picture" - private const val TIME_FORMAT = "yyyyMMdd_HHmmss" - private const val LOG_TAG = "CameraManager" - - private const val CLOSING_INPUT_STREAM_ERROR = "Exception while closing file input stream." - - const val EDIT_REQUEST_CODE = 7 - const val EDIT_FROM_GALLERY_REQUEST_CODE = 11 - + private const val LOG_TAG = "IONCAMRCameraManager" private const val PICTURE_NAMES_PREFIX = "PIC_" private const val VIDEO_NAMES_PREFIX = "VID_" private const val VIDEO_FORMAT = ".mp4" - private const val IMAGE_MAX_RESOLUTION = 1080 - private const val IMAGE_MAX_QUALITY = 100 private const val STORE = "CameraStore" private const val EDIT_FILE_NAME_KEY = "EditFileName" - private const val ALLOW_MULTIPLE = "allowMultiple" - private const val MEDIA_TYPE = "mediaType" + const val EDIT_REQUEST_CODE = 7 } /** @@ -103,6 +74,7 @@ class CameraManager( * Take a picture with the camera. * @param activity Activity object that will be necessary to take the picture * @param encodingType JPEG or PNG. + * @param launcher ActivityResultLauncher to use when launching the camera activity */ fun takePhoto(activity: Activity, encodingType: Int, launcher: ActivityResultLauncher) { // Save filename to fetch later (needed when allowEdit is true) @@ -132,13 +104,14 @@ class CameraManager( * Calls the intent to open the device's camera to record a video. * @param activity Activity object that will be necessary to launch the edit activity. * @param saveVideoToGallery Indicates if the recorded video should be saved to the device gallery + * @param launcher ActivityResultLauncher to use when launching the camera activity * @param onError callback that will be used when an error occurs. */ fun recordVideo( activity: Activity, saveVideoToGallery: Boolean = false, launcher: ActivityResultLauncher, - onError: (IONError) -> Unit + onError: (IONCAMRError) -> Unit ) { val videoFileUri = fileHelper.getUriForFile( activity, @@ -166,31 +139,10 @@ class CameraManager( } } else { Log.d(LOG_TAG, "Error: You don't have a default camera for recording video.") - onError(IONError.NO_CAMERA_AVAILABLE_ERROR) + onError(IONCAMRError.NO_CAMERA_AVAILABLE_ERROR) } } - fun openCropActivity( - activity: Activity?, - picUri: Uri?, - launcher: ActivityResultLauncher - ) { - val cropIntent = Intent(activity, ImageEditorActivity::class.java) - - // creates output file - croppedFilePath = createCaptureFile( - activity, - JPEG, - System.currentTimeMillis().toString() + "" - ).absolutePath - croppedUri = Uri.parse(croppedFilePath) - - cropIntent.putExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) - cropIntent.putExtra(ImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) - - launcher.launch(cropIntent) - } - /** * Create a file in the applications temporary directory based upon the supplied encoding. * @@ -198,19 +150,16 @@ class CameraManager( * @param fileName or resultant File object. * @return a File object pointing to the temporary picture */ - fun createCaptureFile(activity: Activity?, encodingType: Int, fileName: String = ""): File { - var fileName = fileName - if (fileName.isEmpty()) { - fileName = ".Pic" - } - fileName = if (encodingType == JPEG) { - fileName + JPEG_EXTENSION - } else if (encodingType == PNG) { - fileName + PNG_EXTENSION - } else { - throw IllegalArgumentException("Invalid Encoding Type: $encodingType") - } - return fileHelper.createCaptureFile(activity, fileName) + fun createCaptureFile( + activity: Activity?, + encodingType: Int, + fileName: String = "" + ): File { + return mediaProcessor.createCaptureFile( + activity = activity, + encodingType = encodingType, + fileName = fileName + ) } /** @@ -233,20 +182,23 @@ class CameraManager( @Throws(IOException::class) fun processResultFromCamera( activity: Activity, - camParameters: IONCameraParameters, + intent: Intent?, + camParameters: IONCAMRCameraParameters, onImage: (String) -> Unit, - onMediaResult: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit + onMediaResult: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit ) { - // Create an ExifHelper to save the exif data that is lost during compression - //no longer necessary, this will be passed by dependency injection through the constructor - //val exif = OSCAMRExifHelper() - val sourcePath = - if (camParameters.allowEdit && this.croppedUri != null) this.croppedFilePath else imageFilePath - - if (sourcePath == null) { - onError(IONError.TAKE_PHOTO_ERROR) - return + val intentEditedPath = + intent?.getStringExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) + + // NOTE: croppedUri/croppedFilePath are kept only for the legacy flow + // The new API returns the edited image + // via IMAGE_OUTPUT_URI_EXTRAS. This can be removed once + // the legacy flow is removed. + val sourcePath = when { + camParameters.allowEdit && this.croppedUri != null && !this.croppedFilePath.isNullOrEmpty() -> this.croppedFilePath + camParameters.allowEdit && !intentEditedPath.isNullOrEmpty() -> intentEditedPath + else -> imageFilePath } if (camParameters.encodingType == JPEG) { @@ -263,17 +215,20 @@ class CameraManager( // CB-5479 When this option is given the unchanged image should be saved // in the gallery and the modified image is saved in the temporary directory if (camParameters.saveToPhotoAlbum) { - val srcUri: Uri? = if (camParameters.allowEdit && this.croppedUri != null) { - croppedUri - } else { - imageUri + val srcUri: Uri? = sourcePath?.let { + fileHelper.getUriForFile( + activity, + "$applicationId$authority", + File(it) + ) } - savedSuccessfully = savePictureInGallery(activity, camParameters.encodingType, srcUri) + savedSuccessfully = mediaProcessor.savePictureInGallery(activity, camParameters.encodingType, srcUri) } mediaProcessor.processCameraImage( activity = activity, + intent = intent, sourcePath = sourcePath, "$applicationId$authority", camParameters = camParameters, @@ -294,11 +249,11 @@ class CameraManager( uri: Uri?, fromGallery: Boolean = false, includeMetadata: Boolean = false, - onSuccess: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit + onSuccess: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit ) { if (uri == null || uri.path == null) { - onError(IONError.CAPTURE_VIDEO_ERROR) + onError(IONCAMRError.CAPTURE_VIDEO_ERROR) return } @@ -315,12 +270,12 @@ class CameraManager( val fileName = videoFilePath.split("/").last() fileHelper.storeFileNameInPrefs(fileName, activity) } else { - onError(IONError.CAPTURE_VIDEO_ERROR) + onError(IONCAMRError.CAPTURE_VIDEO_ERROR) } } if (videoFilePath.isNullOrEmpty()) { - onError(IONError.MEDIA_PATH_ERROR) + onError(IONCAMRError.MEDIA_PATH_ERROR) return } @@ -335,117 +290,80 @@ class CameraManager( ) } - private fun savePictureInGallery(activity: Activity, encodingType: Int, srcUri: Uri?): Boolean { - return try { - val galleryPathVO: OSCAMRGalleryHelper = getPicturesPath(encodingType) - val fileFromGalleryPath = File(galleryPathVO.galleryPath) - val galleryUri = Uri.fromFile(fileFromGalleryPath) - - if (Build.VERSION.SDK_INT <= 28) { - writeTakenPictureToGalleryLowerThanAndroidQ(activity, srcUri, galleryUri) - } else { - writeTakenPictureToGalleryStartingFromAndroidQ( - activity, - srcUri, - galleryPathVO, - encodingType - ) - } - true - } catch (e: Exception) { - false - } + fun onDestroy(activity: Activity) { + deleteVideoFilesFromCache(activity) } - @Throws(IOException::class) - private fun writeTakenPictureToGalleryLowerThanAndroidQ( - activity: Activity?, - srcUri: Uri?, - galleryUri: Uri? - ) { - writeUncompressedImage(activity, srcUri, galleryUri) - fileHelper.refreshGallery(activity, galleryUri) + override fun onScanCompleted(p0: String?, p1: Uri?) { + conn?.disconnect() } - @Throws(IOException::class) - private fun writeTakenPictureToGalleryStartingFromAndroidQ( - activity: Activity?, - srcUri: Uri?, - galleryPathVO: OSCAMRGalleryHelper, - encodingType: Int - ) { - // Starting from Android Q, working with the ACTION_MEDIA_SCANNER_SCAN_FILE intent is deprecated - // https://developer.android.com/reference/android/content/Intent#ACTION_MEDIA_SCANNER_SCAN_FILE - // we must start working with the MediaStore from Android Q on. - val resolver: ContentResolver? = activity?.contentResolver - val contentValues = ContentValues() - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, galleryPathVO.galleryFileName) - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, getMimetypeForFormat(encodingType)) - val galleryOutputUri = - resolver?.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) - val fileStream: InputStream? = - fileHelper.getInputStreamFromUriString( - srcUri.toString(), - activity + override fun onMediaScannerConnected() { + try { + conn?.scanFile(scanMe.toString(), IONCAMRMediaType.PICTURE.mimeType) + } catch (e: IllegalStateException) { + Log.d( + LOG_TAG, + "Can't scan file in MediaScanner after taking picture" ) - fileHelper.writeUncompressedImage(activity, fileStream, galleryOutputUri) + } } - /** - * Converts output image format int value to string value of mime type. - * @param outputFormat int Output format of camera API. - * Must be value of either JPEG or PNG constant - * @return String String value of mime type or empty string if mime type is not supported - */ - private fun getMimetypeForFormat(outputFormat: Int): String? { - if (outputFormat == PNG) return PNG_MIME_TYPE - return if (outputFormat == JPEG) JPEG_MIME_TYPE else "" - } +// --------------------------------------------------------------------- +// Legacy API (startActivityForResult) – kept for backward compatibility +// --------------------------------------------------------------------- /** - * In the special case where the default width, height and quality are unchanged - * we just write the file out to disk saving the expensive Bitmap.compress function. - * - * @param src - * @throws FileNotFoundException - * @throws IOException + * Take a picture with the camera. + * @param activity Activity object that will be necessary to take the picture + * @param encodingType JPEG or PNG. */ - @Throws(FileNotFoundException::class, IOException::class) - private fun writeUncompressedImage(activity: Activity?, src: Uri?, dest: Uri?) { - val fis: InputStream? = fileHelper.getInputStreamFromUriString(src.toString(), activity) - fileHelper.writeUncompressedImage(activity, fis, dest) - } + fun takePicture(activity: Activity, returnType: Int, encodingType: Int) { + // Save filename to fetch later (needed when allowEdit is true) + val fileName = PICTURE_NAMES_PREFIX + SimpleDateFormat( + TIME_FORMAT + ).format(Date()) + fileHelper.saveStringSharedPreferences( + activity, + EDIT_FILE_NAME_KEY, fileName + ) - private fun getPicturesPath(encodingType: Int): OSCAMRGalleryHelper { - val timeStamp = - SimpleDateFormat(TIME_FORMAT).format( - Date() - ) - val imageFileName = - "IMG_" + timeStamp + if (encodingType == JPEG) JPEG_EXTENSION else PNG_EXTENSION - val storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES + // Specify file so that large image is captured and returned + val photo: File = createCaptureFile( + activity, + encodingType, + fileName ) - storageDir.mkdirs() - return OSCAMRGalleryHelper(storageDir.absolutePath, imageFileName) - } + this.imageFilePath = photo.absolutePath + this.imageUri = fileHelper.getUriForFile(activity, "$applicationId$authority", photo) - fun onDestroy(activity: Activity) { - deleteVideoFilesFromCache(activity) + mediaHelper.openDeviceCamera(activity, imageUri, returnType) } - override fun onScanCompleted(p0: String?, p1: Uri?) { - conn?.disconnect() + fun openCropActivity(activity: Activity?, picUri: Uri?, requestCode: Int?, destType: Int?) { + val cropIntent = createCropIntent(activity, picUri) + var code = EDIT_REQUEST_CODE + if (requestCode != null && destType != null) { + code = requestCode + destType + } + activity?.startActivityForResult( + cropIntent, + code + ) } - override fun onMediaScannerConnected() { - try { - conn?.scanFile(scanMe.toString(), IONMediaType.PICTURE.mimeType) - } catch (e: IllegalStateException) { - Log.d( - LOG_TAG, - "Can't scan file in MediaScanner after taking picture" - ) - } + private fun createCropIntent(activity: Activity?, picUri: Uri?): Intent { + val cropIntent = Intent(activity, IONCAMRImageEditorActivity::class.java) + croppedFilePath = createCaptureFile( + activity, + JPEG, + System.currentTimeMillis().toString() + "" + ).absolutePath + croppedUri = Uri.parse(croppedFilePath) + + cropIntent.putExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) + cropIntent.putExtra(IONCAMRImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) + return cropIntent } + } \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMREditManager.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMREditManager.kt new file mode 100644 index 0000000..0b20206 --- /dev/null +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMREditManager.kt @@ -0,0 +1,242 @@ +package io.ionic.libs.ioncameralib.manager + +import android.app.Activity +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.Base64 +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRImageHelperInterface +import io.ionic.libs.ioncameralib.model.IONCAMRError +import io.ionic.libs.ioncameralib.model.IONCAMRMediaResult +import io.ionic.libs.ioncameralib.model.IONCAMREditParameters +import io.ionic.libs.ioncameralib.view.IONCAMRImageEditorActivity +import java.io.File +import androidx.core.net.toUri +import io.ionic.libs.ioncameralib.helper.IONCAMRExifHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRMediaHelperInterface +import io.ionic.libs.ioncameralib.processor.IONCAMRMediaProcessor + +/** + * Contains edit functions + */ +class IONCAMREditManager( + private var applicationId: String, + private var authority: String, + private var exif: IONCAMRExifHelperInterface, + private var fileHelper: IONCAMRFileHelperInterface, + private var mediaHelper: IONCAMRMediaHelperInterface, + private var imageHelper: IONCAMRImageHelperInterface, +) { + + private var croppedUri: Uri? = null + private var croppedFilePath: String? = null + + private val mediaProcessor = IONCAMRMediaProcessor( + exif = exif, + fileHelper = fileHelper, + mediaHelper = mediaHelper, + imageHelper = imageHelper + ) + + companion object { + private const val JPEG = 0 + private const val PNG = 1 + private const val JPEG_TYPE = "jpg" + private const val IMAGE_MAX_RESOLUTION = 1080 + private const val IMAGE_MAX_QUALITY = 100 + private const val LOG_TAG = "IONCAMREditManager" + } + + /** + * Opens an activity with a UI to edit the provided image. + * @param activity Activity object that will be necessary to launch the edit activity. + * @param image String representing the image in Base64. + * @param launcher ActivityResultLauncher to use when launching the edit activity + */ + fun editImage( + activity: Activity?, + image: String, + launcher: ActivityResultLauncher + ) { + val imageByteArray: ByteArray = Base64.decode(image, Base64.NO_WRAP) + val imageBitmap = imageHelper.base64toBitmap(imageByteArray) + + //Creates temp file + val inputFilePath = + createCaptureFile( + activity, + JPEG, + System.currentTimeMillis().toString() + "" + ).absolutePath + val inputFileUri = inputFilePath.toUri() + val inputFile = File(inputFilePath) + + try { + //Writes bitmap in temp file + imageHelper.writeBitmapToFile(imageBitmap, inputFile) + openCropActivity(activity, inputFileUri, launcher) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * Opens an activity with a UI to edit the provided image. + * @param activity Activity object that will be necessary to launch the edit activity. + * @param pictureFilePath File path of the image to edit. + * @param launcher ActivityResultLauncher to use when launching the edit activity + */ + fun editURIPicture( + activity: Activity?, + pictureFilePath: String, + launcher: ActivityResultLauncher, + onError: (IONCAMRError) -> Unit + ) { + val imageFile = File(pictureFilePath) + if (!fileHelper.fileExists(imageFile)) { + onError(IONCAMRError.FILE_DOES_NOT_EXIST_ERROR) + return + } + val drawable: Drawable? = try { + Drawable.createFromPath(pictureFilePath) + } catch (ex: Exception) { + ex.printStackTrace() + null + } + if (drawable == null) { + // provided file path does not seem to belong to an actual picture + // .e.g could be video, for which edit is not supported + onError(IONCAMRError.FETCH_IMAGE_FROM_URI_ERROR) + return + } + val pictureUri = fileHelper.getUriForFile( + activity, + "$applicationId$authority", + imageFile + ) + try { + openCropActivity(activity, pictureUri, launcher) + } catch (e: Exception) { + e.printStackTrace() + onError(IONCAMRError.EDIT_IMAGE_ERROR) + } + } + + /** + * Opens the crop/edit activity with the provided image URI. + * @param activity Activity object that will be necessary to launch the edit activity. + * @param picUri URI of the picture to edit. + * @param launcher ActivityResultLauncher to use when launching the edit activity + */ + fun openCropActivity( + activity: Activity?, + picUri: Uri?, + launcher: ActivityResultLauncher + ) { + val cropIntent = Intent(activity, IONCAMRImageEditorActivity::class.java) + + // creates output file + croppedFilePath = createCaptureFile( + activity, + JPEG, + System.currentTimeMillis().toString() + "" + ).absolutePath + croppedUri = croppedFilePath?.toUri() + + cropIntent.putExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) + cropIntent.putExtra(IONCAMRImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) + + launcher.launch(cropIntent) + } + + /** + * Applies all needed transformation to the image received from the Edit screen. + * + * @param activity Activity object necessary to process the result. + * @param intent An intent, containing the file path of the image that was just edited. + * @param editParameters IONEditParameters object with parameters for edit + * @param onImage callback that will be used when base64 image should be returned. + * @param onMediaResult callback that will be used when MediaResult object should be returned. + * @param onError callback that will be used when an error occurs. + */ + fun processResultFromEdit( + activity: Activity, + intent: Intent?, + editParameters: IONCAMREditParameters, + onImage: (String) -> Unit, + onMediaResult: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + val resultImagePath = intent?.getStringExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) + if (resultImagePath.isNullOrEmpty()) { + Log.d(LOG_TAG, "Image file path is null or empty") + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + if (editParameters.fromUri) { + val imageFile = File(resultImagePath) + val resultImageUri = fileHelper.getUriForFile( + activity, + "$applicationId$authority", + imageFile + ) + if (resultImageUri == null) { + Log.d(LOG_TAG, "Image URI is null") + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + + var savedSuccessfully = false + + if (editParameters.saveToGallery) { + savedSuccessfully = mediaProcessor.savePictureInGallery( + activity, + if (fileHelper.getFileExtension(resultImagePath) == JPEG_TYPE) 0 else 1, + resultImageUri + ) + } + + mediaProcessor.processEditedImage( + activity = activity, + imagePath = resultImagePath, + uri = resultImageUri, + includeMetadata = editParameters.includeMetadata, + savedSuccessfully = savedSuccessfully, + onMediaResult = onMediaResult, + onError = onError + ) + + } else { + val result = imageHelper.decodeFile(resultImagePath) + imageHelper.bitmapToBase64( + result = result, + resolution = IMAGE_MAX_RESOLUTION, + quality = IMAGE_MAX_QUALITY, + onSuccess = { onImage(it) }, + onError = { onError(it) } + ) + } + } + + /** + * Create a file in the applications temporary directory based upon the supplied encoding. + * + * @param encodingType of the image to be taken + * @param fileName or resultant File object. + * @return a File object pointing to the temporary picture + */ + fun createCaptureFile( + activity: Activity?, + encodingType: Int, + fileName: String = "" + ): File { + return mediaProcessor.createCaptureFile( + activity = activity, + encodingType = encodingType, + fileName = fileName + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRGalleryManager.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRGalleryManager.kt new file mode 100644 index 0000000..8d99c71 --- /dev/null +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRGalleryManager.kt @@ -0,0 +1,414 @@ +package io.ionic.libs.ioncameralib.manager + +import android.app.Activity +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import io.ionic.libs.ioncameralib.helper.IONCAMRExifHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRImageHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRMediaHelperInterface +import io.ionic.libs.ioncameralib.model.IONCAMRCameraParameters +import io.ionic.libs.ioncameralib.model.IONCAMREditParameters +import io.ionic.libs.ioncameralib.model.IONCAMRError +import io.ionic.libs.ioncameralib.model.IONCAMRMediaResult +import io.ionic.libs.ioncameralib.model.IONCAMRMediaType +import io.ionic.libs.ioncameralib.processor.IONCAMRMediaProcessor +import io.ionic.libs.ioncameralib.view.IONCAMROpenPhotoPickerActivity +import io.ionic.libs.ioncameralib.view.IONCAMRLoadingActivity +import io.ionic.libs.ioncameralib.view.IONCAMRImageEditorActivity +import java.io.File + +class IONCAMRGalleryManager( + private var exif: IONCAMRExifHelperInterface, + private var fileHelper: IONCAMRFileHelperInterface, + private var mediaHelper: IONCAMRMediaHelperInterface, + private var imageHelper: IONCAMRImageHelperInterface +) { + private var croppedUri: Uri? = null + private var croppedFilePath: String? = null + private val mediaProcessor = IONCAMRMediaProcessor( + exif = exif, + fileHelper = fileHelper, + mediaHelper = mediaHelper, + imageHelper = imageHelper + ) + + companion object { + private const val JPEG = 0 + private const val JPEG_TYPE = "jpg" + private const val LOG_TAG = "IONCAMRGalleryManager" + private const val ALLOW_MULTIPLE = "allowMultiple" + private const val MEDIA_TYPE = "mediaType" + private const val MEDIA_LIMIT = "limit" + private const val EDIT_REQUEST_CODE = 7 + private const val IMAGE_MAX_RESOLUTION = 1080 + private const val IMAGE_MAX_QUALITY = 100 + } + + /** + * Opens a screen that allows users to select media from device gallery + * @param activity Activity object that will be necessary to launch the edit activity. + * @param mediaType The type of content the user is allowed to select. + * @param allowMultiSelect Whether or not the user should be allowed to select multiple items + * from gallery. + * @param launcher ActivityResultLauncher to use when launching the gallery activity + */ + fun chooseFromGallery( + activity: Activity, + mediaType: IONCAMRMediaType, + allowMultiSelect: Boolean, + limit: Int, + launcher: ActivityResultLauncher + ) { + try { + val intent = Intent(activity, IONCAMROpenPhotoPickerActivity::class.java).apply { + putExtra(ALLOW_MULTIPLE, allowMultiSelect) + putExtra(MEDIA_TYPE, mediaType.mimeType) + putExtra(MEDIA_LIMIT, limit) + + } + + launcher.launch(intent) + } catch (e: Exception) { + Log.e(LOG_TAG, e.message.toString()) + } + } + + /** + * Handles the result after users have selected media from device gallery. + * @param activity Activity object that will be necessary to launch the edit activity. + * @param resultCode The code resulting from the operation. + * @param intent The intent resulting from the operation + * @param onSuccess The code the be executed if the operation was successfully. + * Returns a list of media item results. + * @param onError he code the be executed if the operation was not successfully. + */ + suspend fun onChooseFromGalleryResult( + activity: Activity, + resultCode: Int, + intent: Intent?, + includeMetadata: Boolean = false, + onSuccess: (List) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + + if (intent == null) { + onError(IONCAMRError.CHOOSE_MULTIMEDIA_CANCELLED_ERROR) + return + } + + when (resultCode) { + + RESULT_OK -> { + + val uris = imageHelper.getResultUriFromIntent(intent) + + showLoadingScreen(activity) + + val results: MutableList = mutableListOf() + for (uri in uris) { + + var fileLocation = fileHelper.getRealPath(uri, activity) + + // when fileLocation = null means the file isn't available on the local filesystem. + + if (fileLocation == null) { + fileLocation = + fileHelper.getImagePathFromInputStreamUri(activity, uri) ?: continue + } + + val mediaResult = + createMediaResult(activity, fileLocation, uri, includeMetadata) + + if (mediaResult == null) { + onError(IONCAMRError.GENERIC_CHOOSE_MULTIMEDIA_ERROR) + dismissLoadingScreen(activity) + return + } + results.add(mediaResult) + } + + onSuccess(results) + dismissLoadingScreen(activity) + } + + RESULT_CANCELED -> { + onError(IONCAMRError.CHOOSE_MULTIMEDIA_CANCELLED_ERROR) + } + + else -> { + onError(IONCAMRError.GENERIC_CHOOSE_MULTIMEDIA_ERROR) + } + } + } + + + /** + * Handles the result after users have edited media from device gallery. + * @param activity Activity object that will be necessary to launch the edit activity. + * @param resultCode The code resulting from the operation. + * @param intent The intent resulting from the operation + * @param onSuccess The code the be executed if the operation was successfully. + * Returns a list of media item results. + * @param onError he code the be executed if the operation was not successfully. + */ + suspend fun onChooseFromGalleryEditResult( + activity: Activity, + resultCode: Int, + intent: Intent?, + includeMetadata: Boolean = false, + onSuccess: (List) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + when (resultCode) { + + RESULT_OK -> { + if (intent == null) { + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + + // An empty string here will trigger EDIT_IMAGE_ERROR later + val fileLocation = + intent.getStringExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) ?: "" + + val mediaResult = createMediaResult( + activity, + fileLocation, + fileHelper.getUriFromString(fileLocation), + includeMetadata + ) + if (mediaResult == null) { + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + onSuccess(listOf(mediaResult)) + } + + RESULT_CANCELED -> { + onError(IONCAMRError.EDIT_CANCELLED_ERROR) + } + + else -> { + onError(IONCAMRError.EDIT_IMAGE_ERROR) + } + } + } + + /** + * Create a file in the applications temporary directory based upon the supplied encoding. + * + * @param encodingType of the image to be taken + * @param fileName or resultant File object. + * @return a File object pointing to the temporary picture + */ + fun createCaptureFile( + activity: Activity?, + encodingType: Int, + fileName: String = "" + ): File { + return mediaProcessor.createCaptureFile( + activity = activity, + encodingType = encodingType, + fileName = fileName + ) + } + + /** + * Transforms the media item uri into a media result object. + * @param activity Activity object that will be necessary to launch the edit activity. + * @param filePath The uri for the media item. + * @return An object containing relevant information for the media item. + * Null if an error occurred. + */ + private suspend fun createMediaResult( + activity: Activity, + filePath: String, + uri: Uri, + includeMetadata: Boolean, + ): IONCAMRMediaResult? { + + val mimeType = fileHelper.getMimeType(filePath, activity) + val isImage = mimeType != null && mimeType.startsWith("image") + + return if (isImage) { + mediaProcessor.createImageMediaResult(activity, filePath, uri, includeMetadata, null) + } else { + mediaProcessor.createVideoMediaResult(activity, filePath, uri, includeMetadata) + } + } + + fun extractUris(intent: Intent?): List { + if (intent == null) return emptyList() + return imageHelper.getResultUriFromIntent(intent) + } + + private fun showLoadingScreen(activity: Activity) { + activity.startActivity(Intent(activity, IONCAMRLoadingActivity::class.java)) + } + + private fun dismissLoadingScreen(activity: Activity) { + activity.sendBroadcast(Intent(IONCAMRLoadingActivity.DISMISS_INTENT_FILTER)) + } + +// --------------------------------------------------------------------- +// Legacy API (startActivityForResult) – kept for backward compatibility +// --------------------------------------------------------------------- + + /** + * Get image from photo library. + * + * @param srcType The album to get image from. + * @param returnType Set the type of image to return. + */ + fun getImage( + activity: Activity?, + srcType: Int, + returnType: Int, + camParameters: IONCAMRCameraParameters + ) { + val intent = Intent() + croppedUri = null + croppedFilePath = null + intent.type = IONCAMRMediaType.PICTURE.mimeType + intent.action = Intent.ACTION_PICK + if (camParameters.allowEdit) { + intent.putExtra("crop", "true") + if (camParameters.targetWidth > 0) { + intent.putExtra("outputX", camParameters.targetWidth) + } + if (camParameters.targetHeight > 0) { + intent.putExtra("outputY", camParameters.targetHeight) + } + if (camParameters.targetHeight > 0 && camParameters.targetWidth > 0 && camParameters.targetWidth == camParameters.targetHeight) { + intent.putExtra("aspectX", 1) + intent.putExtra("aspectY", 1) + } + val croppedFile = createCaptureFile(activity, JPEG) + croppedFilePath = croppedFile.absolutePath + croppedUri = Uri.fromFile(croppedFile) + intent.putExtra(MediaStore.EXTRA_OUTPUT, croppedUri) + } + activity?.startActivityForResult(intent, (srcType + 1) * 16 + returnType + 1) + } + + fun openCropActivity(activity: Activity?, picUri: Uri?, requestCode: Int?, destType: Int?) { + val cropIntent = createCropIntent(activity, picUri) + var code = EDIT_REQUEST_CODE + if (requestCode != null && destType != null) { + code = requestCode + destType + } + activity?.startActivityForResult(cropIntent, code) + } + + private fun createCropIntent(activity: Activity?, picUri: Uri?): Intent { + val cropIntent = Intent(activity, IONCAMRImageEditorActivity::class.java) + croppedFilePath = createCaptureFile( + activity, + JPEG, + System.currentTimeMillis().toString() + "" + ).absolutePath + croppedUri = Uri.parse(croppedFilePath) + + cropIntent.putExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) + cropIntent.putExtra(IONCAMRImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) + return cropIntent + } + + /** + * Applies all needed transformation to the image received from the gallery. + * + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + */ + fun processResultFromGallery( + activity: Activity?, + intent: Intent, + camParameters: IONCAMRCameraParameters, + onSuccess: (String) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + mediaProcessor.processResultFromGallery( + activity = activity, + intent = intent, + camParameters = camParameters, + onSuccess = onSuccess, + onError = onError + ) + } + + /** + * Applies all needed transformation to the image received from the Edit screen. + * + * @param intent An intent, containing the file path of the image that was just edited. + * @param fromUri Indicates if image editing was made from an input uri or base64. + * @param onImage callback that will be used when base64 image should be returned. + * @param onMediaResult callback that will be used when MediaResult object should be returned. + * @param onError callback that will be used when an error occurs. + */ + fun processResultFromEdit( + activity: Activity, + intent: Intent?, + editParameters: IONCAMREditParameters, + onImage: (String) -> Unit, + authority: String, + onMediaResult: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + val resultImagePath = intent?.getStringExtra(IONCAMRImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) + if (resultImagePath.isNullOrEmpty()) { + Log.d(LOG_TAG, "Image file path is null or empty") + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + if (editParameters.fromUri) { + val imageFile = File(resultImagePath) + val resultImageUri = fileHelper.getUriForFile( + activity, + authority, + imageFile + ) + if (resultImageUri == null) { + Log.d(LOG_TAG, "Image URI is null") + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + + val mediaResult = mediaProcessor.createImageMediaResult( + activity, + resultImagePath, + resultImageUri, + editParameters.includeMetadata, + null + ) + + if (mediaResult == null) { + Log.d(LOG_TAG, "MediaResult is null") + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + if (editParameters.saveToGallery) { + mediaProcessor.savePictureInGallery( + activity, + if (fileHelper.getFileExtension(resultImagePath) == JPEG_TYPE) 0 else 1, + resultImageUri + ) + } + onMediaResult(mediaResult) + } else { + val result = imageHelper.decodeFile(resultImagePath) + imageHelper.bitmapToBase64( + result = result, + resolution = IMAGE_MAX_RESOLUTION, + quality = IMAGE_MAX_QUALITY, + onSuccess = { onImage(it) }, + onError = { onError(it) } + ) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/VideoManager.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRVideoManager.kt similarity index 76% rename from src/main/kotlin/io/ionic/libs/ioncameralib/manager/VideoManager.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRVideoManager.kt index f33d457..d31c5f8 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/VideoManager.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/IONCAMRVideoManager.kt @@ -2,14 +2,13 @@ package io.ionic.libs.ioncameralib.manager import android.app.Activity import android.content.Intent -import android.util.Log -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface -import io.ionic.libs.ioncameralib.model.IONError +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelperInterface +import io.ionic.libs.ioncameralib.model.IONCAMRError import java.io.File -class VideoManager( +class IONCAMRVideoManager( private var authority: String, - private var fileHelper: OSCAMRFileHelperInterface, + private var fileHelper: IONCAMRFileHelperInterface, ) { /** @@ -21,18 +20,18 @@ class VideoManager( activity: Activity, videoUri: String, onSuccess: () -> Unit, - onError: (IONError) -> Unit + onError: (IONCAMRError) -> Unit ) { val mimeType = fileHelper.getMimeType(videoUri) val file = File(videoUri) if (!fileHelper.fileExists(file)) { - onError(IONError.FILE_DOES_NOT_EXIST_ERROR) + onError(IONCAMRError.FILE_DOES_NOT_EXIST_ERROR) return } if (mimeType.isNullOrEmpty()) { - onError(IONError.MEDIA_PATH_ERROR) + onError(IONCAMRError.MEDIA_PATH_ERROR) return } diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/OSCAMRController.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/manager/OSCAMRController.kt deleted file mode 100644 index 239b65c..0000000 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/manager/OSCAMRController.kt +++ /dev/null @@ -1,1304 +0,0 @@ -package io.ionic.libs.ioncameralib.manager - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentValues -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory.Options -import android.graphics.Matrix -import android.media.ExifInterface -import android.media.MediaScannerConnection -import android.media.MediaScannerConnection.MediaScannerConnectionClient -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import android.util.Log -import io.ionic.libs.ioncameralib.helper.OSCAMRExifHelper -import io.ionic.libs.ioncameralib.helper.OSCAMRExifHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRImageHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRMediaHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRGalleryHelper -import io.ionic.libs.ioncameralib.model.IONError -import io.ionic.libs.ioncameralib.model.IONMediaMetadata -import io.ionic.libs.ioncameralib.model.IONMediaResult -import io.ionic.libs.ioncameralib.model.IONMediaType -import io.ionic.libs.ioncameralib.model.IONCameraParameters -import io.ionic.libs.ioncameralib.model.IONEditParameters -import io.ionic.libs.ioncameralib.view.ImageEditorActivity -import io.ionic.libs.ioncameralib.view.IONLoadingActivity -import io.ionic.libs.ioncameralib.view.IONOpenPhotoPickerActivity -import kotlinx.coroutines.Job -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream -import java.text.SimpleDateFormat -import java.util.Date - -class OSCAMRController( - private var applicationId: String, - private var exif: OSCAMRExifHelperInterface, - private var fileHelper: OSCAMRFileHelperInterface, - private var mediaHelper: OSCAMRMediaHelperInterface, - private var imageHelper: OSCAMRImageHelperInterface -) : MediaScannerConnectionClient { - - private var imageFilePath: String? = null - private var imageUri: Uri? = null - private var croppedUri: Uri? = null - private var croppedFilePath: String? = null - private var exifData: OSCAMRExifHelperInterface? = OSCAMRExifHelper() - private var orientationCorrected = false - - private var conn: MediaScannerConnection? = null - private var scanMe: Uri? = null - - private val job = Job() - private val TARGET_THUMBNAIL_DIMENSION: Int = 480 - - companion object { - private const val JPEG = 0 - private const val PNG = 1 - private const val JPEG_TYPE = "jpg" - private const val PNG_TYPE = "png" - private const val JPEG_EXTENSION = ".$JPEG_TYPE" - private const val PNG_EXTENSION = ".$PNG_TYPE" - private const val PNG_MIME_TYPE = "image/png" - private const val JPEG_MIME_TYPE = "image/jpeg" - - private const val GET_PICTURE = "Get Picture" - - private const val TIME_FORMAT = "yyyyMMdd_HHmmss" - private const val LOG_TAG = "OSCAMRController" - - private const val CLOSING_INPUT_STREAM_ERROR = "Exception while closing file input stream." - - const val EDIT_REQUEST_CODE = 7 - const val EDIT_FROM_GALLERY_REQUEST_CODE = 11 - - private const val AUTHORITY = ".camera.provider" - - private const val PICTURE_NAMES_PREFIX = "PIC_" - private const val VIDEO_NAMES_PREFIX = "VID_" - private const val VIDEO_FORMAT = ".mp4" - private const val IMAGE_MAX_RESOLUTION = 1080 - private const val IMAGE_MAX_QUALITY = 100 - private const val STORE = "CameraStore" - private const val EDIT_FILE_NAME_KEY = "EditFileName" - private const val ALLOW_MULTIPLE = "allowMultiple" - private const val MEDIA_TYPE = "mediaType" - } - - /** - * Deletes all the videos that were captured and saved on the cache while the app was running. - * @param activity Activity to provide the context to delete the file. - */ - fun deleteVideoFilesFromCache(activity: Activity) { - fileHelper.getCachedFileNames(activity)?.keys?.let { fileNames -> - for (fileName in fileNames) { - fileHelper.deleteFileFromCache(activity, fileName) - fileHelper.removeFileNameFromPrefs(fileName, activity) - } - } - } - - /** - * Take a picture with the camera. - * @param activity Activity object that will be necessary to take the picture - * @param encodingType JPEG or PNG. - */ - fun takePicture(activity: Activity, returnType: Int, encodingType: Int) { - // Save filename to fetch later (needed when allowEdit is true) - val fileName = PICTURE_NAMES_PREFIX + SimpleDateFormat(TIME_FORMAT).format(Date()) - fileHelper.saveStringSharedPreferences(activity, EDIT_FILE_NAME_KEY, fileName) - - // Specify file so that large image is captured and returned - val photo: File = createCaptureFile( - activity, - encodingType, - fileName - ) - this.imageFilePath = photo.absolutePath - this.imageUri = fileHelper.getUriForFile(activity, "$applicationId.camera.provider", photo) - - mediaHelper.openDeviceCamera(activity, imageUri, returnType) - } - - /** - * Get image from photo library. - * - * @param srcType The album to get image from. - * @param returnType Set the type of image to return. - */ - fun getImage( - activity: Activity?, - srcType: Int, - returnType: Int, - camParameters: IONCameraParameters - ) { - val intent = Intent() - croppedUri = null - croppedFilePath = null - intent.type = IONMediaType.PICTURE.mimeType - intent.action = Intent.ACTION_PICK - if (camParameters.allowEdit) { - intent.putExtra("crop", "true") - if (camParameters.targetWidth > 0) { - intent.putExtra("outputX", camParameters.targetWidth) - } - if (camParameters.targetHeight > 0) { - intent.putExtra("outputY", camParameters.targetHeight) - } - if (camParameters.targetHeight > 0 && camParameters.targetWidth > 0 && camParameters.targetWidth == camParameters.targetHeight) { - intent.putExtra("aspectX", 1) - intent.putExtra("aspectY", 1) - } - val croppedFile = createCaptureFile(activity, JPEG) - croppedFilePath = croppedFile.absolutePath - croppedUri = Uri.fromFile(croppedFile) - intent.putExtra(MediaStore.EXTRA_OUTPUT, croppedUri) - } - activity?.startActivityForResult(intent, (srcType + 1) * 16 + returnType + 1) - } - - /** - * Opens an activity with a UI to edit the provided image. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param image String representing the image in Base64. - */ - /* fun editImage(activity: Activity?, image: String, requestCode: Int?, destType: Int?) { - // put the following code inside the OSCAMRImageHelper which deals with Bitmap related stuff - val imageByteArray: ByteArray = Base64.decode(image, Base64.NO_WRAP) - val imageBitmap = imageHelper.base64toBitmap(imageByteArray) - - //Creates temp file - val inputFilePath = - createCaptureFile( - activity, - JPEG, - System.currentTimeMillis().toString() + "" - ).absolutePath - val inputFileUri = Uri.parse(inputFilePath) - val inputFile = File(inputFilePath) - - try { - //Writes bitmap in temp file - imageHelper.writeBitmapToFile(imageBitmap, inputFile) - openCropActivity(activity, inputFileUri, requestCode, destType) - } catch (e: Exception) { - e.printStackTrace() - } - }*/ - - /** - * Opens an activity with a UI to edit the provided image. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param pictureFilePath File path of the image to edit. - */ - /* fun editURIPicture(activity: Activity?, pictureFilePath: String, requestCode: Int?, destType: Int?, onError: (OSCAMRError) -> Unit) { - val imageFile = File(pictureFilePath) - if (!fileHelper.fileExists(imageFile)) { - onError(OSCAMRError.FILE_DOES_NOT_EXIST_ERROR) - return - } - val drawable: Drawable? = try { - Drawable.createFromPath(pictureFilePath) - } catch (ex: Exception) { - ex.printStackTrace() - null - } - if (drawable == null) { - // provided file path does not seem to belong to an actual picture - // .e.g could be video, for which edit is not supported - onError(OSCAMRError.FETCH_IMAGE_FROM_URI_ERROR) - return - } - val pictureUri = fileHelper.getUriForFile( - activity, - "$applicationId$AUTHORITY", - imageFile - ) - try { - openCropActivity(activity, pictureUri, requestCode, destType) - } catch (e: Exception) { - e.printStackTrace() - onError(OSCAMRError.EDIT_IMAGE_ERROR) - } - }*/ - - - /** - * Calls the intent to open the device's camera to record a video. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param saveVideoToGallery Indicates if the recorded video should be saved to the device gallery - */ - fun captureVideo( - activity: Activity, - saveVideoToGallery: Boolean = false, - onError: (IONError) -> Unit - ) { - val videoFileUri = fileHelper.getUriForFile( - activity, - "$applicationId$AUTHORITY", - createVideoFile(activity) - ) - fileHelper.saveStringSharedPreferences(activity, STORE, videoFileUri.toString()) - val captureVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) - - if (mediaHelper.existsActivity(activity, captureVideoIntent)) { - /*mediaHelper.openDeviceVideo( - activity, - captureVideoIntent, - videoFileUri, - saveVideoToGallery - )*/ - } else { - Log.d(LOG_TAG, "Error: You don't have a default camera for recording video.") - onError(IONError.NO_CAMERA_AVAILABLE_ERROR) - } - } - - /** - * Opens a screen that allows users to select media from device gallery - * @param activity Activity object that will be necessary to launch the edit activity. - * @param mediaType The type of content the user is allowed to select. - * @param allowMultiSelect Whether or not the user should be allowed to select multiple items - * from gallery. - * @param requestCode Request code for receiving activity results. - */ - /* fun chooseFromGallery( - activity: Activity, - mediaType: OSCAMRMediaType, - allowMultiSelect: Boolean, - requestCode: Int - ) { - try { - activity.startActivityForResult( - Intent(activity, OSCAMROpenPhotoPickerActivity::class.java).apply { - putExtra(ALLOW_MULTIPLE, allowMultiSelect) - putExtra(MEDIA_TYPE, mediaType.mimeType) - }, - requestCode - ) - } catch (e: Exception) { - Log.e(LOG_TAG, e.message.toString()) - } - }*/ - - /** - * Handles the result after users have selected media from device gallery. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param resultCode The code resulting from the operation. - * @param intent The intent resulting from the operation - * @param onSuccess The code the be executed if the operation was successfully. - * Returns a list of media item results. - * @param onError he code the be executed if the operation was not successfully. - */ - /* suspend fun onChooseFromGalleryResult( - activity: Activity, - resultCode: Int, - intent: Intent?, - includeMetadata: Boolean = false, - allowEdit: Boolean = false, - galleryMediaType: OSCAMRMediaType, - onSuccess: (List) -> Unit, - onError: (OSCAMRError) -> Unit) { - - if (intent == null) { - onError(OSCAMRError.CHOOSE_MULTIMEDIA_CANCELLED_ERROR) - return - } - - when (resultCode) { - - RESULT_OK -> { - - val uris = imageHelper.getResultUriFromIntent(intent) - - if (allowEdit && uris.size == 1 && galleryMediaType == OSCAMRMediaType.IMAGE) { - /* - This "destType" needs to be zero so the actual request code is passed to - the activity. - This looks strange, but I'm not brave enough to change the "openCropActivity" - logic. So I'll just play along for now. - */ - openCropActivity(activity, uris.first(), EDIT_FROM_GALLERY_REQUEST_CODE, 0) - return - } - - showLoadingScreen(activity) - - val results: MutableList = mutableListOf() - for (uri in uris) { - - var fileLocation = fileHelper.getRealPath(uri, activity) - - // when fileLocation = null means the file isn't available on the local filesystem. - - if(fileLocation == null) { - fileLocation = fileHelper.getImagePathFromInputStreamUri(activity, uri) ?: continue - } - - val mediaResult = createMediaResult(activity, fileLocation, uri, includeMetadata) - if (mediaResult == null) { - onError(OSCAMRError.GENERIC_CHOOSE_MULTIMEDIA_ERROR) - return - } - results.add(mediaResult) - } - - onSuccess(results) - dismissLoadingScreen(activity) - } - RESULT_CANCELED -> { - onError(OSCAMRError.CHOOSE_MULTIMEDIA_CANCELLED_ERROR) - } - else -> { - onError(OSCAMRError.GENERIC_CHOOSE_MULTIMEDIA_ERROR) - } - } - }*/ - - /** - * Handles the result after users have edited media from device gallery. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param resultCode The code resulting from the operation. - * @param intent The intent resulting from the operation - * @param onSuccess The code the be executed if the operation was successfully. - * Returns a list of media item results. - * @param onError he code the be executed if the operation was not successfully. - */ - /* suspend fun onChooseFromGalleryEditResult( - activity: Activity, - resultCode: Int, - intent: Intent?, - includeMetadata: Boolean = false, - onSuccess: (List) -> Unit, - onError: (OSCAMRError) -> Unit - ) { - when (resultCode) { - - RESULT_OK -> { - if(intent == null) { - onError(OSCAMRError.EDIT_IMAGE_ERROR) - return - } - - // An empty string here will trigger EDIT_IMAGE_ERROR later - val fileLocation = intent.getStringExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) ?: "" - - val mediaResult = createMediaResult( - activity, - fileLocation, - fileHelper.getUriFromString(fileLocation), - includeMetadata - ) - if (mediaResult == null) { - onError(OSCAMRError.EDIT_IMAGE_ERROR) - return - } - onSuccess(listOf(mediaResult)) - } - RESULT_CANCELED -> { - onError(OSCAMRError.EDIT_CANCELLED_ERROR) - } - else -> { - onError(OSCAMRError.EDIT_IMAGE_ERROR) - } - } - }*/ - - /** - * Calls the intent to open the device's camera to record a video. - * @param activity Activity object that will be necessary to launch the video player activity. - * @param videoUri Location of the video file to be played, as String. - */ - fun playVideo( - activity: Activity, - videoUri: String, - onSuccess: () -> Unit, - onError: (IONError) -> Unit - ) { - val mimeType = fileHelper.getMimeType(videoUri) - val file = File(videoUri) - - if (!fileHelper.fileExists(file)) { - onError(IONError.FILE_DOES_NOT_EXIST_ERROR) - return - } - - if (mimeType.isNullOrEmpty()) { - onError(IONError.MEDIA_PATH_ERROR) - return - } - - val contentUri = fileHelper.getUriForFile(activity, activity.packageName + AUTHORITY, file) - val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(contentUri, mimeType) - intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY - intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - activity.startActivity(intent) - onSuccess - } - - /** - * Transforms the media item uri into a media result object. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param filePath The uri for the media item. - * @return An object containing relevant information for the media item. - * Null if an error occurred. - */ - /* private suspend fun createMediaResult( - activity: Activity, - filePath: String, - uri: Uri, - includeMetadata: Boolean, - ): OSCAMRMediaResult? { - - val mimeType = fileHelper.getMimeType(filePath, activity) - val isImage = mimeType != null && mimeType.startsWith("image") - - return if (isImage) { - createImageMediaResult(activity, filePath, uri, includeMetadata, null) - } else { - createVideoMediaResult(activity, filePath, uri, includeMetadata) - } - }*/ - - /** - * Transforms the image media item uri into a media result object. - * @param imagePath A string with the path for the image media item. - * @return An object containing relevant information for the media item. - * Null if an error occurred. - */ - private fun createImageMediaResult( - activity: Activity, - imagePath: String, - mediaUri: Uri, - includeMetadata: Boolean, - camParameters: IONCameraParameters? - ): IONMediaResult? { - var base64Image = "" - var error: IONError? = null - - val file = File(imagePath) - if (!fileHelper.fileExists(file)) return null - - //camParameters are only set when createImageMediaResult is called from processResultFromCamera - val image: Bitmap? = if (camParameters == null) { - val exifOrientation = this.exif.getOrientationFromExif(ExifInterface(imagePath)) - val rotationDegrees = exifToDegrees(exifOrientation) - val decodedImage = imageHelper.decodeFile(imagePath) - var rotationMatrix: Matrix? = null - - if (rotationDegrees != 0) { - rotationMatrix = Matrix().apply { setRotate(rotationDegrees.toFloat()) } - } - - imageHelper.transformBitmapWithMatrix(decodedImage, rotationMatrix) - } else { - getScaledAndRotatedBitmap(activity, imagePath, camParameters) - } - - if (image == null) return null - - val downsizedImage = imageHelper.downsizeBitmapIfNeeded(image, IMAGE_MAX_RESOLUTION) - val compressedImage = imageHelper.compressBitmap(downsizedImage, 100) - - imageHelper.bitmapToBase64( - compressedImage, - resolution = camParameters?.let { - minOf( - it.targetWidth, - it.targetHeight, - IMAGE_MAX_RESOLUTION - ) - } ?: IMAGE_MAX_RESOLUTION, - quality = camParameters?.mQuality ?: IMAGE_MAX_QUALITY, - onSuccess = { base64Image = it }, - onError = { error = it } - ) - - if (error != null) { - return null - } - - var metadata: IONMediaMetadata? = null - if (includeMetadata) { - val resolution = getMediaResolution(activity, true, imagePath, mediaUri) - metadata = IONMediaMetadata( - fileHelper.getFileSizeFromUri(activity, mediaUri), - null, - fileHelper.getFileExtension(imagePath), - resolution, - fileHelper.getFileCreationDate(file), - ) - } - - return IONMediaResult(IONMediaType.PICTURE.type, imagePath, base64Image, metadata, true) - } - - /** - * Transforms the media item uri into a media result object. - * @param activity Activity object that will be necessary to launch the edit activity. - * @param videoPath A string with the path for the video media item. - * @return An object containing relevant information for the media item. - * Null if an error occurred. - */ - /* private suspend fun createVideoMediaResult( - activity: Activity, - videoPath: String, - mediaUri: Uri, - includeMetadata: Boolean, - ): OSCAMRMediaResult? { - - val file = File(videoPath) - if (!fileHelper.fileExists(file)) return null - - val uri = fileHelper.getUriFromString(videoPath) - val base64Thumbnail = getVideoThumbnailBase64(activity, uri) ?: return null - - var metadata: IONMediaMetadata? = null - if (includeMetadata) { - val resolution = getMediaResolution(activity, false, videoPath, uri) - metadata = IONMediaMetadata( - fileHelper.getFileSizeFromUri(activity, mediaUri), - (mediaHelper.getVideoDuration(activity, uri).toDouble() / 1000).roundToInt(), - fileHelper.getFileExtension(videoPath), - resolution, - fileHelper.getFileCreationDate(file), - ) - } - return OSCAMRMediaResult(IONMediaType.VIDEO.type, videoPath, base64Thumbnail, metadata) - }*/ - - fun openCropActivity(activity: Activity?, picUri: Uri?, requestCode: Int?, destType: Int?) { - val cropIntent = Intent(activity, ImageEditorActivity::class.java) - - // creates output file - croppedFilePath = createCaptureFile( - activity, - JPEG, - System.currentTimeMillis().toString() + "" - ).absolutePath - croppedUri = Uri.parse(croppedFilePath) - - cropIntent.putExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS, croppedFilePath) - cropIntent.putExtra(ImageEditorActivity.IMAGE_INPUT_URI_EXTRAS, picUri.toString()) - var code = EDIT_REQUEST_CODE - if (requestCode != null && destType != null) { - code = requestCode + destType - } - activity?.startActivityForResult( - cropIntent, - code - ) - } - - /** - * Create a file in the applications temporary directory based upon the supplied encoding. - * - * @param encodingType of the image to be taken - * @param fileName or resultant File object. - * @return a File object pointing to the temporary picture - */ - fun createCaptureFile(activity: Activity?, encodingType: Int, fileName: String = ""): File { - var fileName = fileName - if (fileName.isEmpty()) { - fileName = ".Pic" - } - fileName = if (encodingType == JPEG) { - fileName + JPEG_EXTENSION - } else if (encodingType == PNG) { - fileName + PNG_EXTENSION - } else { - throw IllegalArgumentException("Invalid Encoding Type: $encodingType") - } - return fileHelper.createCaptureFile(activity, fileName) - } - - /** - * Create a video file in the applications temporary directory based upon the supplied encoding. - * - * @param fileName or resultant File object. - * @return a File object pointing to the temporary picture - */ - fun createVideoFile(activity: Activity?): File { - val fileName = - VIDEO_NAMES_PREFIX + SimpleDateFormat(TIME_FORMAT).format(Date()) + VIDEO_FORMAT - return fileHelper.createCaptureFile(activity, fileName) - } - - /** - * Applies all needed transformation to the image received from the camera. - * - * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). - */ - @Throws(IOException::class) - fun processResultFromCamera( - activity: Activity, - intent: Intent?, - camParameters: IONCameraParameters, - onImage: (String) -> Unit, - onMediaResult: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit - ) { - // Create an ExifHelper to save the exif data that is lost during compression - //no longer necessary, this will be passed by dependency injection through the constructor - //val exif = OSCAMRExifHelper() - val sourcePath = - if (camParameters.allowEdit && this.croppedUri != null) this.croppedFilePath else imageFilePath - if (camParameters.encodingType == JPEG) { - try { - exif.createInFile(sourcePath) - exif.readExifData() - } catch (e: IOException) { - e.printStackTrace() - } - } - var bitmap: Bitmap? - - // CB-5479 When this option is given the unchanged image should be saved - // in the gallery and the modified image is saved in the temporary directory - if (camParameters.saveToPhotoAlbum) { - val srcUri: Uri? = if (camParameters.allowEdit && this.croppedUri != null) { - croppedUri - } else { - imageUri - } - savePictureInGallery(activity, camParameters.encodingType, srcUri) - } - if (camParameters.latestVersion) { - val mediaResult = - sourcePath?.let { - val imageUri = fileHelper.getUriForFile( - activity, - "$applicationId$AUTHORITY", - File(sourcePath) - ) - if (imageUri == null) { - onError(IONError.TAKE_PHOTO_ERROR) - return - } - createImageMediaResult( - activity, - it, - imageUri, - camParameters.includeMetadata, - camParameters - ) - } - if (mediaResult == null) { - onError(IONError.TAKE_PHOTO_ERROR) - return - } - onMediaResult(mediaResult) - } else { - //get bitmap - bitmap = sourcePath?.let { getScaledAndRotatedBitmap(activity, it, camParameters) } - if (bitmap == null) { - // Try to get the bitmap from intent. - if (intent != null) { - try { - // getExtras can throw different exceptions - val extras = intent.extras - if (extras != null) { - bitmap = extras["data"] as Bitmap? - } - } catch (e: Exception) { - // Don't let the exception bubble up, bitmap will be null (check below) - } - } - } - //get base64 representation of bitmap - var processPictureError = false - imageHelper.processPicture( - bitmap, camParameters.encodingType, camParameters.mQuality, - { - onImage(it) - }, - { - processPictureError = true - onError(it) - } - ) - if (processPictureError) { - return - } - } - } - - - /** - * Applies all needed transformation to the image received from the gallery. - * - * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). - */ - fun processResultFromGallery( - activity: Activity?, - intent: Intent, - camParameters: IONCameraParameters, - onSuccess: (String) -> Unit, - onError: (IONError) -> Unit - ) { - var uri = intent.data - val fileLocation = fileHelper.getRealPath(uri, activity) - Log.d(LOG_TAG, "File location is: $fileLocation") - val uriString = fileHelper.getUriString(uri) - - var bitmap: Bitmap? = null - try { - bitmap = getScaledAndRotatedBitmap(activity, uriString, camParameters) - } catch (e: IOException) { - e.printStackTrace() - } - imageHelper.processPicture( - bitmap, camParameters.encodingType, camParameters.mQuality, - { - onSuccess(it) - }, - { - onError(it) - } - ) - bitmap?.recycle() - System.gc() - } - - /** - * Applies all needed transformation to the image received from the Edit screen. - * - * @param intent An intent, containing the file path of the image that was just edited. - * @param fromUri Indicates if image editing was made from an input uri or base64. - * @param onImage callback that will be used when base64 image should be returned. - * @param onMediaResult callback that will be used when MediaResult object should be returned. - * @param onError callback that will be used when an error occurs. - */ - fun processResultFromEdit( - activity: Activity, - intent: Intent?, - editParameters: IONEditParameters, - onImage: (String) -> Unit, - onMediaResult: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit - ) { - val resultImagePath = intent?.getStringExtra(ImageEditorActivity.IMAGE_OUTPUT_URI_EXTRAS) - if (resultImagePath.isNullOrEmpty()) { - Log.d(LOG_TAG, "Image file path is null or empty") - onError(IONError.EDIT_IMAGE_ERROR) - return - } - if (editParameters.fromUri) { - val imageFile = File(resultImagePath) - val resultImageUri = fileHelper.getUriForFile( - activity, - "$applicationId$AUTHORITY", - imageFile - ) - if (resultImageUri == null) { - Log.d(LOG_TAG, "Image URI is null") - onError(IONError.EDIT_IMAGE_ERROR) - return - } - val mediaResult = createImageMediaResult( - activity, - resultImagePath, - resultImageUri, - editParameters.includeMetadata, - null - ) - if (mediaResult == null) { - Log.d(LOG_TAG, "MediaResult is null") - onError(IONError.EDIT_IMAGE_ERROR) - return - } - if (editParameters.saveToGallery) { - savePictureInGallery( - activity, - if (fileHelper.getFileExtension(resultImagePath) == JPEG_TYPE) 0 else 1, - resultImageUri - ) - } - onMediaResult(mediaResult) - } else { - val result = imageHelper.decodeFile(resultImagePath) - imageHelper.bitmapToBase64( - result = result, - resolution = IMAGE_MAX_RESOLUTION, - quality = IMAGE_MAX_QUALITY, - onSuccess = { onImage(it) }, - onError = { onError(it) } - ) - } - } - - private fun savePictureInGallery(activity: Activity, encodingType: Int, srcUri: Uri?) { - val galleryPathVO: OSCAMRGalleryHelper = getPicturesPath(encodingType) - val fileFromGalleryPath = File(galleryPathVO.galleryPath) - val galleryUri = Uri.fromFile(fileFromGalleryPath) - - if (Build.VERSION.SDK_INT <= 28) { - writeTakenPictureToGalleryLowerThanAndroidQ(activity, srcUri, galleryUri) - } else { - writeTakenPictureToGalleryStartingFromAndroidQ( - activity, - srcUri, - galleryPathVO, - encodingType - ) - } - } - - /** - * Obtains the URI of the file containing the video that was just recorded and returns it. - * - * @param intent An Intent containing the video URI in the its data property. - */ - /*suspend fun processResultFromVideo( - activity: Activity, - uri: Uri?, - fromGallery: Boolean = false, - includeMetadata: Boolean = false, - onSuccess: (OSCAMRMediaResult) -> Unit, - onError: (IONError) -> Unit - ) { - if (uri == null || uri.path == null) { - onError(IONError.CAPTURE_VIDEO_ERROR) - return - } - - val thumbnail = getVideoThumbnailBase64(activity, uri) ?: "" - var videoFilePath: String? - if (fromGallery) { - videoFilePath = mediaHelper.getVideoPathFromUri(activity, uri) - } else { - val fileName = uri.path?.split("/")?.last() ?: "" - videoFilePath = fileHelper.getAbsoluteCachedFilePath(activity, fileName) - if (videoFilePath.isNotEmpty()) { - val fileName = videoFilePath.split("/").last() - fileHelper.storeFileNameInPrefs(fileName, activity) - } else { - onError(IONError.CAPTURE_VIDEO_ERROR) - } - } - val file = File(videoFilePath) - if (videoFilePath != null && fileHelper.fileExists(file)) { - var metadata: IONMediaMetadata? = null - if (includeMetadata) { - val resolution = getMediaResolution(activity, false, videoFilePath, uri) - metadata = IONMediaMetadata( - fileHelper.getFileSizeFromUri(activity, uri), - (mediaHelper.getVideoDuration(activity, uri) - .toDouble() / 1000).roundToInt(), - fileHelper.getFileExtension(videoFilePath), - resolution, - fileHelper.getFileCreationDate(file), - ) - } - val mediaResult = OSCAMRMediaResult( - IONMediaType.VIDEO.ordinal, - videoFilePath, - thumbnail, - metadata - ) - onSuccess(mediaResult) - } else { - onError(IONError.MEDIA_PATH_ERROR) - } - }*/ - - private fun getMediaResolution( - activity: Activity, - isImage: Boolean, - mediaPath: String, - uri: Uri - ): String { - var resolutionPair: Pair = - if (isImage) mediaHelper.getImageResolution(mediaPath) else mediaHelper.getVideoResolution( - activity, - uri - ) - val height = resolutionPair.first - val width = resolutionPair.second - return if (height >= width) "${height}x${width}" else "${width}x${height}" - } - - private suspend fun getVideoThumbnailBase64( - activity: Activity, - videoUri: Uri - ): String? { - return mediaHelper.getThumbnailBase64String(activity, videoUri, TARGET_THUMBNAIL_DIMENSION) - } - - fun onDestroy(activity: Activity) { - deleteVideoFilesFromCache(activity) - job.cancel() - } - - private fun getPicturesPath(encodingType: Int): OSCAMRGalleryHelper { - val timeStamp = - SimpleDateFormat(TIME_FORMAT).format( - Date() - ) - val imageFileName = - "IMG_" + timeStamp + if (encodingType == JPEG) JPEG_EXTENSION else PNG_EXTENSION - val storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES - ) - storageDir.mkdirs() - return OSCAMRGalleryHelper(storageDir.absolutePath, imageFileName) - } - - /** - * In the special case where the default width, height and quality are unchanged - * we just write the file out to disk saving the expensive Bitmap.compress function. - * - * @param src - * @throws FileNotFoundException - * @throws IOException - */ - @Throws(FileNotFoundException::class, IOException::class) - private fun writeUncompressedImage(activity: Activity?, src: Uri?, dest: Uri?) { - val fis: InputStream? = fileHelper.getInputStreamFromUriString(src.toString(), activity) - fileHelper.writeUncompressedImage(activity, fis, dest) - } - - /** - * Return a scaled and rotated bitmap based on the target width and height - * - * @param imageUrl - * @return - * @throws IOException - */ - @Throws(IOException::class) - private fun getScaledAndRotatedBitmap( - activity: Activity?, - imageUrl: String, - camParameters: IONCameraParameters - ): Bitmap? { - // If no new width or height were specified, and orientation is not needed return the original bitmap - if (camParameters.targetWidth <= 0 && camParameters.targetHeight <= 0 && !camParameters.correctOrientation) { - var fileStream: InputStream? = null - var image: Bitmap? = null - try { - fileStream = fileHelper.getInputStreamFromUriString(imageUrl, activity) - image = imageHelper.getBitmapForInputStream(fileStream) - } catch (e: Exception) { - Log.d(LOG_TAG, e.localizedMessage) - } finally { - if (fileStream != null) { - try { - fileStream.close() - } catch (e: IOException) { - Log.d( - LOG_TAG, - CLOSING_INPUT_STREAM_ERROR - ) - } - } - } - return image - } - - - /* Copy the input stream to a temporary file on the device. - We then use this temporary file to determine the width/height/orientation. - This is the only way to determine the orientation of the photo coming from 3rd party providers (Google Drive, Dropbox,etc) - This also ensures we create a scaled bitmap with the correct orientation - - We delete the temporary file once we are done - */ - val localFile: File? - val galleryUri: Uri? - var rotate = 0 - try { - val fileStream: InputStream? = - fileHelper.getInputStreamFromUriString(imageUrl, activity) - // Generate a temporary file - val timeStamp = - SimpleDateFormat(TIME_FORMAT).format( - Date() - ) - val fileName = - "IMG_" + timeStamp + if (camParameters.encodingType == JPEG) JPEG_EXTENSION else PNG_EXTENSION - localFile = File(activity?.let { fileHelper.getTempDirectoryPath(it) } + fileName) - galleryUri = Uri.fromFile(localFile) - fileHelper.writeUncompressedImage(activity, fileStream, galleryUri) - try { - // ExifInterface doesn't like the file:// prefix - val filePath = fileHelper.getUriString(galleryUri).replace("file://", "") - // read exifData of source - exifData?.createInFile(filePath) - exifData?.readExifData() - // Use ExifInterface to pull rotation information - if (camParameters.correctOrientation) { - rotate = - exifToDegrees(this.exif.getOrientationFromExif(ExifInterface(filePath))) - } - } catch (oe: Exception) { - Log.d( - LOG_TAG, - "Unable to read Exif data: $oe" - ) - rotate = 0 - } - } catch (e: Exception) { - Log.d( - LOG_TAG, - "Exception while getting input stream: $e" - ) - return null - } - return try { - // figure out the original width and height of the image - val options = Options() - options.inJustDecodeBounds = true - var fileStream: InputStream? = null - try { - fileStream = fileHelper.getInputStreamFromUriString( - fileHelper.getUriString(galleryUri), - activity - ) - imageHelper.decodeStream(fileStream, options) - } finally { - if (fileStream != null) { - try { - fileStream.close() - } catch (e: IOException) { - Log.d( - LOG_TAG, - CLOSING_INPUT_STREAM_ERROR - ) - } - } - } - - if (imageHelper.areOptionsZero(options)) { - return null - } - - // User didn't specify output dimensions, but they need orientation - if (camParameters.targetWidth <= 0 && camParameters.targetHeight <= 0) { - camParameters.targetWidth = imageHelper.getOutWidth(options) - camParameters.targetHeight = imageHelper.getOutHeight(options) - } - - // Setup target width/height based on orientation - val rotatedWidth: Int - val rotatedHeight: Int - var rotated = false - if (rotate == 90 || rotate == 270) { - rotatedWidth = imageHelper.getOutHeight(options) - rotatedHeight = imageHelper.getOutWidth(options) - rotated = true - } else { - rotatedWidth = imageHelper.getOutWidth(options) - rotatedHeight = imageHelper.getOutHeight(options) - } - - // determine the correct aspect ratio - val widthHeight: IntArray = calculateAspectRatio( - rotatedWidth, - rotatedHeight, - camParameters.targetWidth, - camParameters.targetHeight - ) - - // Load in the smallest bitmap possible that is closest to the size we want - options.inJustDecodeBounds = false - options.inSampleSize = calculateSampleSize( - rotatedWidth, rotatedHeight, - widthHeight[0], - widthHeight[1] - ) - var unscaledBitmap: Bitmap? - try { - fileStream = fileHelper.getInputStreamFromUriString( - fileHelper.getUriString(galleryUri), - activity - ) - unscaledBitmap = imageHelper.decodeStream(fileStream, options) - } finally { - if (fileStream != null) { - try { - fileStream.close() - } catch (e: IOException) { - Log.d( - LOG_TAG, - CLOSING_INPUT_STREAM_ERROR - ) - } - } - } - val scaledWidth = if (!rotated) widthHeight[0] else widthHeight[1] - val scaledHeight = if (!rotated) widthHeight[1] else widthHeight[0] - var scaledBitmap = - imageHelper.getScaledBitmap(unscaledBitmap, scaledWidth, scaledHeight) - if (scaledBitmap != unscaledBitmap) { - unscaledBitmap?.recycle() - } - if (camParameters.correctOrientation && rotate != 0) { - val matrix = Matrix() - matrix.setRotate(rotate.toFloat()) - try { - scaledBitmap = imageHelper.transformBitmapWithMatrix(scaledBitmap, matrix) - this.orientationCorrected = true - } catch (oom: OutOfMemoryError) { - this.orientationCorrected = false - } - } - scaledBitmap - } finally { - // delete the temporary copy - localFile?.delete() - } - } - - /** - * Maintain the aspect ratio so the resulting image does not look smooshed - * - * @param origWidth - * @param origHeight - * @return - */ - private fun calculateAspectRatio( - origWidth: Int, - origHeight: Int, - targetWidth: Int, - targetHeight: Int - ): IntArray { - var newWidth: Int = targetWidth - var newHeight: Int = targetHeight - - // If no new width or height were specified return the original bitmap - if (newWidth <= 0 && newHeight <= 0) { - newWidth = origWidth - newHeight = origHeight - } else if (newWidth > 0 && newHeight <= 0) { - newHeight = ((newWidth / origWidth.toDouble()) * origHeight).toInt() - } else if (newWidth <= 0 && newHeight > 0) { - newWidth = ((newHeight / origHeight.toDouble()) * origWidth).toInt() - } else { - val newRatio = newWidth / newHeight.toDouble() - val origRatio = origWidth / origHeight.toDouble() - if (origRatio > newRatio) { - newHeight = newWidth * origHeight / origWidth - } else if (origRatio < newRatio) { - newWidth = newHeight * origWidth / origHeight - } - } - val retVal = IntArray(2) - retVal[0] = if (newWidth > 0) newWidth else 1 - retVal[1] = if (newHeight > 0) newHeight else 1 - return retVal - } - - /** - * Figure out what ratio we can load our image into memory at while still being bigger than - * our desired width and height - * - * @param srcWidth - * @param srcHeight - * @param dstWidth - * @param dstHeight - * @return - */ - private fun calculateSampleSize( - srcWidth: Int, - srcHeight: Int, - dstWidth: Int, - dstHeight: Int - ): Int { - val srcAspect = srcWidth.toFloat() / srcHeight.toFloat() - val dstAspect = dstWidth.toFloat() / dstHeight.toFloat() - return if (srcAspect > dstAspect) { - srcWidth / dstWidth - } else { - srcHeight / dstHeight - } - } - - @Throws(IOException::class) - private fun writeTakenPictureToGalleryLowerThanAndroidQ( - activity: Activity?, - srcUri: Uri?, - galleryUri: Uri? - ) { - writeUncompressedImage(activity, srcUri, galleryUri) - fileHelper.refreshGallery(activity, galleryUri) - } - - @Throws(IOException::class) - private fun writeTakenPictureToGalleryStartingFromAndroidQ( - activity: Activity?, - srcUri: Uri?, - galleryPathVO: OSCAMRGalleryHelper, - encodingType: Int - ) { - // Starting from Android Q, working with the ACTION_MEDIA_SCANNER_SCAN_FILE intent is deprecated - // https://developer.android.com/reference/android/content/Intent#ACTION_MEDIA_SCANNER_SCAN_FILE - // we must start working with the MediaStore from Android Q on. - val resolver: ContentResolver? = activity?.contentResolver - val contentValues = ContentValues() - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, galleryPathVO.galleryFileName) - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, getMimetypeForFormat(encodingType)) - val galleryOutputUri = - resolver?.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) - val fileStream: InputStream? = - fileHelper.getInputStreamFromUriString( - srcUri.toString(), - activity - ) - fileHelper.writeUncompressedImage(activity, fileStream, galleryOutputUri) - } - - private fun exifToDegrees(exifOrientation: Int): Int { - return if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) { - 90 - } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) { - 180 - } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { - 270 - } else { - 0 - } - } - - /** - * Converts output image format int value to string value of mime type. - * @param outputFormat int Output format of camera API. - * Must be value of either JPEG or PNG constant - * @return String String value of mime type or empty string if mime type is not supported - */ - private fun getMimetypeForFormat(outputFormat: Int): String? { - if (outputFormat == PNG) return PNG_MIME_TYPE - return if (outputFormat == JPEG) JPEG_MIME_TYPE else "" - } - - override fun onScanCompleted(p0: String?, p1: Uri?) { - conn?.disconnect() - } - - override fun onMediaScannerConnected() { - try { - conn?.scanFile(scanMe.toString(), IONMediaType.PICTURE.mimeType) - } catch (e: IllegalStateException) { - Log.d( - LOG_TAG, - "Can't scan file in MediaScanner after taking picture" - ) - } - } - - /* private fun showLoadingScreen(activity: Activity) { - activity.startActivity(Intent(activity, LoadingActivity::class.java)) - } - - private fun dismissLoadingScreen(activity: Activity) { - activity.sendBroadcast(Intent(LoadingActivity.DISMISS_INTENT_FILTER)) - }*/ - -} \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCameraParameters.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRCameraParameters.kt similarity index 95% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCameraParameters.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRCameraParameters.kt index bf81abf..a37c735 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCameraParameters.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRCameraParameters.kt @@ -2,7 +2,7 @@ package io.ionic.libs.ioncameralib.model import com.google.gson.annotations.SerializedName -data class IONCameraParameters( +data class IONCAMRCameraParameters( @SerializedName("mQuality") val mQuality: Int, @SerializedName("targetWidth") var targetWidth: Int, @SerializedName("targetHeight") var targetHeight: Int, diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONEditParameters.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMREditParameters.kt similarity index 90% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONEditParameters.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMREditParameters.kt index 6af86e0..f8a5203 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONEditParameters.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMREditParameters.kt @@ -2,7 +2,7 @@ package io.ionic.libs.ioncameralib.model import com.google.gson.annotations.SerializedName -data class IONEditParameters( +data class IONCAMREditParameters( @SerializedName("editURI") var editURI: String?, @SerializedName("fromUri") var fromUri: Boolean, @SerializedName("saveToGallery") val saveToGallery: Boolean, diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONError.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRError.kt similarity index 97% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONError.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRError.kt index 58c8a98..08ee732 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONError.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRError.kt @@ -1,6 +1,6 @@ package io.ionic.libs.ioncameralib.model -enum class IONError(val code: Int, val description: String) { +enum class IONCAMRError(val code: Int, val description: String) { CAMERA_PERMISSION_DENIED_ERROR(3, "Couldn't access camera. Check your camera permissions and try again."), NO_IMAGE_SELECTED_ERROR(5, "No image selected."), GALLERY_PERMISSION_DENIED_ERROR(6, "Couldn't access your photo gallery because access wasn't provided."), diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaMetadata.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaMetadata.kt similarity index 78% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaMetadata.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaMetadata.kt index 54a4852..58b39fa 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaMetadata.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaMetadata.kt @@ -2,9 +2,9 @@ package io.ionic.libs.ioncameralib.model import com.google.gson.annotations.SerializedName -data class IONMediaMetadata( +data class IONCAMRMediaMetadata( @SerializedName("size") val size: Long? = 0, - @SerializedName("duration") val duration: Long? = 0, + @SerializedName("duration") val duration: Int? = 0, @SerializedName("format") val format: String? = "", @SerializedName("resolution") val resolution: String? = "", @SerializedName("creationDate") val creationDate: String? = "" diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaResult.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaResult.kt similarity index 74% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaResult.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaResult.kt index 465e44c..c6cc3b5 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaResult.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaResult.kt @@ -2,10 +2,10 @@ package io.ionic.libs.ioncameralib.model import com.google.gson.annotations.SerializedName -data class IONMediaResult( +data class IONCAMRMediaResult( @SerializedName("type") val type: Int, @SerializedName("uri") val uri: String, @SerializedName("thumbnail") val thumbnail: String?, - @SerializedName("metadata") val metadata: IONMediaMetadata?, + @SerializedName("metadata") val metadata: IONCAMRMediaMetadata?, @SerializedName("saved") val saved: Boolean, ) \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaType.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaType.kt similarity index 66% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaType.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaType.kt index ca2023c..81e5560 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONMediaType.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRMediaType.kt @@ -1,11 +1,11 @@ package io.ionic.libs.ioncameralib.model -enum class IONMediaType(val type: Int, val mimeType: String) { +enum class IONCAMRMediaType(val type: Int, val mimeType: String) { PICTURE(0, "image/*"), VIDEO(1, "video/*"), ALL(2, "*/*"); - companion object Companion { + companion object { fun fromValue(value: Int) = values().first { it.type == value } } } \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONVideoParameters.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRVideoParameters.kt similarity index 86% rename from src/main/kotlin/io/ionic/libs/ioncameralib/model/IONVideoParameters.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRVideoParameters.kt index 6cd1f4a..119a6ab 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONVideoParameters.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/model/IONCAMRVideoParameters.kt @@ -2,7 +2,7 @@ package io.ionic.libs.ioncameralib.model import com.google.gson.annotations.SerializedName -data class IONVideoParameters( +data class IONCAMRVideoParameters( @SerializedName("saveToGallery") val saveToGallery: Boolean, @SerializedName("includeMetadata") val includeMetadata: Boolean ) diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/processor/IONMediaProcessor.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/processor/IONCAMRMediaProcessor.kt similarity index 57% rename from src/main/kotlin/io/ionic/libs/ioncameralib/processor/IONMediaProcessor.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/processor/IONCAMRMediaProcessor.kt index 804c12e..3c35718 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/processor/IONMediaProcessor.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/processor/IONCAMRMediaProcessor.kt @@ -1,65 +1,59 @@ package io.ionic.libs.ioncameralib.processor import android.app.Activity +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory.Options import android.graphics.Matrix import android.media.ExifInterface import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore import android.util.Log -import io.ionic.libs.ioncameralib.helper.OSCAMRExifHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRImageHelperInterface -import io.ionic.libs.ioncameralib.helper.OSCAMRMediaHelperInterface -import io.ionic.libs.ioncameralib.model.IONCameraParameters -import io.ionic.libs.ioncameralib.model.IONError -import io.ionic.libs.ioncameralib.model.IONMediaMetadata -import io.ionic.libs.ioncameralib.model.IONMediaResult -import io.ionic.libs.ioncameralib.model.IONMediaType +import io.ionic.libs.ioncameralib.helper.IONCAMRExifHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRGalleryHelper +import io.ionic.libs.ioncameralib.helper.IONCAMRImageHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRMediaHelperInterface +import io.ionic.libs.ioncameralib.model.IONCAMRCameraParameters +import io.ionic.libs.ioncameralib.model.IONCAMRError +import io.ionic.libs.ioncameralib.model.IONCAMRMediaMetadata +import io.ionic.libs.ioncameralib.model.IONCAMRMediaResult +import io.ionic.libs.ioncameralib.model.IONCAMRMediaType import java.io.File +import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream import java.text.SimpleDateFormat import java.util.Date +import kotlin.math.roundToInt -class IONMediaProcessor( - private val exif: OSCAMRExifHelperInterface, - private val fileHelper: OSCAMRFileHelperInterface, - private val mediaHelper: OSCAMRMediaHelperInterface, - private val imageHelper: OSCAMRImageHelperInterface +class IONCAMRMediaProcessor( + private val exif: IONCAMRExifHelperInterface, + private val fileHelper: IONCAMRFileHelperInterface, + private val mediaHelper: IONCAMRMediaHelperInterface, + private val imageHelper: IONCAMRImageHelperInterface ) { private var orientationCorrected = false private val TARGET_THUMBNAIL_DIMENSION: Int = 480 - companion object Companion { + companion object { private const val JPEG = 0 private const val PNG = 1 private const val JPEG_TYPE = "jpg" private const val PNG_TYPE = "png" - private const val JPEG_EXTENSION = ".$JPEG_TYPE" - private const val PNG_EXTENSION = ".$PNG_TYPE" private const val PNG_MIME_TYPE = "image/png" private const val JPEG_MIME_TYPE = "image/jpeg" - - private const val GET_PICTURE = "Get Picture" - + private const val JPEG_EXTENSION = ".$JPEG_TYPE" + private const val PNG_EXTENSION = ".$PNG_TYPE" private const val TIME_FORMAT = "yyyyMMdd_HHmmss" - private const val LOG_TAG = "MediaProcessor" - + private const val LOG_TAG = "IONCAMRMediaProcessor" private const val CLOSING_INPUT_STREAM_ERROR = "Exception while closing file input stream." - - const val EDIT_REQUEST_CODE = 7 - const val EDIT_FROM_GALLERY_REQUEST_CODE = 11 - - private const val PICTURE_NAMES_PREFIX = "PIC_" - private const val VIDEO_NAMES_PREFIX = "VID_" - private const val VIDEO_FORMAT = ".mp4" private const val IMAGE_MAX_RESOLUTION = 1080 private const val IMAGE_MAX_QUALITY = 100 - private const val STORE = "CameraStore" - private const val EDIT_FILE_NAME_KEY = "EditFileName" - private const val ALLOW_MULTIPLE = "allowMultiple" - private const val MEDIA_TYPE = "mediaType" } /** @@ -73,11 +67,11 @@ class IONMediaProcessor( imagePath: String, mediaUri: Uri, includeMetadata: Boolean, - camParameters: IONCameraParameters?, + camParameters: IONCAMRCameraParameters?, saved: Boolean = false - ): IONMediaResult? { + ): IONCAMRMediaResult? { var base64Image = "" - var error: IONError? = null + var error: IONCAMRError? = null val file = File(imagePath) if (!fileHelper.fileExists(file)) return null @@ -123,10 +117,10 @@ class IONMediaProcessor( return null } - var metadata: IONMediaMetadata? = null + var metadata: IONCAMRMediaMetadata? = null if (includeMetadata) { val resolution = getMediaResolution(activity, true, imagePath, mediaUri) - metadata = IONMediaMetadata( + metadata = IONCAMRMediaMetadata( fileHelper.getFileSizeFromUri(activity, mediaUri), null, fileHelper.getFileExtension(imagePath), @@ -135,51 +129,122 @@ class IONMediaProcessor( ) } - return IONMediaResult(IONMediaType.PICTURE.type, imagePath, base64Image, metadata, saved) + return IONCAMRMediaResult(IONCAMRMediaType.PICTURE.type, imagePath, base64Image, metadata, saved) + } + + /** + * Transforms the image media item uri into a media result object. + * @param imagePath A string with the path for the image media item. + * @return An object containing relevant information for the media item. + * Null if an error occurred. + */ + private fun createEditedImageMediaResult( + activity: Activity, + imagePath: String, + mediaUri: Uri, + includeMetadata: Boolean, + saved: Boolean = false + ): IONCAMRMediaResult? { + var base64Image = "" + var error: IONCAMRError? = null + + val file = File(imagePath) + if (!fileHelper.fileExists(file)) return null + + val decodedImage = imageHelper.decodeFile(imagePath) + + if (decodedImage == null) return null + + val downsizedImage = imageHelper.downsizeBitmapIfNeeded( + decodedImage, + IMAGE_MAX_RESOLUTION + ) + val compressedImage = imageHelper.compressBitmap(downsizedImage, 100) + + imageHelper.bitmapToBase64( + compressedImage, + resolution = IMAGE_MAX_RESOLUTION, + quality = IMAGE_MAX_QUALITY, + onSuccess = { base64Image = it }, + onError = { error = it } + ) + + if (error != null) { + return null + } + + var metadata: IONCAMRMediaMetadata? = null + if (includeMetadata) { + metadata = IONCAMRMediaMetadata( + fileHelper.getFileSizeFromUri(activity, mediaUri), + null, + fileHelper.getFileExtension(imagePath), + "${decodedImage.height}x${decodedImage.width}", + fileHelper.getFileCreationDate(file), + ) + } + + return IONCAMRMediaResult(IONCAMRMediaType.PICTURE.type, imagePath, base64Image, metadata, saved) } fun processCameraImage( activity: Activity, - sourcePath: String, + intent: Intent?, + sourcePath: String?, authority: String, - camParameters: IONCameraParameters, + camParameters: IONCAMRCameraParameters, savedSuccessfully: Boolean, onImage: (String) -> Unit, - onMediaResult: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit + onMediaResult: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit ) { + var bitmap: Bitmap? if (camParameters.latestVersion) { - val imageUri = fileHelper.getUriForFile( - activity, - authority, - File(sourcePath) - ) - if (imageUri == null) { - onError(IONError.TAKE_PHOTO_ERROR) - return - } - val mediaResult = createImageMediaResult( - activity, - sourcePath, - imageUri, - camParameters.includeMetadata, - camParameters, - savedSuccessfully - ) + val mediaResult = + sourcePath?.let { + val imageUri = fileHelper.getUriForFile( + activity, + authority, + File(sourcePath) + ) + if (imageUri == null) { + onError(IONCAMRError.TAKE_PHOTO_ERROR) + return + } + createImageMediaResult( + activity, + it, + imageUri, + camParameters.includeMetadata, + camParameters, + savedSuccessfully + ) + } if (mediaResult == null) { - onError(IONError.TAKE_PHOTO_ERROR) + onError(IONCAMRError.TAKE_PHOTO_ERROR) return } onMediaResult(mediaResult) } else { //get bitmap - val bitmap = getScaledAndRotatedBitmap(activity, sourcePath, camParameters) + bitmap = sourcePath?.let { getScaledAndRotatedBitmap(activity, it, camParameters) } if (bitmap == null) { - onError(IONError.PROCESS_IMAGE_ERROR) - return + // Try to get the bitmap from intent. + if (intent != null) { + try { + // getExtras can throw different exceptions + val extras = intent.extras + if (extras != null) { + bitmap = extras["data"] as Bitmap? + } + } catch (e: Exception) { + // Don't let the exception bubble up, bitmap will be null (check below) + } + } } + //get base64 representation of bitmap imageHelper.processPicture( bitmap, camParameters.encodingType, camParameters.mQuality, @@ -193,6 +258,31 @@ class IONMediaProcessor( } } + fun processEditedImage( + activity: Activity, + imagePath: String, + uri: Uri, + includeMetadata: Boolean, + savedSuccessfully: Boolean, + onMediaResult: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + val mediaResult = createEditedImageMediaResult( + activity, + imagePath, + uri, + includeMetadata, + savedSuccessfully + ) + + if (mediaResult == null) { + onError(IONCAMRError.EDIT_IMAGE_ERROR) + return + } + + onMediaResult(mediaResult) + } + /** * Transforms the media item uri into a media result object. * @param activity Activity object that will be necessary to launch the edit activity. @@ -205,7 +295,7 @@ class IONMediaProcessor( videoPath: String, mediaUri: Uri, includeMetadata: Boolean, - ): IONMediaResult? { + ): IONCAMRMediaResult? { val file = File(videoPath) if (!fileHelper.fileExists(file)) return null @@ -213,19 +303,19 @@ class IONMediaProcessor( val uri = fileHelper.getUriFromString(videoPath) val base64Thumbnail = getVideoThumbnailBase64(activity, uri) ?: return null - var metadata: IONMediaMetadata? = null + var metadata: IONCAMRMediaMetadata? = null if (includeMetadata) { val resolution = getMediaResolution(activity, false, videoPath, uri) - metadata = IONMediaMetadata( + metadata = IONCAMRMediaMetadata( fileHelper.getFileSizeFromUri(activity, mediaUri), - mediaHelper.getVideoDuration(activity, uri), + (mediaHelper.getVideoDuration(activity, uri).toDouble() / 1000).roundToInt(), fileHelper.getFileExtension(videoPath), resolution, fileHelper.getFileCreationDate(file), ) } - return IONMediaResult( - IONMediaType.VIDEO.type, + return IONCAMRMediaResult( + IONCAMRMediaType.VIDEO.type, videoPath, base64Thumbnail, metadata, @@ -239,37 +329,37 @@ class IONMediaProcessor( uri: Uri, includeMetadata: Boolean, recordedInGallery: Boolean, - onSuccess: (IONMediaResult) -> Unit, - onError: (IONError) -> Unit + onSuccess: (IONCAMRMediaResult) -> Unit, + onError: (IONCAMRError) -> Unit ) { val file = File(videoPath) if (!fileHelper.fileExists(file)) { - onError(IONError.MEDIA_PATH_ERROR) + onError(IONCAMRError.MEDIA_PATH_ERROR) return } val thumbnail = getVideoThumbnailBase64(activity, uri) if (thumbnail == null) { - onError(IONError.CAPTURE_VIDEO_ERROR) + onError(IONCAMRError.CAPTURE_VIDEO_ERROR) return } - var metadata: IONMediaMetadata? = null + var metadata: IONCAMRMediaMetadata? = null if (includeMetadata) { val resolution = getMediaResolution(activity, false, videoPath, uri) - metadata = IONMediaMetadata( + metadata = IONCAMRMediaMetadata( fileHelper.getFileSizeFromUri(activity, uri), - mediaHelper.getVideoDuration(activity, uri), + (mediaHelper.getVideoDuration(activity, uri).toDouble() / 1000).roundToInt(), fileHelper.getFileExtension(videoPath), resolution, fileHelper.getFileCreationDate(file), ) } - val mediaResult = IONMediaResult( - IONMediaType.VIDEO.ordinal, + val mediaResult = IONCAMRMediaResult( + IONCAMRMediaType.VIDEO.ordinal, videoPath, thumbnail, metadata, @@ -278,6 +368,28 @@ class IONMediaProcessor( onSuccess(mediaResult) } + /** + * Create a file in the applications temporary directory based upon the supplied encoding. + * + * @param encodingType of the image to be taken + * @param fileName or resultant File object. + * @return a File object pointing to the temporary picture + */ + fun createCaptureFile(activity: Activity?, encodingType: Int, fileName: String = ""): File { + var fileName = fileName + if (fileName.isEmpty()) { + fileName = ".Pic" + } + fileName = if (encodingType == JPEG) { + fileName + JPEG_EXTENSION + } else if (encodingType == PNG) { + fileName + PNG_EXTENSION + } else { + throw IllegalArgumentException("Invalid Encoding Type: $encodingType") + } + return fileHelper.createCaptureFile(activity, fileName) + } + suspend fun getVideoThumbnailBase64( activity: Activity, @@ -298,7 +410,7 @@ class IONMediaProcessor( private fun getScaledAndRotatedBitmap( activity: Activity?, imageUrl: String, - camParameters: IONCameraParameters + camParameters: IONCAMRCameraParameters ): Bitmap? { // If no new width or height were specified, and orientation is not needed return the original bitmap if (camParameters.targetWidth <= 0 && camParameters.targetHeight <= 0 && !camParameters.correctOrientation) { @@ -570,4 +682,143 @@ class IONMediaProcessor( 0 } } + + internal fun savePictureInGallery(activity: Activity, encodingType: Int, srcUri: Uri?): Boolean { + return try { + val galleryPathVO: IONCAMRGalleryHelper = getPicturesPath(encodingType) + val fileFromGalleryPath = File(galleryPathVO.galleryPath) + val galleryUri = Uri.fromFile(fileFromGalleryPath) + + if (Build.VERSION.SDK_INT <= 28) { + writeTakenPictureToGalleryLowerThanAndroidQ(activity, srcUri, galleryUri) + } else { + writeTakenPictureToGalleryStartingFromAndroidQ( + activity, + srcUri, + galleryPathVO, + encodingType + ) + } + true + } catch (e: Exception) { + false + } + } + + private fun getPicturesPath(encodingType: Int): IONCAMRGalleryHelper { + val timeStamp = + SimpleDateFormat(TIME_FORMAT).format( + Date() + ) + val imageFileName = + "IMG_" + timeStamp + if (encodingType == JPEG) JPEG_EXTENSION else PNG_EXTENSION + val storageDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES + ) + storageDir.mkdirs() + return IONCAMRGalleryHelper(storageDir.absolutePath, imageFileName) + } + + @Throws(IOException::class) + private fun writeTakenPictureToGalleryLowerThanAndroidQ( + activity: Activity?, + srcUri: Uri?, + galleryUri: Uri? + ) { + writeUncompressedImage(activity, srcUri, galleryUri) + fileHelper.refreshGallery(activity, galleryUri) + } + + @Throws(IOException::class) + private fun writeTakenPictureToGalleryStartingFromAndroidQ( + activity: Activity?, + srcUri: Uri?, + galleryPathVO: IONCAMRGalleryHelper, + encodingType: Int + ) { + // Starting from Android Q, working with the ACTION_MEDIA_SCANNER_SCAN_FILE intent is deprecated + // https://developer.android.com/reference/android/content/Intent#ACTION_MEDIA_SCANNER_SCAN_FILE + // we must start working with the MediaStore from Android Q on. + val resolver: ContentResolver? = activity?.contentResolver + val contentValues = ContentValues() + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, galleryPathVO.galleryFileName) + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, getMimetypeForFormat(encodingType)) + contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) + val galleryOutputUri = + resolver?.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + val fileStream: InputStream? = + fileHelper.getInputStreamFromUriString( + srcUri.toString(), + activity + ) + fileHelper.writeUncompressedImage(activity, fileStream, galleryOutputUri) + } + + + /** + * In the special case where the default width, height and quality are unchanged + * we just write the file out to disk saving the expensive Bitmap.compress function. + * + * @param src + * @throws FileNotFoundException + * @throws IOException + */ + @Throws(FileNotFoundException::class, IOException::class) + private fun writeUncompressedImage(activity: Activity?, src: Uri?, dest: Uri?) { + val fis: InputStream? = fileHelper.getInputStreamFromUriString(src.toString(), activity) + fileHelper.writeUncompressedImage(activity, fis, dest) + } + + /** + * Converts output image format int value to string value of mime type. + * @param outputFormat int Output format of camera API. + * Must be value of either JPEG or PNG constant + * @return String String value of mime type or empty string if mime type is not supported + */ + private fun getMimetypeForFormat(outputFormat: Int): String? { + if (outputFormat == PNG) return PNG_MIME_TYPE + return if (outputFormat == JPEG) JPEG_MIME_TYPE else "" + } + + +// --------------------------------------------------------------------- +// Legacy API (startActivityForResult) – kept for backward compatibility +// --------------------------------------------------------------------- + + + /** + * Applies all needed transformation to the image received from the gallery. + * + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + */ + fun processResultFromGallery( + activity: Activity?, + intent: Intent, + camParameters: IONCAMRCameraParameters, + onSuccess: (String) -> Unit, + onError: (IONCAMRError) -> Unit + ) { + var uri = intent.data + val fileLocation = fileHelper.getRealPath(uri, activity) + Log.d(LOG_TAG, "File location is: $fileLocation") + val uriString = fileHelper.getUriString(uri) + + var bitmap: Bitmap? = null + try { + bitmap = getScaledAndRotatedBitmap(activity, uriString, camParameters) + } catch (e: IOException) { + e.printStackTrace() + } + imageHelper.processPicture( + bitmap, camParameters.encodingType, camParameters.mQuality, + { + onSuccess(it) + }, + { + onError(it) + } + ) + bitmap?.recycle() + System.gc() + } } \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageCropperView.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageCropperView.kt similarity index 99% rename from src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageCropperView.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageCropperView.kt index ac0cd54..5d233d6 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageCropperView.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageCropperView.kt @@ -8,7 +8,7 @@ import android.view.MotionEvent import android.view.View import kotlin.math.sqrt -class ImageCropperView @JvmOverloads constructor( +class IONCAMRImageCropperView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : View(context, attrs) { @@ -44,7 +44,7 @@ class ImageCropperView @JvmOverloads constructor( private var isImageReady: Boolean = false companion object { - private const val TAG = "ImageCropperView" + private const val TAG = "IONCAMRImageCropperView" private const val BORDER_WIDTH = 2 private const val DEFAULT_BACKGROUND_ALPHA = 0.7f diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageEditorActivity.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageEditorActivity.kt similarity index 90% rename from src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageEditorActivity.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageEditorActivity.kt index 7785300..a327af8 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageEditorActivity.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageEditorActivity.kt @@ -1,7 +1,6 @@ package io.ionic.libs.ioncameralib.view import android.net.Uri -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle @@ -10,9 +9,9 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding -class ImageEditorActivity : ComponentActivity() { +class IONCAMRImageEditorActivity : ComponentActivity() { - private val editorView by lazy { findViewById(getResourceId("id/imageEditorView")) } + private val editorView by lazy { findViewById(getResourceId("id/imageEditorView")) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageEditorView.kt b/src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageEditorView.kt similarity index 95% rename from src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageEditorView.kt rename to src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageEditorView.kt index 842fd02..eaa54e9 100644 --- a/src/main/kotlin/io/ionic/libs/ioncameralib/view/ImageEditorView.kt +++ b/src/main/kotlin/io/ionic/libs/ioncameralib/view/IONCAMRImageEditorView.kt @@ -25,10 +25,10 @@ import android.widget.ImageButton import android.widget.ImageView import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelper -import io.ionic.libs.ioncameralib.imageeditor.OSCAMRImageEditorControllerInterface -import io.ionic.libs.ioncameralib.imageeditor.OSCAMRImageEditorController -import io.ionic.libs.ioncameralib.helper.OSCAMRFileHelperInterface +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelper +import io.ionic.libs.ioncameralib.imageeditor.IONCAMRImageEditorControllerInterface +import io.ionic.libs.ioncameralib.imageeditor.IONCAMRImageEditorController +import io.ionic.libs.ioncameralib.helper.IONCAMRFileHelperInterface import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Default @@ -38,12 +38,12 @@ import java.io.InputStream import kotlin.math.floor -class ImageEditorView @JvmOverloads constructor( +class IONCAMRImageEditorView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : FrameLayout(context, attrs) { - private val cropView by lazy { findViewById(getResourceId("id/cropperView")) } + private val cropView by lazy { findViewById(getResourceId("id/cropperView")) } private val imageView by lazy { findViewById(getResourceId("id/imageView")) } private val cancelButton by lazy { findViewById