├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lassi │ │ └── app │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── lassi │ │ │ └── app │ │ │ ├── MainActivity.kt │ │ │ └── adapter │ │ │ └── SelectedMediaAdapter.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_rounded_button.xml │ │ ├── ic_audio.xml │ │ ├── ic_audio_placeholder.xml │ │ ├── ic_document.xml │ │ ├── ic_document_placeholder.xml │ │ ├── ic_image.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_video.xml │ │ ├── ic_video_cam_white.xml │ │ └── ic_video_placeholder.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── row_selected_media.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-es │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── lassi │ └── app │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lassi ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lassi │ │ ├── common │ │ ├── extenstions │ │ │ ├── ContextExt.kt │ │ │ ├── FlowExt.kt │ │ │ ├── ImageViewExts.kt │ │ │ ├── LiveDataExt.kt │ │ │ ├── ViewExts.kt │ │ │ └── ViewGroupExts.kt │ │ └── utils │ │ │ ├── DrawableUtils.kt │ │ │ ├── DurationUtils.kt │ │ │ ├── FilePickerUtils.kt │ │ │ ├── ImageUtils.kt │ │ │ ├── KeyUtils.kt │ │ │ ├── Logger.kt │ │ │ ├── TimerUtils.kt │ │ │ ├── ToastLength.kt │ │ │ ├── ToastUtils.kt │ │ │ └── UriHelper.kt │ │ ├── data │ │ ├── common │ │ │ ├── Response.kt │ │ │ ├── Result.kt │ │ │ ├── StartVideoContract.kt │ │ │ └── VideoRecord.kt │ │ ├── database │ │ │ └── MediaFileDatabase.kt │ │ ├── media │ │ │ ├── MiItemMedia.kt │ │ │ ├── MiMedia.kt │ │ │ ├── entity │ │ │ │ ├── AlbumCoverPathEntity.kt │ │ │ │ ├── DurationEntity.kt │ │ │ │ ├── MediaFileDao.kt │ │ │ │ ├── MediaFileEntity.kt │ │ │ │ └── SelectedMediaModel.kt │ │ │ └── repository │ │ │ │ ├── MediaRepositoryImpl.kt │ │ │ │ └── SelectedMediaRepositoryImpl.kt │ │ └── mediadirectory │ │ │ ├── Folder.kt │ │ │ └── FolderBucket.kt │ │ ├── domain │ │ ├── common │ │ │ ├── SafeObserver.kt │ │ │ └── SingleLiveEvent.kt │ │ └── media │ │ │ ├── LassiConfig.kt │ │ │ ├── LassiOption.kt │ │ │ ├── MediaRepository.kt │ │ │ ├── MediaType.kt │ │ │ ├── MultiLangModel.kt │ │ │ ├── SelectedMediaRepository.kt │ │ │ └── SortingOption.kt │ │ └── presentation │ │ ├── builder │ │ └── Lassi.kt │ │ ├── camera │ │ ├── CameraFragment.kt │ │ └── CameraViewModel.kt │ │ ├── cameraview │ │ ├── audio │ │ │ ├── Audio.java │ │ │ ├── Control.java │ │ │ ├── Facing.java │ │ │ ├── Flash.java │ │ │ ├── Gesture.java │ │ │ ├── GestureAction.java │ │ │ ├── Grid.java │ │ │ ├── Hdr.java │ │ │ ├── Mode.java │ │ │ ├── Preview.java │ │ │ ├── VideoCodec.java │ │ │ └── WhiteBalance.java │ │ ├── controls │ │ │ ├── AspectRatio.kt │ │ │ ├── CamcorderProfiles.java │ │ │ ├── Camera1.java │ │ │ ├── CameraController.java │ │ │ ├── CameraException.java │ │ │ ├── CameraListener.java │ │ │ ├── CameraMapper.java │ │ │ ├── CameraOptions.java │ │ │ ├── CameraView.java │ │ │ ├── Frame.java │ │ │ ├── FrameManager.java │ │ │ ├── FrameProcessor.java │ │ │ ├── FullPictureRecorder.java │ │ │ ├── FullVideoRecorder.java │ │ │ ├── Mapper.java │ │ │ ├── PictureRecorder.java │ │ │ ├── PictureResult.java │ │ │ ├── Size.java │ │ │ ├── SizeSelector.java │ │ │ ├── SnapshotPictureRecorder.java │ │ │ ├── SnapshotVideoRecorder.java │ │ │ ├── VideoRecorder.java │ │ │ └── VideoResult.java │ │ ├── preview │ │ │ ├── CameraPreview.java │ │ │ ├── GestureLayout.java │ │ │ ├── GlCameraPreview.java │ │ │ ├── GridLinesLayout.java │ │ │ ├── PinchGestureLayout.java │ │ │ ├── RendererThread.java │ │ │ ├── ScrollGestureLayout.java │ │ │ ├── SurfaceCameraPreview.java │ │ │ ├── TapGestureLayout.java │ │ │ └── TextureCameraPreview.java │ │ ├── utils │ │ │ ├── BitmapCallback.java │ │ │ ├── CameraLogger.java │ │ │ ├── CameraUtils.java │ │ │ ├── CropHelper.java │ │ │ ├── FileCallback.java │ │ │ ├── OrientationHelper.java │ │ │ ├── RotationHelper.java │ │ │ ├── SizeSelectors.java │ │ │ ├── Task.java │ │ │ └── WorkerHandler.java │ │ └── video │ │ │ ├── AudioMediaEncoder.java │ │ │ ├── ByteBufferPool.java │ │ │ ├── EglBaseSurface.java │ │ │ ├── EglCore.java │ │ │ ├── EglElement.java │ │ │ ├── EglViewport.java │ │ │ ├── EglWindowSurface.java │ │ │ ├── EncoderThread.java │ │ │ ├── InputBuffer.java │ │ │ ├── InputBufferPool.java │ │ │ ├── MediaCodecBuffers.java │ │ │ ├── MediaEncoder.java │ │ │ ├── MediaEncoderEngine.java │ │ │ ├── OutputBuffer.java │ │ │ ├── OutputBufferPool.java │ │ │ ├── Pool.java │ │ │ ├── TextureMediaEncoder.java │ │ │ └── VideoMediaEncoder.java │ │ ├── common │ │ ├── LassiBaseActivity.kt │ │ ├── LassiBaseFragment.kt │ │ ├── LassiBaseViewModel.kt │ │ ├── LassiBaseViewModelActivity.kt │ │ ├── LassiBaseViewModelFragment.kt │ │ └── decoration │ │ │ └── GridSpacingItemDecoration.kt │ │ ├── cropper │ │ ├── BitmapCroppingWorkerJob.kt │ │ ├── BitmapLoadingWorkerJob.kt │ │ ├── BitmapUtils.kt │ │ ├── CropException.kt │ │ ├── CropImage.kt │ │ ├── CropImageActivity.kt │ │ ├── CropImageAnimation.kt │ │ ├── CropImageContract.kt │ │ ├── CropImageContractOptions.kt │ │ ├── CropImageIntentChooser.kt │ │ ├── CropImageOptions.kt │ │ ├── CropImageView.kt │ │ ├── CropOverlayView.kt │ │ ├── CropWindowHandler.kt │ │ ├── CropWindowMoveHandler.kt │ │ ├── ParcelableUtils.kt │ │ └── utils │ │ │ ├── GetFilePathFromUri.kt │ │ │ └── GetUriForFile.kt │ │ ├── docs │ │ ├── DocsFragment.kt │ │ ├── DocsViewModel.kt │ │ └── DocsViewModelFactory.kt │ │ ├── media │ │ ├── MediaFragment.kt │ │ ├── SelectedMediaViewModel.kt │ │ └── adapter │ │ │ └── MediaAdapter.kt │ │ ├── mediadirectory │ │ ├── CropImageImpl.kt │ │ ├── FolderFragment.kt │ │ ├── FolderViewModel.kt │ │ ├── FolderViewModelFactory.kt │ │ ├── LassiMediaPickerActivity.kt │ │ ├── SelectedMediaViewModelFactory.kt │ │ └── adapter │ │ │ └── FolderAdapter.kt │ │ └── videopreview │ │ └── VideoPreviewActivity.kt │ └── res │ ├── anim │ ├── right_in.xml │ └── right_out.xml │ ├── drawable │ ├── focus_marker_fill.xml │ ├── focus_marker_outline.xml │ ├── ic_arrow_back_24.xml │ ├── ic_back_white.xml │ ├── ic_camera_white.xml │ ├── ic_checked_media.xml │ ├── ic_crop_image_menu_flip.xml │ ├── ic_crop_image_menu_rotate_left.xml │ ├── ic_crop_image_menu_rotate_right.xml │ ├── ic_done_white.xml │ ├── ic_flash_auto_white.xml │ ├── ic_flash_off_white.xml │ ├── ic_flash_on_white.xml │ ├── ic_flip_24.xml │ ├── ic_flip_camera_white.xml │ ├── ic_image_placeholder.xml │ ├── ic_rotate_left_24.xml │ ├── ic_rotate_right_24.xml │ ├── ic_sorting_background.xml │ ├── ic_sorting_foreground.xml │ ├── ic_tick_red.xml │ ├── shape_circle_red.xml │ └── shape_circle_white.xml │ ├── layout │ ├── activity_media_picker.xml │ ├── activity_video_preview.xml │ ├── cameraview_gl_view.xml │ ├── cameraview_layout_focus_marker.xml │ ├── cameraview_surface_view.xml │ ├── cameraview_texture_view.xml │ ├── crop_image_activity.xml │ ├── crop_image_view.xml │ ├── fragment_camera.xml │ ├── fragment_media_picker.xml │ ├── item_media.xml │ └── sorting_option.xml │ ├── menu │ ├── crop_image_menu.xml │ ├── crop_image_menu_old.xml │ ├── main.xml │ ├── media_picker_menu.xml │ └── video_preview_menu.xml │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── integer.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── provider_paths.xml ├── media ├── image-picker-camera.gif └── image-picker.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android ### 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # IntelliJ 38 | *.iml 39 | *.idea 40 | 41 | # Keystore files 42 | # Uncomment the following lines if you do not want to check your keystore files in. 43 | #*.jks 44 | #*.keystore 45 | 46 | # External native build folder generated in Android Studio 2.2 and later 47 | .externalNativeBuild 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | # fastlane 55 | fastlane/report.xml 56 | fastlane/Preview.html 57 | fastlane/screenshots 58 | fastlane/test_output 59 | fastlane/readme.md 60 | 61 | # lint 62 | lint/intermediates/ 63 | lint/generated/ 64 | lint/outputs/ 65 | lint/tmp/ 66 | # lint/reports/ 67 | 68 | ### AndroidStudio ### 69 | # Covers files to be ignored for android development using Android Studio. 70 | 71 | # Signing files 72 | .signing/ 73 | 74 | # Local configuration file (sdk path, etc) 75 | 76 | # Proguard folder generated by Eclipse 77 | 78 | # Log Files 79 | 80 | # Android Patch 81 | 82 | # External native build folder generated in Android Studio 2.2 and later 83 | 84 | # NDK 85 | obj/ 86 | 87 | # IntelliJ IDEA 88 | *.iws 89 | /out/ 90 | 91 | # OS-specific files 92 | .DS_Store 93 | .DS_Store? 94 | ._* 95 | .Spotlight-V100 96 | .Trashes 97 | ehthumbs.db 98 | Thumbs.db 99 | 100 | # Legacy Eclipse project files 101 | .classpath 102 | .project 103 | .cproject 104 | .settings/ 105 | 106 | # Mobile Tools for Java (J2ME) 107 | .mtj.tmp/ 108 | 109 | # Package Files # 110 | *.war 111 | *.ear 112 | 113 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 114 | hs_err_pid* 115 | 116 | # Package Files # 117 | *.nar 118 | *.zip 119 | *.tar.gz 120 | *.rar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mindinventory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'kotlin-parcelize' 6 | id 'org.jetbrains.kotlin.android' 7 | } 8 | 9 | 10 | android { 11 | namespace 'com.lassi.app' 12 | compileSdk 34 13 | defaultConfig { 14 | applicationId "com.lassi.app" 15 | minSdk 21 16 | targetSdk 34 17 | versionCode 1 18 | versionName "1.0" 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | multiDexEnabled true 21 | } 22 | buildTypes { 23 | debug { 24 | debuggable true 25 | minifyEnabled false 26 | shrinkResources false 27 | } 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_17 36 | targetCompatibility JavaVersion.VERSION_17 37 | } 38 | 39 | kotlinOptions { 40 | jvmTarget = '17' 41 | } 42 | 43 | buildFeatures { 44 | viewBinding = true 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation fileTree(dir: 'libs', include: ['*.jar']) 50 | implementation 'androidx.appcompat:appcompat:1.6.1' 51 | implementation 'androidx.core:core-ktx:1.10.1' 52 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 53 | testImplementation 'junit:junit:4.13.2' 54 | androidTestImplementation 'androidx.test:runner:1.5.2' 55 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 56 | implementation project(path: ':lassi') 57 | implementation 'androidx.recyclerview:recyclerview:1.3.1' 58 | implementation 'androidx.cardview:cardview:1.0.0' 59 | 60 | implementation 'com.github.bumptech.glide:glide:4.14.2' 61 | kapt 'com.github.bumptech.glide:compiler:4.14.2' 62 | 63 | implementation "androidx.multidex:multidex:2.0.1" 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/lassi/app/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.app 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getTargetContext() 20 | assertEquals("com.mimediapicker.app", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/lassi/app/adapter/SelectedMediaAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.app.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.lassi.app.databinding.RowSelectedMediaBinding 6 | import com.lassi.common.extenstions.loadImage 7 | import com.lassi.common.extenstions.toBinding 8 | import com.lassi.common.utils.ImageUtils 9 | import com.lassi.data.media.MiMedia 10 | 11 | class SelectedMediaAdapter(private val onItemClicked: (miMedia: MiMedia) -> Unit) : 12 | RecyclerView.Adapter() { 13 | 14 | private val selectedMedias = ArrayList() 15 | 16 | fun setList(selectedMedias: List?) { 17 | selectedMedias?.let { 18 | this.selectedMedias.clear() 19 | this.selectedMedias.addAll(selectedMedias) 20 | } 21 | notifyDataSetChanged() 22 | } 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder { 25 | return MediaViewHolder(parent.toBinding()) 26 | } 27 | 28 | override fun getItemCount() = this.selectedMedias.size 29 | 30 | override fun onBindViewHolder(holder: MediaViewHolder, position: Int) { 31 | holder.bind(this.selectedMedias[position]) 32 | } 33 | 34 | inner class MediaViewHolder(private val binding: RowSelectedMediaBinding) : 35 | RecyclerView.ViewHolder(binding.root) { 36 | fun bind(miMedia: MiMedia) { 37 | binding.ivSelectedMediaThumbnail.loadImage(ImageUtils.getThumb(miMedia)) 38 | binding.root.setOnClickListener { 39 | onItemClicked(miMedia) 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_audio.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_audio_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_document.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_document_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_image.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_cam_white.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_selected_media.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lassi sample 4 | Medios seleccionados 5 | Abrir selector de vídeos 6 | Abrir selector de imágenes 7 | Imagen 8 | Video 9 | Documento 10 | Audio 11 | "Abrir vista del sistema " 12 | Selector de fotos 13 | 14 | Ordenar por fecha 15 | Ascendente 16 | Descendente 17 | No se concede el permiso de la cámara. Por favor, permita que se configure. 18 | Cultivo 19 | Fotos y videos 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFC107 4 | #FFA000 5 | #FFC107 6 | #E3E3E3 7 | 8 | #E9333C 9 | #DFFF00 10 | #50C878 11 | #22223b 12 | #98c1d9 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Lassi sample 3 | Selected Media 4 | Open Video Picker 5 | Open Image Picker 6 | Image 7 | Video 8 | Document 9 | Audio 10 | Open System View 11 | Photo Picker 12 | 13 | Sort by Date 14 | Ascending 15 | Descending 16 | Camera permission is not granted. Please allow it from setting. 17 | Crop 18 | Photo & Video Picker 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/lassi/app/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.app 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | agp_version = '8.1.1' 6 | } 7 | ext.kotlin_version = '1.8.20' 8 | repositories { 9 | google() 10 | mavenCentral() 11 | maven { url "https://jitpack.io" } 12 | maven { 13 | url "https://plugins.gradle.org/m2/" 14 | } 15 | } 16 | dependencies { 17 | classpath "com.android.tools.build:gradle:$agp_version" 18 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 19 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 20 | // NOTE: Do not place your application dependencies here; they belong 21 | // in the individual module build.gradle files 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.defaults.buildfeatures.buildconfig=true 23 | android.nonTransitiveRClass=false 24 | android.nonFinalResIds=false 25 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 12 19:15:15 IST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lassi/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lassi/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'kotlin-parcelize' 6 | id 'org.jetbrains.kotlin.android' 7 | } 8 | 9 | group='com.github.Mindinventory' 10 | 11 | android { 12 | namespace 'com.lassi' 13 | compileSdk 34 14 | defaultConfig { 15 | minSdk 21 16 | targetSdk 34 17 | versionCode 30 18 | versionName "1.4.2" 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables.useSupportLibrary = true 21 | multiDexEnabled true 22 | } 23 | 24 | buildTypes { 25 | debug{ 26 | minifyEnabled false 27 | shrinkResources false 28 | } 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | 35 | compileOptions { 36 | sourceCompatibility JavaVersion.VERSION_17 37 | targetCompatibility JavaVersion.VERSION_17 38 | } 39 | 40 | kotlinOptions { 41 | jvmTarget = '17' 42 | } 43 | 44 | buildFeatures { 45 | viewBinding = true 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation fileTree(dir: 'libs', include: ['*.jar']) 51 | implementation 'androidx.appcompat:appcompat:1.6.1' 52 | implementation 'androidx.core:core-ktx:1.10.1' 53 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 54 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 55 | implementation "androidx.recyclerview:recyclerview:1.3.1" 56 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 57 | implementation 'com.github.livefront:bridge:v1.2.0' 58 | testImplementation 'junit:junit:4.13.2' 59 | 60 | // Rx 61 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 62 | implementation 'io.reactivex.rxjava2:rxjava:2.2.21' 63 | 64 | // Google material 65 | implementation 'com.google.android.material:material:1.9.0' 66 | 67 | // glide 68 | implementation 'com.github.bumptech.glide:glide:4.14.2' 69 | kapt 'com.github.bumptech.glide:compiler:4.14.2' 70 | implementation 'androidx.exifinterface:exifinterface:1.3.6' 71 | 72 | implementation "androidx.activity:activity-ktx:1.7.2" 73 | implementation 'androidx.fragment:fragment-ktx:1.6.1' 74 | 75 | implementation "androidx.multidex:multidex:2.0.1" 76 | 77 | // Room DB 78 | implementation 'androidx.room:room-runtime:2.5.2' 79 | kapt 'androidx.room:room-compiler:2.5.2' 80 | implementation 'androidx.room:room-ktx:2.5.2' 81 | 82 | //Coroutine 83 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' 84 | 85 | //Gson 86 | implementation 'com.google.code.gson:gson:2.10' 87 | } 88 | -------------------------------------------------------------------------------- /lassi/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keep class androidx.appcompat.widget.** { *; } 23 | -keep class com.google.gson.reflect.TypeToken 24 | -keep class * extends com.google.gson.reflect.TypeToken 25 | -keep public class * implements java.lang.reflect.Type -------------------------------------------------------------------------------- /lassi/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 22 | 23 | 26 | 32 | 40 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/extenstions/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.extenstions 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.net.Uri 6 | import android.provider.OpenableColumns 7 | 8 | fun Context.getFileName(uri: Uri): String? { 9 | val returnCursor: Cursor? = this.contentResolver.query(uri, null, null, null, null) 10 | val nameIndex: Int? = returnCursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) 11 | returnCursor?.moveToFirst() 12 | val name: String? = nameIndex?.let { returnCursor.getString(it) } 13 | returnCursor?.close() 14 | return name 15 | } 16 | 17 | fun Context.getFileSize(uri: Uri): Long { 18 | val returnCursor: Cursor? = this.contentResolver.query(uri, null, null, null, null) 19 | val sizeIndex: Int? = returnCursor?.getColumnIndex(OpenableColumns.SIZE) 20 | returnCursor?.moveToFirst() 21 | val size: Long? = sizeIndex?.let { returnCursor.getLong(it) } 22 | returnCursor?.close() 23 | return size ?: 0L 24 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/extenstions/FlowExt.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.extenstions 2 | 3 | import com.lassi.data.common.Result 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.catch 6 | import kotlin.experimental.ExperimentalTypeInference 7 | 8 | @OptIn(ExperimentalTypeInference::class) 9 | fun Flow>.catch(): Flow> { 10 | return this.catch { e -> 11 | emit(Result.Error(e)) 12 | } 13 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/extenstions/ImageViewExts.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.extenstions 2 | 3 | import android.widget.ImageView 4 | import com.bumptech.glide.Glide 5 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 6 | import com.bumptech.glide.request.transition.DrawableCrossFadeFactory 7 | import com.lassi.domain.media.LassiConfig 8 | 9 | 10 | fun ImageView.loadImage(source: String?) { 11 | val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build() 12 | Glide.with(context) 13 | .load(source ?: "") 14 | .error(LassiConfig.getConfig().errorDrawable) 15 | .placeholder(LassiConfig.getConfig().placeHolder) 16 | .transition(DrawableTransitionOptions.withCrossFade(factory)) 17 | .into(this) 18 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/extenstions/LiveDataExt.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.extenstions 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import com.lassi.common.utils.Logger 7 | import com.lassi.data.common.Response 8 | 9 | fun LiveData.safeObserve(owner: LifecycleOwner, observer: (T) -> Unit) { 10 | observe(owner) { it?.let(observer) ?: Logger.d("TAG", "Live data value is null") } 11 | } 12 | 13 | fun MutableLiveData>.setSuccess(data: T) = postValue(Response.Success(data)) 14 | 15 | fun MutableLiveData>.setLoading() = postValue(Response.Loading()) 16 | 17 | fun MutableLiveData>.setError(throwable: Throwable) = 18 | postValue(Response.Error(throwable)) 19 | 20 | fun MutableLiveData>.isLoading() = value is Response.Loading 21 | 22 | fun LiveData>.isLoading() = value is Response.Loading 23 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/extenstions/ViewExts.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.extenstions 2 | 3 | import android.view.View 4 | import androidx.core.view.isVisible 5 | 6 | fun View.invisible() { 7 | this.visibility = View.INVISIBLE 8 | } 9 | 10 | fun View.hide() { 11 | this.isVisible = false 12 | } 13 | 14 | fun View.show() { 15 | this.isVisible = true 16 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/extenstions/ViewGroupExts.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.extenstions 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.annotation.LayoutRes 7 | import androidx.viewbinding.ViewBinding 8 | 9 | fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View { 10 | return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot) 11 | } 12 | 13 | inline fun ViewGroup.toBinding(): V { 14 | return V::class.java.getMethod( 15 | "inflate", 16 | LayoutInflater::class.java, 17 | ViewGroup::class.java, 18 | Boolean::class.java 19 | ).invoke(null, LayoutInflater.from(context), this, false) as V 20 | } 21 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/DrawableUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.graphics.drawable.Drawable 6 | import androidx.annotation.DrawableRes 7 | import androidx.core.content.ContextCompat 8 | 9 | object DrawableUtils { 10 | fun changeIconColor(context: Context, @DrawableRes drawableRes: Int, color: Int): Drawable? { 11 | val iconDrawable = ContextCompat.getDrawable(context, drawableRes) 12 | iconDrawable?.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) 13 | return iconDrawable 14 | } 15 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/DurationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | object DurationUtils { 6 | fun getDuration(videoDuration: Long): String { 7 | var duration = videoDuration 8 | val hours = TimeUnit.MILLISECONDS.toHours(duration) 9 | duration -= TimeUnit.HOURS.toMillis(hours) 10 | val minutes = TimeUnit.MILLISECONDS.toMinutes(duration) 11 | duration -= TimeUnit.MINUTES.toMillis(minutes) 12 | val seconds = TimeUnit.MILLISECONDS.toSeconds(duration) 13 | val durationBuilder = StringBuilder() 14 | if (hours > 0) { 15 | durationBuilder.append(hours) 16 | .append(":") 17 | } 18 | if (minutes < 10) 19 | durationBuilder.append('0') 20 | durationBuilder.append(minutes) 21 | .append(":") 22 | if (seconds < 10) 23 | durationBuilder.append('0') 24 | durationBuilder.append(seconds) 25 | return durationBuilder.toString() 26 | } 27 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/FilePickerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.media.MediaScannerConnection 6 | import android.net.Uri 7 | import android.webkit.MimeTypeMap 8 | import java.io.File 9 | import java.io.FileOutputStream 10 | import java.io.InputStream 11 | import java.io.OutputStream 12 | import java.util.Locale 13 | 14 | object FilePickerUtils { 15 | 16 | fun contains(types: Array, path: String): Boolean { 17 | for (string in types) { 18 | if (path.lowercase(Locale.getDefault()).endsWith(string)) return true 19 | } 20 | return false 21 | } 22 | 23 | fun notifyGalleryUpdateNewFile( 24 | context: Context, 25 | filePath: String, 26 | mimeType: String = "image/*", 27 | onFileScanComplete: (uri: Uri?, path: String?) -> Unit 28 | ) { 29 | context.let { 30 | MediaScannerConnection.scanFile( 31 | it, 32 | arrayOf(filePath), 33 | arrayOf(mimeType) 34 | ) { path, _ -> 35 | onFileScanComplete(Uri.fromFile(File(path)), path) 36 | } 37 | } 38 | } 39 | 40 | private fun getFileExtension(context: Context, uri: Uri): String? = 41 | if (uri.scheme == ContentResolver.SCHEME_CONTENT) 42 | MimeTypeMap.getSingleton() 43 | .getExtensionFromMimeType(context.contentResolver.getType(uri)) 44 | else uri.path?.let { 45 | MimeTypeMap.getFileExtensionFromUrl( 46 | Uri.fromFile(File(it)).toString() 47 | ) 48 | } 49 | 50 | fun getFilePathFromUri(context: Context, uri: Uri, uniqueName: Boolean): String = 51 | if (uri.path?.contains("file://") == true) uri.path!! 52 | else getFileFromContentUri(context, uri, uniqueName).path 53 | 54 | private fun getFileFromContentUri( 55 | context: Context, 56 | contentUri: Uri, 57 | uniqueName: Boolean, 58 | ): File { 59 | // Preparing Temp file name 60 | val fileExtension = getFileExtension(context, contentUri) ?: "" 61 | val fileName = 62 | ("file" + if (uniqueName) System.currentTimeMillis() else "") + ".$fileExtension" 63 | 64 | // Creating Temp file 65 | val tempFile = File(context.cacheDir, fileName) 66 | tempFile.createNewFile() 67 | // Initialize streams 68 | var oStream: FileOutputStream? = null 69 | var inputStream: InputStream? = null 70 | 71 | try { 72 | oStream = FileOutputStream(tempFile) 73 | inputStream = context.contentResolver.openInputStream(contentUri) 74 | 75 | inputStream?.let { copy(inputStream, oStream) } 76 | oStream.flush() 77 | } catch (e: Exception) { 78 | e.printStackTrace() 79 | } finally { 80 | // Close streams 81 | inputStream?.close() 82 | oStream?.close() 83 | } 84 | return tempFile 85 | } 86 | 87 | private fun copy(source: InputStream, target: OutputStream) { 88 | val buf = ByteArray(8192) 89 | var length: Int 90 | while (source.read(buf).also { length = it } > 0) { 91 | target.write(buf, 0, length) 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/ImageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import com.lassi.data.media.MiMedia 4 | import com.lassi.domain.media.LassiConfig 5 | import com.lassi.domain.media.MediaType 6 | 7 | object ImageUtils { 8 | fun getThumb(miMedia: MiMedia): String? { 9 | return if (LassiConfig.getConfig().mediaType == MediaType.AUDIO) { 10 | miMedia.thumb 11 | } else { 12 | miMedia.path 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/KeyUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | object KeyUtils { 4 | const val REQUEST_PERMISSIONS_REQUEST_CODE = 102 5 | const val SELECTED_MEDIA = "selected_media" 6 | const val MEDIA_PREVIEW = "mediaPreview" 7 | const val SELECTED_FOLDER = "selectedFolder" 8 | const val VIDEO_PATH = "videoPath" 9 | const val DEFAULT_MEDIA_COUNT = 1 10 | const val DEFAULT_GRID_SIZE = 2 11 | const val MAX_GRID_SIZE = 4 12 | const val DEFAULT_DURATION = 0L 13 | const val ONE_SECOND_INTERVAL = 1000L 14 | const val FIVE_SECOND_INTERVAL = 5 * 1000L 15 | const val TEN_SECOND_INTERVAL = 10 * 1000L 16 | const val SETTINGS_REQUEST_CODE = 100 17 | const val DEFAULT_FILE_SIZE = 0L 18 | const val DESCENDING_ORDER = 0 19 | const val ASCENDING_ORDER = 1 20 | const val DEFAULT_ORDER = 2 21 | const val ERROR_EXCEEDING_MSG = "You are exceeding the defined Max limit." 22 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import android.util.Log 4 | import com.lassi.BuildConfig 5 | 6 | object Logger { 7 | fun d(tag: String, message: Any?) { 8 | if (BuildConfig.DEBUG) { 9 | Log.d(tag, message.toString()) 10 | } 11 | } 12 | 13 | fun e(tag: String, message: Any?) { 14 | if (BuildConfig.DEBUG) { 15 | Log.e(tag, message.toString()) 16 | } 17 | } 18 | 19 | fun i(tag: String, message: Any?) { 20 | if (BuildConfig.DEBUG) { 21 | Log.i(tag, message.toString()) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/TimerUtils.kt: -------------------------------------------------------------------------------- 1 | import java.text.SimpleDateFormat 2 | import java.util.* 3 | 4 | object TimerUtils { 5 | 6 | fun formatTimeInMinuteSecond(millisec: Long): String { 7 | val d = Date(millisec) 8 | val df = SimpleDateFormat("mm:ss", Locale.US) 9 | df.timeZone = TimeZone.getTimeZone("GMT") 10 | return df.format(d) 11 | } 12 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/ToastLength.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import android.widget.Toast 4 | 5 | sealed class ToastLength(val value: Int) { 6 | object Short : ToastLength(Toast.LENGTH_SHORT) 7 | object Long : ToastLength(Toast.LENGTH_LONG) 8 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | 6 | object ToastUtils { 7 | private lateinit var toast: Toast 8 | 9 | fun showToast(context: Context, message: String?, duration: ToastLength = ToastLength.Short) { 10 | if (ToastUtils::toast.isInitialized) 11 | toast.cancel() 12 | message?.let { 13 | toast = Toast.makeText(context, message, duration.value) 14 | toast.show() 15 | } 16 | } 17 | 18 | fun showToast(context: Context, message: Int, duration: ToastLength = ToastLength.Short) { 19 | if (ToastUtils::toast.isInitialized) toast.cancel() 20 | toast = Toast.makeText(context, message, duration.value) 21 | toast.show() 22 | } 23 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/common/utils/UriHelper.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.common.utils 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.graphics.Bitmap.CompressFormat 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.provider.MediaStore 9 | import android.webkit.MimeTypeMap 10 | 11 | object UriHelper { 12 | private fun getMediaType(uri: Uri, contentResolver: ContentResolver): String? { 13 | // Columns to retrieve from the media store 14 | val projection = arrayOf(MediaStore.MediaColumns.MIME_TYPE) 15 | 16 | // Query the content resolver for the media information 17 | contentResolver.query(uri, projection, null, null, null)?.use { cursor -> 18 | if (cursor.moveToFirst()) { 19 | val mimeTypeColumnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE) 20 | if (mimeTypeColumnIndex >= 0) { 21 | return cursor.getString(mimeTypeColumnIndex) 22 | } 23 | } 24 | } 25 | return null 26 | } 27 | 28 | fun isVideo(uri: Uri, contentResolver: ContentResolver): Boolean { 29 | val mimeType = getMediaType(uri, contentResolver) 30 | return mimeType?.startsWith("video/") == true 31 | } 32 | 33 | fun isPhoto(uri: Uri, contentResolver: ContentResolver): Boolean { 34 | val mimeType: String? = if (uri.scheme == ContentResolver.SCHEME_CONTENT) { 35 | contentResolver.getType(uri) 36 | } else { 37 | // For file:// URIs, manually get the MIME type based on the file extension 38 | val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()) 39 | MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) 40 | } 41 | val t = mimeType?.startsWith("image/") == true 42 | return t 43 | } 44 | 45 | fun getCompressFormatForUri(uri: Uri, context: Context): CompressFormat { 46 | val mimeType = context.contentResolver?.getType(uri) 47 | return when (mimeType) { 48 | "image/png" -> CompressFormat.PNG 49 | "image/jpeg" -> CompressFormat.JPEG 50 | "image/webp" -> if (Build.VERSION.SDK_INT >= 30) CompressFormat.WEBP_LOSSLESS else CompressFormat.WEBP 51 | else -> CompressFormat.JPEG 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/common/Response.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.common 2 | 3 | sealed class Response { 4 | data class Success(val item: T) : Response() 5 | data class Error(val throwable: Throwable) : Response() 6 | class Loading : Response() 7 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/common/Result.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.common 2 | 3 | sealed class Result { 4 | 5 | data class Success( 6 | val data: T 7 | ) : Result() 8 | 9 | data class Error(val throwable: Throwable) : Result() 10 | object Loading : Result() 11 | 12 | override fun toString(): String { 13 | return when (this) { 14 | is Success<*> -> "Success[data=$data]" 15 | is Error -> "Error[exception=$throwable]" 16 | Loading -> "Loading" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/common/StartVideoContract.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.common 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.activity.result.contract.ActivityResultContract 7 | import com.lassi.common.utils.KeyUtils 8 | import com.lassi.data.media.MiMedia 9 | import com.lassi.presentation.videopreview.VideoPreviewActivity 10 | 11 | class StartVideoContract : ActivityResultContract() { 12 | override fun createIntent(context: Context, videoPath: String?): Intent { 13 | val intent = Intent(context, VideoPreviewActivity::class.java) 14 | intent.putExtra(KeyUtils.VIDEO_PATH, videoPath) 15 | return intent 16 | } 17 | 18 | override fun parseResult(resultCode: Int, intent: Intent?): MiMedia? { 19 | if (resultCode != Activity.RESULT_OK) return null 20 | return intent?.getParcelableExtra(KeyUtils.MEDIA_PREVIEW) 21 | } 22 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/common/VideoRecord.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.common 2 | 3 | sealed class VideoRecord { 4 | data class Start(val item: T) : VideoRecord() 5 | 6 | class Timer(val item: String) : VideoRecord() 7 | 8 | class End : VideoRecord() 9 | 10 | data class Error(val minVideoTime: String) : VideoRecord() 11 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/database/MediaFileDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.lassi.data.media.entity.AlbumCoverPathEntity 8 | import com.lassi.data.media.entity.DurationEntity 9 | import com.lassi.data.media.entity.MediaFileDao 10 | import com.lassi.data.media.entity.MediaFileEntity 11 | 12 | @Database( 13 | entities = arrayOf( 14 | MediaFileEntity::class, 15 | DurationEntity::class, 16 | AlbumCoverPathEntity::class 17 | ), version = 1, exportSchema = false 18 | ) 19 | abstract class MediaFileDatabase : RoomDatabase() { 20 | abstract fun mediaFileDao(): MediaFileDao 21 | 22 | companion object { 23 | @Volatile 24 | private var INSTANCE: MediaFileDatabase? = null 25 | private val LOCK = Any() 26 | 27 | operator fun invoke(context: Context) = INSTANCE ?: synchronized(LOCK) { 28 | buildDatabase(context).also { 29 | INSTANCE = it 30 | } 31 | } 32 | 33 | private fun buildDatabase(context: Context): MediaFileDatabase = Room.databaseBuilder( 34 | context, MediaFileDatabase::class.java, "media_file_database" 35 | ).build() 36 | } 37 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/media/MiItemMedia.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.media 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class MiItemMedia( 8 | var bucketName: String? = null, 9 | var latestItemPathForBucket: String? = null, 10 | var totalItemSizeForBucket: Long = 0L 11 | ) : Parcelable 12 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/media/MiMedia.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.media 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class MiMedia( 8 | var id: Long = 0, 9 | var name: String? = null, 10 | var path: String? = null, 11 | var duration: Long = 0L, 12 | var thumb: String? = null, 13 | var fileSize: Long = 0L, 14 | var doesUri:Boolean = false 15 | ) : Parcelable 16 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/media/entity/AlbumCoverPathEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.media.entity 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.ForeignKey 7 | import androidx.room.PrimaryKey 8 | import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_ENTITY 9 | import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_MEDIA_ID 10 | import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID 11 | import kotlinx.parcelize.Parcelize 12 | 13 | @Parcelize 14 | @Entity( 15 | tableName = ALBUM_COVER_ENTITY, 16 | foreignKeys = arrayOf( 17 | ForeignKey( 18 | entity = MediaFileEntity::class, 19 | parentColumns = arrayOf(MEDIA_ID), 20 | childColumns = arrayOf(ALBUM_COVER_MEDIA_ID), 21 | onDelete = ForeignKey.CASCADE 22 | ) 23 | ) 24 | ) 25 | data class AlbumCoverPathEntity( 26 | @PrimaryKey 27 | @ColumnInfo(name = ALBUM_COVER_MEDIA_ID) 28 | var mediaId: Long, 29 | 30 | @ColumnInfo(name = ALBUM_COVER_MEDIA_PATH, defaultValue = "default_media_album_cover_path") 31 | var mediaAlbumCoverPath: String, 32 | 33 | ) : Parcelable { 34 | companion object { 35 | const val ALBUM_COVER_ENTITY = "album_cover" 36 | const val ALBUM_COVER_MEDIA_ID = "album_cover_media_id" 37 | const val ALBUM_COVER_MEDIA_PATH = "album_cover_path" 38 | } 39 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/media/entity/DurationEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.media.entity 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.ForeignKey 7 | import androidx.room.ForeignKey.Companion.CASCADE 8 | import androidx.room.PrimaryKey 9 | import com.lassi.data.media.entity.DurationEntity.Companion.DURATION_ENTITY 10 | import com.lassi.data.media.entity.DurationEntity.Companion.DURATION_MEDIA_ID 11 | import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID 12 | import kotlinx.parcelize.Parcelize 13 | 14 | @Parcelize 15 | @Entity( 16 | tableName = DURATION_ENTITY, 17 | foreignKeys = arrayOf( 18 | ForeignKey( 19 | entity = MediaFileEntity::class, 20 | parentColumns = arrayOf(MEDIA_ID), 21 | childColumns = arrayOf(DURATION_MEDIA_ID), 22 | onDelete = CASCADE 23 | ) 24 | ) 25 | ) 26 | data class DurationEntity( 27 | @PrimaryKey 28 | @ColumnInfo(name = DURATION_MEDIA_ID) 29 | var mediaId: Long, 30 | 31 | @ColumnInfo(name = DURATION_MEDIA_DURATION, defaultValue = "default_media_duration") 32 | var mediaDuration: Long, 33 | 34 | ) : Parcelable { 35 | companion object { 36 | const val DURATION_ENTITY = "duration" 37 | const val DURATION_MEDIA_ID = "duration_media_id" 38 | const val DURATION_MEDIA_DURATION = "media_duration" 39 | } 40 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/media/entity/MediaFileEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.media.entity 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.Index 7 | import androidx.room.PrimaryKey 8 | import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_FILE_ENTITY 9 | import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID 10 | import kotlinx.parcelize.Parcelize 11 | 12 | @Parcelize 13 | @Entity(tableName = MEDIA_FILE_ENTITY, indices = [Index(value = [MEDIA_ID], unique = true)]) 14 | data class MediaFileEntity( 15 | 16 | @PrimaryKey 17 | @ColumnInfo(name = MEDIA_ID) 18 | var mediaId: Long, 19 | 20 | @ColumnInfo(name = MEDIA_NAME) 21 | var mediaName: String, 22 | 23 | @ColumnInfo(name = MEDIA_PATH) 24 | var mediaPath: String, 25 | 26 | @ColumnInfo(name = MEDIA_BUCKET, defaultValue = "default_media_bucket") 27 | var mediaBucket: String, 28 | 29 | @ColumnInfo(name = MEDIA_SIZE) 30 | var mediaSize: Long, 31 | 32 | @ColumnInfo(name = MEDIA_DATE_ADDED) 33 | var mediaDateAdded: Long, 34 | 35 | @ColumnInfo(name = MEDIA_TYPE) 36 | var mediaType: Int, 37 | 38 | ) : Parcelable { 39 | companion object { 40 | const val MEDIA_FILE_ENTITY = "media" 41 | const val MEDIA_ID = "media_id" 42 | const val MEDIA_NAME = "media_name" 43 | const val MEDIA_PATH = "media_path" 44 | const val MEDIA_BUCKET = "media_bucket" 45 | const val MEDIA_SIZE = "media_size" 46 | const val MEDIA_TYPE = "media_type" 47 | const val MEDIA_DATE_ADDED = "media_date_added" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/media/entity/SelectedMediaModel.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.media.entity 2 | 3 | import android.os.Parcelable 4 | import com.google.gson.annotations.SerializedName 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class SelectedMediaModel( 9 | @SerializedName("mediaId") 10 | var mediaId: Long, 11 | @SerializedName("mediaName") 12 | var mediaName: String, 13 | @SerializedName("mediaPath") 14 | var mediaPath: String, 15 | @SerializedName("mediaSize") 16 | var mediaSize: Long, 17 | @SerializedName("mediaDuration") 18 | var mediaDuration: Long, 19 | @SerializedName("mediaAlbumCoverPath") 20 | var mediaAlbumCoverPath: String, 21 | ) : Parcelable 22 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/mediadirectory/Folder.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.mediadirectory 2 | 3 | import android.os.Parcelable 4 | import com.lassi.data.media.MiMedia 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class Folder(var folderName: String?, var medias: ArrayList = ArrayList()) : 9 | Parcelable 10 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/data/mediadirectory/FolderBucket.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.data.mediadirectory 2 | 3 | import android.os.Parcelable 4 | import com.lassi.data.media.MiMedia 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class FolderBucket( 9 | var bucketName: String?, 10 | var totalItems: ArrayList = ArrayList(), 11 | var lastImagePath: String? 12 | ) : Parcelable 13 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/common/SafeObserver.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.domain.common 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | class SafeObserver(private val notifier: (T) -> Unit) : Observer { 6 | override fun onChanged(value: T) { 7 | value?.let { notifier(value) } 8 | } 9 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/common/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lassi.domain.common 18 | 19 | import android.util.Log 20 | import androidx.annotation.MainThread 21 | import androidx.lifecycle.LifecycleOwner 22 | import androidx.lifecycle.MutableLiveData 23 | import androidx.lifecycle.Observer 24 | 25 | import java.util.concurrent.atomic.AtomicBoolean 26 | 27 | class SingleLiveEvent : MutableLiveData() { 28 | 29 | private val mPending = AtomicBoolean(false) 30 | private val logTag = SingleLiveEvent::class.java.simpleName 31 | 32 | 33 | override fun observe(owner: LifecycleOwner, observer: Observer) { 34 | if (hasActiveObservers()) { 35 | Log.w(logTag, "Multiple observers registered but only one will be notified of changes.") 36 | } 37 | 38 | // Observe the internal MutableLiveData 39 | super.observe(owner, Observer { t -> 40 | if (mPending.compareAndSet(true, false)) { 41 | observer.onChanged(t) 42 | } 43 | }) 44 | } 45 | 46 | @MainThread 47 | override fun setValue(t: T?) { 48 | mPending.set(true) 49 | super.setValue(t) 50 | } 51 | 52 | /** 53 | * Used for cases where T is Void, to make calls cleaner. 54 | */ 55 | @MainThread 56 | fun call() { 57 | value = null 58 | } 59 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/media/LassiOption.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.domain.media 2 | 3 | enum class LassiOption { 4 | CAMERA, 5 | GALLERY, 6 | CAMERA_AND_GALLERY 7 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/media/MediaRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.domain.media 2 | 3 | import com.lassi.data.common.Result 4 | import com.lassi.data.media.MiItemMedia 5 | import com.lassi.data.media.MiMedia 6 | import com.lassi.data.media.entity.MediaFileEntity 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface MediaRepository { 10 | suspend fun getDataFromDb(): Flow>> 11 | suspend fun getAllImgVidMediaFile(): Flow>> 12 | suspend fun fetchDocs(): Flow>> 13 | suspend fun isDbEmpty(): Boolean 14 | suspend fun insertMediaData(): Result 15 | suspend fun insertAllMediaData(): Result 16 | suspend fun removeMediaData(allDataList: List?) 17 | suspend fun deleteMediaFiles() 18 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/media/MediaType.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.domain.media 2 | 3 | enum class MediaType(val value: Int) { 4 | IMAGE(1), 5 | VIDEO(2), 6 | AUDIO(3), 7 | DOC(4), 8 | FILE_TYPE_WITH_SYSTEM_VIEW(5), 9 | PHOTO_PICKER(6), 10 | VIDEO_PICKER(7), 11 | PHOTO_VIDEO_PICKER(8), 12 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/media/SelectedMediaRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.domain.media 2 | 3 | import com.lassi.data.common.Result 4 | import com.lassi.data.media.MiMedia 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface SelectedMediaRepository { 8 | suspend fun getSelectedMediaData(bucket: String): Flow>> 9 | suspend fun getSortedDataFromDb( 10 | bucket: String, 11 | isAsc: Int, 12 | mediaType: MediaType 13 | ): Flow>> 14 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/domain/media/SortingOption.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.domain.media 2 | 3 | enum class SortingOption { 4 | ASCENDING, 5 | DESCENDING 6 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Audio.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | /** 9 | * Audio values indicate whether to record audio stream when record video. 10 | * 11 | * @see CameraView#setAudio(Audio) 12 | */ 13 | public enum Audio implements Control { 14 | 15 | /** 16 | * No Audio. 17 | */ 18 | OFF(0), 19 | 20 | /** 21 | * With Audio. 22 | */ 23 | ON(1); 24 | 25 | public final static Audio DEFAULT = ON; 26 | 27 | private int value; 28 | 29 | Audio(int value) { 30 | this.value = value; 31 | } 32 | 33 | @Nullable 34 | public static Audio fromValue(int value) { 35 | Audio[] list = Audio.values(); 36 | for (Audio action : list) { 37 | if (action.value() == value) { 38 | return action; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | public int value() { 45 | return value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Control.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | /** 4 | * Base interface for controls like {@link Audio}, 5 | * {@link Facing}, {@link Flash} and so on. 6 | */ 7 | public interface Control { 8 | } 9 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Facing.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import android.content.Context; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | 9 | import com.lassi.presentation.cameraview.controls.CameraView; 10 | import com.lassi.presentation.cameraview.utils.CameraUtils; 11 | 12 | /** 13 | * Facing value indicates which camera sensor should be used for the current session. 14 | * 15 | * @see CameraView#setFacing(Facing) 16 | */ 17 | public enum Facing implements Control { 18 | 19 | /** 20 | * Back-facing camera sensor. 21 | */ 22 | BACK(0), 23 | 24 | /** 25 | * Front-facing camera sensor. 26 | */ 27 | FRONT(1); 28 | 29 | private int value; 30 | 31 | Facing(int value) { 32 | this.value = value; 33 | } 34 | 35 | @NonNull 36 | public static Facing DEFAULT(@Nullable Context context) { 37 | if (context == null) { 38 | return BACK; 39 | } else if (CameraUtils.hasCameraFacing(context, BACK)) { 40 | return BACK; 41 | } else if (CameraUtils.hasCameraFacing(context, FRONT)) { 42 | return FRONT; 43 | } else { 44 | // The controller will throw a CameraException. 45 | // This device has no cameras. 46 | return BACK; 47 | } 48 | } 49 | 50 | @Nullable 51 | public static Facing fromValue(int value) { 52 | Facing[] list = Facing.values(); 53 | for (Facing action : list) { 54 | if (action.value() == value) { 55 | return action; 56 | } 57 | } 58 | return null; 59 | } 60 | 61 | public int value() { 62 | return value; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Flash.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraOptions; 7 | import com.lassi.presentation.cameraview.controls.CameraView; 8 | 9 | /** 10 | * Flash value indicates the flash mode to be used. 11 | * 12 | * @see CameraView#setFlash(Flash) 13 | */ 14 | public enum Flash implements Control { 15 | 16 | /** 17 | * Flash is always off. 18 | */ 19 | OFF(0), 20 | 21 | /** 22 | * Flash will be on when capturing. 23 | * This is not guaranteed to be supported. 24 | * 25 | * @see CameraOptions#getSupportedFlash() 26 | */ 27 | ON(1), 28 | 29 | 30 | /** 31 | * Flash mode is chosen by the camera. 32 | * This is not guaranteed to be supported. 33 | * 34 | * @see CameraOptions#getSupportedFlash() 35 | */ 36 | AUTO(2), 37 | 38 | 39 | /** 40 | * Flash is always on, working as a torch. 41 | * This is not guaranteed to be supported. 42 | * 43 | * @see CameraOptions#getSupportedFlash() 44 | */ 45 | TORCH(3); 46 | 47 | public static final Flash DEFAULT = OFF; 48 | 49 | private int value; 50 | 51 | Flash(int value) { 52 | this.value = value; 53 | } 54 | 55 | @Nullable 56 | public static Flash fromValue(int value) { 57 | Flash[] list = Flash.values(); 58 | for (Flash action : list) { 59 | if (action.value() == value) { 60 | return action; 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | public int value() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Gesture.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.NonNull; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | 12 | /** 13 | * Gestures listen to finger gestures over the {@link CameraView} bounds and can be mapped 14 | * to one or more camera controls using XML attributes or {@link CameraView#mapGesture(Gesture, GestureAction)}. 15 | *

16 | * Not every gesture can control a certain action. For example, pinch gestures can only control 17 | * continuous values, such as zoom or AE correction. Single point gestures, on the other hand, 18 | * can only control point actions such as focusing or capturing a picture. 19 | */ 20 | public enum Gesture { 21 | 22 | /** 23 | * Pinch gesture, typically assigned to the zoom control. 24 | * This gesture can be mapped to: 25 | *

26 | * - {@link GestureAction#ZOOM} 27 | * - {@link GestureAction#EXPOSURE_CORRECTION} 28 | * - {@link GestureAction#NONE} 29 | */ 30 | PINCH(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION), 31 | 32 | /** 33 | * Single tap gesture, typically assigned to the focus control. 34 | * This gesture can be mapped to: 35 | *

36 | * - {@link GestureAction#FOCUS} 37 | * - {@link GestureAction#FOCUS_WITH_MARKER} 38 | * - {@link GestureAction#CAPTURE} 39 | * - {@link GestureAction#NONE} 40 | */ 41 | TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE), 42 | // DOUBLE_TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE), 43 | 44 | /** 45 | * Long tap gesture. 46 | * This gesture can be mapped to: 47 | *

48 | * - {@link GestureAction#FOCUS} 49 | * - {@link GestureAction#FOCUS_WITH_MARKER} 50 | * - {@link GestureAction#CAPTURE} 51 | * - {@link GestureAction#NONE} 52 | */ 53 | LONG_TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE), 54 | 55 | /** 56 | * Horizontal scroll gesture. 57 | * This gesture can be mapped to: 58 | *

59 | * - {@link GestureAction#ZOOM} 60 | * - {@link GestureAction#EXPOSURE_CORRECTION} 61 | * - {@link GestureAction#NONE} 62 | */ 63 | SCROLL_HORIZONTAL(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION), 64 | 65 | /** 66 | * Vertical scroll gesture. 67 | * This gesture can be mapped to: 68 | *

69 | * - {@link GestureAction#ZOOM} 70 | * - {@link GestureAction#EXPOSURE_CORRECTION} 71 | * - {@link GestureAction#NONE} 72 | */ 73 | SCROLL_VERTICAL(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION); 74 | 75 | private List mControls; 76 | 77 | Gesture(GestureAction... controls) { 78 | mControls = Arrays.asList(controls); 79 | } 80 | 81 | public boolean isAssignableTo(@NonNull GestureAction control) { 82 | return control == GestureAction.NONE || mControls.contains(control); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/GestureAction.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | /** 9 | * Gestures actions are actions over camera controls that can be mapped to certain gestures over 10 | * the screen, using XML attributes or {@link CameraView#mapGesture(Gesture, GestureAction)}. 11 | *

12 | * Not every gesture can control a certain action. For example, pinch gestures can only control 13 | * continuous values, such as zoom or AE correction. Single point gestures, on the other hand, 14 | * can only control point actions such as focusing or capturing a picture. 15 | */ 16 | public enum GestureAction { 17 | 18 | /** 19 | * No action. This can be mapped to any gesture to disable it. 20 | */ 21 | NONE(0), 22 | 23 | /** 24 | * Auto focus control, typically assigned to the tap gesture. 25 | * This action can be mapped to: 26 | *

27 | * - {@link Gesture#TAP} 28 | * - {@link Gesture#LONG_TAP} 29 | */ 30 | FOCUS(1), 31 | 32 | /** 33 | * Auto focus control, typically assigned to the tap gesture. 34 | * On top of {@link #FOCUS}, this will draw a default marker on screen. 35 | * This action can be mapped to: 36 | *

37 | * - {@link Gesture#TAP} 38 | * - {@link Gesture#LONG_TAP} 39 | */ 40 | FOCUS_WITH_MARKER(2), 41 | 42 | /** 43 | * When triggered, this action will fire a picture shoot. 44 | * This action can be mapped to: 45 | *

46 | * - {@link Gesture#TAP} 47 | * - {@link Gesture#LONG_TAP} 48 | */ 49 | CAPTURE(3), 50 | 51 | /** 52 | * Zoom control, typically assigned to the pinch gesture. 53 | * This action can be mapped to: 54 | *

55 | * - {@link Gesture#PINCH} 56 | * - {@link Gesture#SCROLL_HORIZONTAL} 57 | * - {@link Gesture#SCROLL_VERTICAL} 58 | */ 59 | ZOOM(4), 60 | 61 | /** 62 | * Exposure correction control. 63 | * This action can be mapped to: 64 | *

65 | * - {@link Gesture#PINCH} 66 | * - {@link Gesture#SCROLL_HORIZONTAL} 67 | * - {@link Gesture#SCROLL_VERTICAL} 68 | */ 69 | EXPOSURE_CORRECTION(5); 70 | 71 | 72 | public final static GestureAction DEFAULT_PINCH = NONE; 73 | public final static GestureAction DEFAULT_TAP = NONE; 74 | public final static GestureAction DEFAULT_LONG_TAP = NONE; 75 | public final static GestureAction DEFAULT_SCROLL_HORIZONTAL = NONE; 76 | public final static GestureAction DEFAULT_SCROLL_VERTICAL = NONE; 77 | 78 | private int value; 79 | 80 | GestureAction(int value) { 81 | this.value = value; 82 | } 83 | 84 | @Nullable 85 | public static GestureAction fromValue(int value) { 86 | GestureAction[] list = GestureAction.values(); 87 | for (GestureAction action : list) { 88 | if (action.value() == value) { 89 | return action; 90 | } 91 | } 92 | return null; 93 | } 94 | 95 | public int value() { 96 | return value; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Grid.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | /** 9 | * Grid values can be used to draw grid lines over the camera preview. 10 | * 11 | * @see CameraView#setGrid(Grid) 12 | */ 13 | public enum Grid implements Control { 14 | 15 | /** 16 | * No grid is drawn. 17 | */ 18 | OFF(0), 19 | 20 | /** 21 | * Draws a regular, 3x3 grid. 22 | */ 23 | DRAW_3X3(1), 24 | 25 | /** 26 | * Draws a regular, 4x4 grid. 27 | */ 28 | DRAW_4X4(2), 29 | 30 | /** 31 | * Draws a grid respecting the 'phi' constant proportions, 32 | * often referred as to the golden ratio. 33 | */ 34 | DRAW_PHI(3); 35 | 36 | public static final Grid DEFAULT = OFF; 37 | 38 | private int value; 39 | 40 | Grid(int value) { 41 | this.value = value; 42 | } 43 | 44 | @Nullable 45 | public static Grid fromValue(int value) { 46 | Grid[] list = Grid.values(); 47 | for (Grid action : list) { 48 | if (action.value() == value) { 49 | return action; 50 | } 51 | } 52 | return null; 53 | } 54 | 55 | public int value() { 56 | return value; 57 | } 58 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Hdr.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import com.lassi.presentation.cameraview.controls.CameraView; 6 | 7 | 8 | /** 9 | * Hdr values indicate whether to use high dynamic range techniques when capturing pictures. 10 | * 11 | * @see CameraView#setHdr(Hdr) 12 | */ 13 | public enum Hdr implements Control { 14 | 15 | /** 16 | * No HDR. 17 | */ 18 | OFF(0), 19 | 20 | /** 21 | * Using HDR. 22 | */ 23 | ON(1); 24 | 25 | public final static Hdr DEFAULT = OFF; 26 | 27 | private int value; 28 | 29 | Hdr(int value) { 30 | this.value = value; 31 | } 32 | 33 | @Nullable 34 | public static Hdr fromValue(int value) { 35 | Hdr[] list = Hdr.values(); 36 | for (Hdr action : list) { 37 | if (action.value() == value) { 38 | return action; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | public int value() { 45 | return value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Mode.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * Type of the session to be opened or to move to. 12 | * Session modes have influence over the capture and preview size, ability to shoot pictures, 13 | * focus modes, runtime permissions needed. 14 | * 15 | * @see CameraView#setMode(Mode) 16 | */ 17 | public enum Mode implements Control { 18 | 19 | /** 20 | * Session used to capture pictures. 21 | *

22 | * - {@link CameraView#takeVideo(File)} will throw an exception 23 | * - Only the camera permission is requested 24 | * - Capture size is chosen according to the current picture size selector 25 | */ 26 | PICTURE(0), 27 | 28 | /** 29 | * Session used to capture videos. 30 | *

31 | * - {@link CameraView#takePicture()} will throw an exception 32 | * - Camera and audio record permissions are requested 33 | * - Capture size is chosen according to the current video size selector 34 | */ 35 | VIDEO(1); 36 | 37 | public static final Mode DEFAULT = PICTURE; 38 | 39 | private int value; 40 | 41 | Mode(int value) { 42 | this.value = value; 43 | } 44 | 45 | @Nullable 46 | public static Mode fromValue(int value) { 47 | Mode[] list = Mode.values(); 48 | for (Mode action : list) { 49 | if (action.value() == value) { 50 | return action; 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | public int value() { 57 | return value; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/Preview.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | /** 9 | * The preview engine to be used. 10 | * 11 | * @see CameraView#setPreview(Preview) 12 | */ 13 | public enum Preview implements Control { 14 | 15 | /** 16 | * Preview engine based on {@link android.view.SurfaceView}. 17 | * Not recommended. 18 | */ 19 | SURFACE(0), 20 | 21 | /** 22 | * Preview engine based on {@link android.view.TextureView}. 23 | * Stable, but does not support all features (like video snapshots, 24 | * or picture snapshot while taking videos). 25 | */ 26 | TEXTURE(1), 27 | 28 | /** 29 | * Preview engine based on {@link android.opengl.GLSurfaceView}. 30 | * This is the best engine available. Supports video snapshots, 31 | * and picture snapshots while taking videos. 32 | */ 33 | GL_SURFACE(2); 34 | 35 | public final static Preview DEFAULT = GL_SURFACE; 36 | 37 | private int value; 38 | 39 | Preview(int value) { 40 | this.value = value; 41 | } 42 | 43 | @Nullable 44 | public static Preview fromValue(int value) { 45 | Preview[] list = Preview.values(); 46 | for (Preview action : list) { 47 | if (action.value() == value) { 48 | return action; 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | public int value() { 55 | return value; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/VideoCodec.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraView; 7 | 8 | /** 9 | * Constants for selecting the encoder of video recordings. 10 | * https://developer.android.com/guide/topics/media/media-formats.html#video-formats 11 | * 12 | * @see CameraView#setVideoCodec(VideoCodec) 13 | */ 14 | public enum VideoCodec implements Control { 15 | 16 | 17 | /** 18 | * Let the device choose its codec. 19 | */ 20 | DEVICE_DEFAULT(0), 21 | 22 | /** 23 | * The H.263 codec. 24 | */ 25 | H_263(1), 26 | 27 | /** 28 | * The H.264 codec. 29 | */ 30 | H_264(2); 31 | 32 | public static final VideoCodec DEFAULT = DEVICE_DEFAULT; 33 | 34 | private int value; 35 | 36 | VideoCodec(int value) { 37 | this.value = value; 38 | } 39 | 40 | @Nullable 41 | public static VideoCodec fromValue(int value) { 42 | VideoCodec[] list = VideoCodec.values(); 43 | for (VideoCodec action : list) { 44 | if (action.value() == value) { 45 | return action; 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | public int value() { 52 | return value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/audio/WhiteBalance.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.audio; 2 | 3 | 4 | import androidx.annotation.Nullable; 5 | 6 | import com.lassi.presentation.cameraview.controls.CameraOptions; 7 | import com.lassi.presentation.cameraview.controls.CameraView; 8 | 9 | /** 10 | * White balance values control the white balance settings. 11 | * 12 | * @see CameraView#setWhiteBalance(WhiteBalance) 13 | */ 14 | public enum WhiteBalance implements Control { 15 | 16 | /** 17 | * Automatic white balance selection (AWB). 18 | * This is not guaranteed to be supported. 19 | * 20 | * @see CameraOptions#getSupportedWhiteBalance() 21 | */ 22 | AUTO(0), 23 | 24 | /** 25 | * White balance appropriate for incandescent light. 26 | * This is not guaranteed to be supported. 27 | * 28 | * @see CameraOptions#getSupportedWhiteBalance() 29 | */ 30 | INCANDESCENT(1), 31 | 32 | /** 33 | * White balance appropriate for fluorescent light. 34 | * This is not guaranteed to be supported. 35 | * 36 | * @see CameraOptions#getSupportedWhiteBalance() 37 | */ 38 | FLUORESCENT(2), 39 | 40 | /** 41 | * White balance appropriate for daylight captures. 42 | * This is not guaranteed to be supported. 43 | * 44 | * @see CameraOptions#getSupportedWhiteBalance() 45 | */ 46 | DAYLIGHT(3), 47 | 48 | /** 49 | * White balance appropriate for pictures in cloudy conditions. 50 | * This is not guaranteed to be supported. 51 | * 52 | * @see CameraOptions#getSupportedWhiteBalance() 53 | */ 54 | CLOUDY(4); 55 | 56 | public static final WhiteBalance DEFAULT = AUTO; 57 | 58 | private int value; 59 | 60 | WhiteBalance(int value) { 61 | this.value = value; 62 | } 63 | 64 | @Nullable 65 | public static WhiteBalance fromValue(int value) { 66 | WhiteBalance[] list = WhiteBalance.values(); 67 | for (WhiteBalance action : list) { 68 | if (action.value() == value) { 69 | return action; 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | public int value() { 76 | return value; 77 | } 78 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/CamcorderProfiles.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.media.CamcorderProfile; 5 | import android.os.Build; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.Comparator; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | /** 17 | * Wraps the {@link CamcorderProfile} static utilities. 18 | */ 19 | class CamcorderProfiles { 20 | 21 | @SuppressLint("UseSparseArrays") 22 | private static Map sizeToProfileMap = new HashMap<>(); 23 | 24 | static { 25 | sizeToProfileMap.put(new Size(176, 144), CamcorderProfile.QUALITY_QCIF); 26 | sizeToProfileMap.put(new Size(320, 240), CamcorderProfile.QUALITY_QVGA); 27 | sizeToProfileMap.put(new Size(352, 288), CamcorderProfile.QUALITY_CIF); 28 | sizeToProfileMap.put(new Size(720, 480), CamcorderProfile.QUALITY_480P); 29 | sizeToProfileMap.put(new Size(1280, 720), CamcorderProfile.QUALITY_720P); 30 | sizeToProfileMap.put(new Size(1920, 1080), CamcorderProfile.QUALITY_1080P); 31 | if (Build.VERSION.SDK_INT >= 21) { 32 | sizeToProfileMap.put(new Size(3840, 2160), CamcorderProfile.QUALITY_2160P); 33 | } 34 | } 35 | 36 | /** 37 | * Returns a CamcorderProfile that's somewhat coherent with the target size, 38 | * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate). 39 | * 40 | * @param cameraId the camera id 41 | * @param targetSize the target video size 42 | * @return a profile 43 | */ 44 | @NonNull 45 | static CamcorderProfile get(int cameraId, @NonNull Size targetSize) { 46 | final int targetArea = targetSize.getWidth() * targetSize.getHeight(); 47 | List sizes = new ArrayList<>(sizeToProfileMap.keySet()); 48 | Collections.sort(sizes, new Comparator() { 49 | @Override 50 | public int compare(Size s1, Size s2) { 51 | int a1 = Math.abs(s1.getWidth() * s1.getHeight() - targetArea); 52 | int a2 = Math.abs(s2.getWidth() * s2.getHeight() - targetArea); 53 | //noinspection UseCompareMethod 54 | return (a1 < a2) ? -1 : ((a1 == a2) ? 0 : 1); 55 | } 56 | }); 57 | while (sizes.size() > 0) { 58 | Size candidate = sizes.remove(0); 59 | //noinspection ConstantConditions 60 | int quality = sizeToProfileMap.get(candidate); 61 | if (CamcorderProfile.hasProfile(cameraId, quality)) { 62 | return CamcorderProfile.get(cameraId, quality); 63 | } 64 | } 65 | // Should never happen, but fallback to low. 66 | return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/CameraException.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import com.lassi.presentation.cameraview.audio.Facing; 4 | 5 | /** 6 | * Holds an error with the camera configuration. 7 | */ 8 | public class CameraException extends RuntimeException { 9 | 10 | /** 11 | * Unknown error. No further info available. 12 | */ 13 | public static final int REASON_UNKNOWN = 0; 14 | 15 | /** 16 | * We failed to connect to the camera service. 17 | * The camera might be in use by another app. 18 | */ 19 | public static final int REASON_FAILED_TO_CONNECT = 1; 20 | 21 | /** 22 | * Failed to start the camera preview. 23 | * Again, the camera might be in use by another app. 24 | */ 25 | public static final int REASON_FAILED_TO_START_PREVIEW = 2; 26 | 27 | /** 28 | * Camera was forced to disconnect. 29 | * In Camera1, this is thrown when android.hardware.Camera.CAMERA_ERROR_EVICTED 30 | * is caught. 31 | */ 32 | public static final int REASON_DISCONNECTED = 3; 33 | 34 | /** 35 | * Could not take a picture or a picture snapshot, 36 | * for some not specified reason. 37 | */ 38 | public static final int REASON_PICTURE_FAILED = 4; 39 | 40 | /** 41 | * Could not take a video or a video snapshot, 42 | * for some not specified reason. 43 | */ 44 | public static final int REASON_VIDEO_FAILED = 5; 45 | 46 | /** 47 | * Indicates that we could not find a camera for the current {@link Facing} 48 | * value. 49 | * This can be solved by changing the facing value and starting again. 50 | */ 51 | public static final int REASON_NO_CAMERA = 6; 52 | 53 | private int reason = REASON_UNKNOWN; 54 | 55 | CameraException(Throwable cause) { 56 | super(cause); 57 | } 58 | 59 | CameraException(Throwable cause, int reason) { 60 | super(cause); 61 | this.reason = reason; 62 | } 63 | 64 | CameraException(int reason) { 65 | super(); 66 | this.reason = reason; 67 | } 68 | 69 | public int getReason() { 70 | return reason; 71 | } 72 | 73 | /** 74 | * Whether this error is unrecoverable. If this function returns true, 75 | * the Camera has been closed and it is likely showing a black preview. 76 | * This is the right moment to show an error dialog to the user. 77 | * 78 | * @return true if this error is unrecoverable 79 | */ 80 | public boolean isUnrecoverable() { 81 | switch (getReason()) { 82 | case REASON_FAILED_TO_CONNECT: 83 | return true; 84 | case REASON_FAILED_TO_START_PREVIEW: 85 | return true; 86 | case REASON_DISCONNECTED: 87 | return true; 88 | default: 89 | return false; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/CameraMapper.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import android.hardware.Camera; 4 | import android.os.Build; 5 | 6 | import com.lassi.presentation.cameraview.audio.Facing; 7 | import com.lassi.presentation.cameraview.audio.Flash; 8 | import com.lassi.presentation.cameraview.audio.Hdr; 9 | import com.lassi.presentation.cameraview.audio.WhiteBalance; 10 | 11 | import java.util.HashMap; 12 | 13 | 14 | @SuppressWarnings("unchecked") 15 | public class CameraMapper extends Mapper { 16 | 17 | private static final HashMap FLASH = new HashMap<>(); 18 | private static final HashMap WB = new HashMap<>(); 19 | private static final HashMap FACING = new HashMap<>(); 20 | private static final HashMap HDR = new HashMap<>(); 21 | 22 | static { 23 | FLASH.put(Flash.OFF, Camera.Parameters.FLASH_MODE_OFF); 24 | FLASH.put(Flash.ON, Camera.Parameters.FLASH_MODE_ON); 25 | FLASH.put(Flash.AUTO, Camera.Parameters.FLASH_MODE_AUTO); 26 | FLASH.put(Flash.TORCH, Camera.Parameters.FLASH_MODE_TORCH); 27 | FACING.put(Facing.BACK, Camera.CameraInfo.CAMERA_FACING_BACK); 28 | FACING.put(Facing.FRONT, Camera.CameraInfo.CAMERA_FACING_FRONT); 29 | WB.put(WhiteBalance.AUTO, Camera.Parameters.WHITE_BALANCE_AUTO); 30 | WB.put(WhiteBalance.INCANDESCENT, Camera.Parameters.WHITE_BALANCE_INCANDESCENT); 31 | WB.put(WhiteBalance.FLUORESCENT, Camera.Parameters.WHITE_BALANCE_FLUORESCENT); 32 | WB.put(WhiteBalance.DAYLIGHT, Camera.Parameters.WHITE_BALANCE_DAYLIGHT); 33 | WB.put(WhiteBalance.CLOUDY, Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT); 34 | HDR.put(Hdr.OFF, Camera.Parameters.SCENE_MODE_AUTO); 35 | if (Build.VERSION.SDK_INT >= 17) { 36 | HDR.put(Hdr.ON, Camera.Parameters.SCENE_MODE_HDR); 37 | } else { 38 | HDR.put(Hdr.ON, "hdr"); 39 | } 40 | } 41 | 42 | @Override 43 | T map(Flash flash) { 44 | return (T) FLASH.get(flash); 45 | } 46 | 47 | @Override 48 | public T map(Facing facing) { 49 | return (T) FACING.get(facing); 50 | } 51 | 52 | @Override 53 | T map(WhiteBalance whiteBalance) { 54 | return (T) WB.get(whiteBalance); 55 | } 56 | 57 | @Override 58 | T map(Hdr hdr) { 59 | return (T) HDR.get(hdr); 60 | } 61 | 62 | private T reverseLookup(HashMap map, Object object) { 63 | for (T value : map.keySet()) { 64 | if (map.get(value).equals(object)) { 65 | return value; 66 | } 67 | } 68 | return null; 69 | } 70 | 71 | @Override 72 | Flash unmapFlash(T cameraConstant) { 73 | return reverseLookup(FLASH, cameraConstant); 74 | } 75 | 76 | @Override 77 | Facing unmapFacing(T cameraConstant) { 78 | return reverseLookup(FACING, cameraConstant); 79 | } 80 | 81 | @Override 82 | WhiteBalance unmapWhiteBalance(T cameraConstant) { 83 | return reverseLookup(WB, cameraConstant); 84 | } 85 | 86 | @Override 87 | Hdr unmapHdr(T cameraConstant) { 88 | return reverseLookup(HDR, cameraConstant); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/FrameProcessor.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.WorkerThread; 5 | 6 | /** 7 | * A FrameProcessor will process {@link Frame}s coming from the camera preview. 8 | * It must be passed to {@link CameraView#addFrameProcessor(FrameProcessor)}. 9 | */ 10 | public interface FrameProcessor { 11 | 12 | /** 13 | * Processes the given frame. The frame will hold the correct values only for the 14 | * duration of this method. When it returns, the frame contents will be replaced. 15 | *

16 | * To keep working with the Frame in an async manner, please use {@link Frame#freeze()}, 17 | * which will return an immutable Frame. In that case you can pass / hold the frame for 18 | * as long as you want, and then release its contents using {@link Frame#release()}. 19 | * 20 | * @param frame the new frame 21 | */ 22 | @WorkerThread 23 | void process(@NonNull Frame frame); 24 | } 25 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/FullPictureRecorder.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import android.hardware.Camera; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.exifinterface.media.ExifInterface; 8 | 9 | import com.lassi.presentation.cameraview.utils.CameraLogger; 10 | import com.lassi.presentation.cameraview.utils.CameraUtils; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.IOException; 14 | 15 | /** 16 | * A {@link PictureResult} that uses standard APIs. 17 | */ 18 | class FullPictureRecorder extends PictureRecorder { 19 | 20 | private static final String TAG = FullPictureRecorder.class.getSimpleName(); 21 | private static final CameraLogger LOG = CameraLogger.create(TAG); 22 | 23 | private Camera mCamera; 24 | 25 | FullPictureRecorder(@NonNull PictureResult stub, @Nullable PictureRecorder.PictureResultListener listener, @NonNull Camera camera) { 26 | super(stub, listener); 27 | mCamera = camera; 28 | 29 | // We set the rotation to the camera parameters, but we don't know if the result will be 30 | // already rotated with 0 exif, or original with non zero exif. we will have to read EXIF. 31 | Camera.Parameters params = mCamera.getParameters(); 32 | params.setRotation(mResult.rotation); 33 | mCamera.setParameters(params); 34 | } 35 | 36 | // Camera2 constructor here... 37 | 38 | @Override 39 | void take() { 40 | mCamera.takePicture( 41 | new Camera.ShutterCallback() { 42 | @Override 43 | public void onShutter() { 44 | dispatchOnShutter(true); 45 | } 46 | }, 47 | null, 48 | null, 49 | new Camera.PictureCallback() { 50 | @Override 51 | public void onPictureTaken(byte[] data, final Camera camera) { 52 | int exifRotation; 53 | try { 54 | ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data)); 55 | int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); 56 | exifRotation = CameraUtils.readExifOrientation(exifOrientation); 57 | } catch (IOException e) { 58 | exifRotation = 0; 59 | } 60 | mResult.format = PictureResult.FORMAT_JPEG; 61 | mResult.data = data; 62 | mResult.rotation = exifRotation; 63 | camera.startPreview(); // This is needed, read somewhere in the docs. 64 | dispatchResult(); 65 | } 66 | } 67 | ); 68 | } 69 | 70 | @Override 71 | protected void dispatchResult() { 72 | mCamera = null; 73 | super.dispatchResult(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/Mapper.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | 4 | import com.lassi.presentation.cameraview.audio.Facing; 5 | import com.lassi.presentation.cameraview.audio.Flash; 6 | import com.lassi.presentation.cameraview.audio.Hdr; 7 | import com.lassi.presentation.cameraview.audio.WhiteBalance; 8 | 9 | abstract class Mapper { 10 | 11 | abstract T map(Flash flash); 12 | 13 | abstract T map(Facing facing); 14 | 15 | abstract T map(WhiteBalance whiteBalance); 16 | 17 | abstract T map(Hdr hdr); 18 | 19 | abstract Flash unmapFlash(T cameraConstant); 20 | 21 | abstract Facing unmapFacing(T cameraConstant); 22 | 23 | abstract WhiteBalance unmapWhiteBalance(T cameraConstant); 24 | 25 | abstract Hdr unmapHdr(T cameraConstant); 26 | } 27 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/PictureRecorder.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | /** 7 | * Interface for picture capturing. 8 | * Don't call start if already started. Don't call stop if already stopped. 9 | * Don't reuse. 10 | */ 11 | abstract class PictureRecorder { 12 | 13 | /* tests */ PictureResult mResult; 14 | /* tests */ PictureResultListener mListener; 15 | 16 | PictureRecorder(@NonNull PictureResult stub, @Nullable PictureResultListener listener) { 17 | mResult = stub; 18 | mListener = listener; 19 | } 20 | 21 | abstract void take(); 22 | 23 | @SuppressWarnings("WeakerAccess") 24 | protected void dispatchOnShutter(boolean didPlaySound) { 25 | if (mListener != null) mListener.onPictureShutter(didPlaySound); 26 | } 27 | 28 | protected void dispatchResult() { 29 | if (mListener != null) { 30 | mListener.onPictureResult(mResult); 31 | mListener = null; 32 | mResult = null; 33 | } 34 | } 35 | 36 | interface PictureResultListener { 37 | void onPictureShutter(boolean didPlaySound); 38 | 39 | void onPictureResult(@Nullable PictureResult result); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/Size.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | /** 6 | * A simple class representing a size, with width and height values. 7 | */ 8 | public class Size implements Comparable { 9 | 10 | private final int mWidth; 11 | private final int mHeight; 12 | 13 | public Size(int width, int height) { 14 | mWidth = width; 15 | mHeight = height; 16 | } 17 | 18 | public int getWidth() { 19 | return mWidth; 20 | } 21 | 22 | public int getHeight() { 23 | return mHeight; 24 | } 25 | 26 | @SuppressWarnings("SuspiciousNameCombination") 27 | Size flip() { 28 | return new Size(mHeight, mWidth); 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (o == null) { 34 | return false; 35 | } 36 | if (this == o) { 37 | return true; 38 | } 39 | if (o instanceof Size) { 40 | Size size = (Size) o; 41 | return mWidth == size.mWidth && mHeight == size.mHeight; 42 | } 43 | return false; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return mWidth + "x" + mHeight; 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); 54 | } 55 | 56 | @Override 57 | public int compareTo(@NonNull Size another) { 58 | return mWidth * mHeight - another.mWidth * another.mHeight; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/SizeSelector.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * A size selector receives a list of {@link Size}s and returns another list with 9 | * sizes that are considered acceptable. 10 | */ 11 | public interface SizeSelector { 12 | 13 | /** 14 | * Returns a list of acceptable sizes from the given input. 15 | * The final size will be the first element in the output list. 16 | * 17 | * @param source input list 18 | * @return output list 19 | */ 20 | @NonNull 21 | List select(@NonNull List source); 22 | } 23 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/controls/VideoRecorder.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.controls; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | /** 7 | * Interface for video recording. 8 | * Don't call start if already started. Don't call stop if already stopped. 9 | * Don't reuse. 10 | */ 11 | abstract class VideoRecorder { 12 | 13 | protected Exception mError; 14 | /* tests */ VideoResult mResult; 15 | /* tests */ VideoResultListener mListener; 16 | 17 | VideoRecorder(@NonNull VideoResult stub, @Nullable VideoResultListener listener) { 18 | mResult = stub; 19 | mListener = listener; 20 | } 21 | 22 | abstract void start(); 23 | 24 | abstract void stop(); 25 | 26 | @SuppressWarnings("WeakerAccess") 27 | protected void dispatchResult() { 28 | if (mListener != null) { 29 | mListener.onVideoResult(mResult, mError); 30 | mListener = null; 31 | mResult = null; 32 | mError = null; 33 | } 34 | } 35 | 36 | 37 | interface VideoResultListener { 38 | void onVideoResult(@Nullable VideoResult result, @Nullable Exception exception); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/preview/GestureLayout.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.preview; 2 | 3 | import android.content.Context; 4 | import android.graphics.PointF; 5 | import android.view.MotionEvent; 6 | import android.widget.FrameLayout; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import com.lassi.presentation.cameraview.audio.Gesture; 11 | 12 | public abstract class GestureLayout extends FrameLayout { 13 | 14 | // The number of possible values between minValue and maxValue, for the scaleValue method. 15 | // We could make this non-static (e.g. larger granularity for exposure correction). 16 | private final static int GRANULARITY = 50; 17 | 18 | protected boolean mEnabled; 19 | protected Gesture mType; 20 | protected PointF[] mPoints; 21 | 22 | public GestureLayout(@NonNull Context context) { 23 | super(context); 24 | onInitialize(context); 25 | } 26 | 27 | // Checks for newValue to be between minValue and maxValue, 28 | // and checks that it is 'far enough' from the oldValue, in order 29 | // to reduce useless updates. 30 | protected static float capValue(float oldValue, float newValue, float minValue, float maxValue) { 31 | if (newValue < minValue) newValue = minValue; 32 | if (newValue > maxValue) newValue = maxValue; 33 | 34 | float distance = (maxValue - minValue) / (float) GRANULARITY; 35 | float half = distance / 2; 36 | if (newValue >= oldValue - half && newValue <= oldValue + half) { 37 | // Too close! Return the oldValue. 38 | return oldValue; 39 | } 40 | return newValue; 41 | } 42 | 43 | protected void onInitialize(@NonNull Context context) { 44 | } 45 | 46 | public void enable(boolean enable) { 47 | mEnabled = enable; 48 | } 49 | 50 | public boolean enabled() { 51 | return mEnabled; 52 | } 53 | 54 | public abstract boolean onTouchEvent(MotionEvent event); 55 | 56 | @NonNull 57 | public final Gesture getGestureType() { 58 | return mType; 59 | } 60 | 61 | // For tests. 62 | void setGestureType(@NonNull Gesture type) { 63 | mType = type; 64 | } 65 | 66 | @NonNull 67 | public final PointF[] getPoints() { 68 | return mPoints; 69 | } 70 | 71 | // Implementors should call capValue at the end. 72 | public abstract float scaleValue(float currValue, float minValue, float maxValue); 73 | } 74 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/preview/PinchGestureLayout.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.preview; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.PointF; 6 | import android.os.Build; 7 | import android.view.MotionEvent; 8 | import android.view.ScaleGestureDetector; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import com.lassi.presentation.cameraview.audio.Gesture; 13 | 14 | public class PinchGestureLayout extends GestureLayout { 15 | 16 | private final static float ADD_SENSITIVITY = 2f; 17 | 18 | ScaleGestureDetector mDetector; 19 | /* tests */ float mFactor = 0; 20 | private boolean mNotify; 21 | 22 | public PinchGestureLayout(@NonNull Context context) { 23 | super(context); 24 | } 25 | 26 | @Override 27 | protected void onInitialize(@NonNull Context context) { 28 | super.onInitialize(context); 29 | mPoints = new PointF[]{new PointF(0, 0), new PointF(0, 0)}; 30 | mDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { 31 | @Override 32 | public boolean onScale(ScaleGestureDetector detector) { 33 | mNotify = true; 34 | mFactor = ((detector.getScaleFactor() - 1) * ADD_SENSITIVITY); 35 | return true; 36 | } 37 | }); 38 | 39 | if (Build.VERSION.SDK_INT >= 19) { 40 | mDetector.setQuickScaleEnabled(false); 41 | } 42 | 43 | // We listen only to the pinch type. 44 | setGestureType(Gesture.PINCH); 45 | } 46 | 47 | 48 | @SuppressLint("ClickableViewAccessibility") 49 | @Override 50 | public boolean onTouchEvent(MotionEvent event) { 51 | if (!mEnabled) return false; 52 | 53 | // Reset the mNotify flag on a new gesture. 54 | // This is to ensure that the mNotify flag stays on until the 55 | // previous gesture ends. 56 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 57 | mNotify = false; 58 | } 59 | 60 | // Let's see if we detect something. This will call onScale(). 61 | mDetector.onTouchEvent(event); 62 | 63 | // Keep notifying CameraView as long as the gesture goes. 64 | if (mNotify) { 65 | getPoints()[0].x = event.getX(0); 66 | getPoints()[0].y = event.getY(0); 67 | if (event.getPointerCount() > 1) { 68 | getPoints()[1].x = event.getX(1); 69 | getPoints()[1].y = event.getY(1); 70 | } 71 | return true; 72 | } 73 | return false; 74 | } 75 | 76 | @Override 77 | public float scaleValue(float currValue, float minValue, float maxValue) { 78 | float add = mFactor; 79 | // ^ This works well if minValue = 0, maxValue = 1. 80 | // Account for the different range: 81 | add *= (maxValue - minValue); 82 | 83 | // ^ This works well if currValue = 0. 84 | // Account for a different starting point: 85 | /* if (add > 0) { 86 | add *= (maxValue - currValue); 87 | } else if (add < 0) { 88 | add *= (currValue - minValue); 89 | } Nope, I don't like this, it slows everything down. */ 90 | return GestureLayout.capValue(currValue, currValue + add, minValue, maxValue); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/preview/RendererThread.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.preview; 2 | 3 | public @interface RendererThread { 4 | } 5 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/preview/SurfaceCameraPreview.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.preview; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.SurfaceHolder; 6 | import android.view.SurfaceView; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | 13 | import com.lassi.R; 14 | import com.lassi.presentation.cameraview.utils.CameraLogger; 15 | 16 | // Fallback preview when hardware acceleration is off. 17 | // Currently this does NOT support cropping (e. g. the crop inside behavior), 18 | // so we return false in supportsCropping() in order to have proper measuring. 19 | // This means that CameraView is forced to be wrap_content. 20 | public class SurfaceCameraPreview extends CameraPreview { 21 | 22 | private final static CameraLogger LOG = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName()); 23 | 24 | private boolean mDispatched; 25 | private View mRootView; 26 | 27 | public SurfaceCameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable CameraPreview.SurfaceCallback callback) { 28 | super(context, parent, callback); 29 | } 30 | 31 | @NonNull 32 | @Override 33 | protected SurfaceView onCreateView(@NonNull Context context, @NonNull ViewGroup parent) { 34 | View root = LayoutInflater.from(context).inflate(R.layout.cameraview_surface_view, parent, false); 35 | parent.addView(root, 0); 36 | SurfaceView surfaceView = root.findViewById(R.id.surface_view); 37 | final SurfaceHolder holder = surfaceView.getHolder(); 38 | holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 39 | holder.addCallback(new SurfaceHolder.Callback() { 40 | 41 | @Override 42 | public void surfaceCreated(SurfaceHolder holder) { 43 | // This is too early to call anything. 44 | // surfaceChanged is guaranteed to be called after, with exact dimensions. 45 | } 46 | 47 | @Override 48 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 49 | LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "dispatched:", mDispatched); 50 | if (!mDispatched) { 51 | dispatchOnSurfaceAvailable(width, height); 52 | mDispatched = true; 53 | } else { 54 | dispatchOnSurfaceSizeChanged(width, height); 55 | } 56 | } 57 | 58 | @Override 59 | public void surfaceDestroyed(SurfaceHolder holder) { 60 | LOG.i("callback:", "surfaceDestroyed"); 61 | dispatchOnSurfaceDestroyed(); 62 | mDispatched = false; 63 | } 64 | }); 65 | mRootView = root; 66 | return surfaceView; 67 | } 68 | 69 | @NonNull 70 | @Override 71 | public View getRootView() { 72 | return mRootView; 73 | } 74 | 75 | @NonNull 76 | @Override 77 | public SurfaceHolder getOutput() { 78 | return getView().getHolder(); 79 | } 80 | 81 | @NonNull 82 | @Override 83 | public Class getOutputClass() { 84 | return SurfaceHolder.class; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/BitmapCallback.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import androidx.annotation.Nullable; 6 | import androidx.annotation.UiThread; 7 | 8 | /** 9 | * Receives callbacks about a bitmap decoding operation. 10 | */ 11 | public interface BitmapCallback { 12 | 13 | /** 14 | * Notifies that the bitmap was succesfully decoded. 15 | * This is run on the UI thread. 16 | * Returns a null object if a {@link OutOfMemoryError} was encountered. 17 | * 18 | * @param bitmap decoded bitmap, or null 19 | */ 20 | @UiThread 21 | void onBitmapReady(@Nullable Bitmap bitmap); 22 | } 23 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/CropHelper.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import android.graphics.Rect; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.lassi.presentation.cameraview.controls.AspectRatio; 8 | import com.lassi.presentation.cameraview.controls.Size; 9 | 10 | public class CropHelper { 11 | 12 | // It's important that size and aspect ratio belong to the same reference. 13 | @NonNull 14 | public static Rect computeCrop(@NonNull Size currentSize, @NonNull AspectRatio targetRatio) { 15 | int currentWidth = currentSize.getWidth(); 16 | int currentHeight = currentSize.getHeight(); 17 | if (targetRatio.matches(currentSize)) { 18 | return new Rect(0, 0, currentWidth, currentHeight); 19 | } 20 | 21 | // They are not equal. Compute. 22 | AspectRatio currentRatio = AspectRatio.Companion.of(currentWidth, currentHeight); 23 | int x, y, width, height; 24 | if (currentRatio.toFloat() > targetRatio.toFloat()) { 25 | height = currentHeight; 26 | width = (int) (height * targetRatio.toFloat()); 27 | y = 0; 28 | x = (currentWidth - width) / 2; 29 | } else { 30 | width = currentWidth; 31 | height = (int) (width / targetRatio.toFloat()); 32 | y = (currentHeight - height) / 2; 33 | x = 0; 34 | } 35 | return new Rect(x, y, x + width, y + height); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/FileCallback.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import androidx.annotation.Nullable; 4 | import androidx.annotation.UiThread; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * Receives callbacks about a file saving operation. 10 | */ 11 | public interface FileCallback { 12 | 13 | /** 14 | * Notifies that the data was succesfully written to file. 15 | * This is run on the UI thread. 16 | * Returns a null object if an exception was encountered, for example 17 | * if you don't have permissions to write to file. 18 | * 19 | * @param file the written file, or null 20 | */ 21 | @UiThread 22 | void onFileReady(@Nullable File file); 23 | } 24 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/OrientationHelper.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import android.content.Context; 4 | import android.hardware.SensorManager; 5 | import android.view.Display; 6 | import android.view.OrientationEventListener; 7 | import android.view.Surface; 8 | import android.view.WindowManager; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | public class OrientationHelper { 13 | 14 | final OrientationEventListener mListener; 15 | 16 | private final Callback mCallback; 17 | private int mDeviceOrientation = -1; 18 | private int mDisplayOffset = -1; 19 | 20 | public OrientationHelper(@NonNull Context context, @NonNull Callback callback) { 21 | mCallback = callback; 22 | mListener = new OrientationEventListener(context.getApplicationContext(), SensorManager.SENSOR_DELAY_NORMAL) { 23 | 24 | @SuppressWarnings("ConstantConditions") 25 | @Override 26 | public void onOrientationChanged(int orientation) { 27 | int or = 0; 28 | if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) { 29 | or = mDeviceOrientation != -1 ? mDeviceOrientation : 0; 30 | } else if (orientation >= 315 || orientation < 45) { 31 | or = 0; 32 | } else if (orientation >= 45 && orientation < 135) { 33 | or = 90; 34 | } else if (orientation >= 135 && orientation < 225) { 35 | or = 180; 36 | } else if (orientation >= 225 && orientation < 315) { 37 | or = 270; 38 | } 39 | 40 | if (or != mDeviceOrientation) { 41 | mDeviceOrientation = or; 42 | mCallback.onDeviceOrientationChanged(mDeviceOrientation); 43 | } 44 | } 45 | }; 46 | } 47 | 48 | public void enable(@NonNull Context context) { 49 | Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); 50 | switch (display.getRotation()) { 51 | case Surface.ROTATION_0: 52 | mDisplayOffset = 0; 53 | break; 54 | case Surface.ROTATION_90: 55 | mDisplayOffset = 90; 56 | break; 57 | case Surface.ROTATION_180: 58 | mDisplayOffset = 180; 59 | break; 60 | case Surface.ROTATION_270: 61 | mDisplayOffset = 270; 62 | break; 63 | default: 64 | mDisplayOffset = 0; 65 | break; 66 | } 67 | mListener.enable(); 68 | } 69 | 70 | public void disable() { 71 | mListener.disable(); 72 | mDisplayOffset = -1; 73 | mDeviceOrientation = -1; 74 | } 75 | 76 | int getDeviceOrientation() { 77 | return mDeviceOrientation; 78 | } 79 | 80 | public int getDisplayOffset() { 81 | return mDisplayOffset; 82 | } 83 | 84 | public interface Callback { 85 | void onDeviceOrientationChanged(int deviceOrientation); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/RotationHelper.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.lassi.presentation.cameraview.controls.Size; 6 | 7 | /** 8 | * This will only be used on low APIs or when GL surface is not available. 9 | * This risks OOMs and was never a good tool. 10 | */ 11 | @SuppressWarnings("DeprecatedIsStillUsed") 12 | @Deprecated 13 | public class RotationHelper { 14 | 15 | public static byte[] rotate(@NonNull final byte[] yuv, @NonNull final Size size, final int rotation) { 16 | if (rotation == 0) return yuv; 17 | if (rotation % 90 != 0 || rotation < 0 || rotation > 270) { 18 | throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0"); 19 | } 20 | final int width = size.getWidth(); 21 | final int height = size.getHeight(); 22 | final byte[] output = new byte[yuv.length]; 23 | final int frameSize = width * height; 24 | final boolean swap = rotation % 180 != 0; 25 | final boolean xflip = rotation % 270 != 0; 26 | final boolean yflip = rotation >= 180; 27 | 28 | for (int j = 0; j < height; j++) { 29 | for (int i = 0; i < width; i++) { 30 | final int yIn = j * width + i; 31 | final int uIn = frameSize + (j >> 1) * width + (i & ~1); 32 | final int vIn = uIn + 1; 33 | 34 | final int wOut = swap ? height : width; 35 | final int hOut = swap ? width : height; 36 | final int iSwapped = swap ? j : i; 37 | final int jSwapped = swap ? i : j; 38 | final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped; 39 | final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped; 40 | 41 | final int yOut = jOut * wOut + iOut; 42 | final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1); 43 | final int vOut = uOut + 1; 44 | 45 | output[yOut] = (byte) (0xff & yuv[yIn]); 46 | output[uOut] = (byte) (0xff & yuv[uIn]); 47 | output[vOut] = (byte) (0xff & yuv[vIn]); 48 | } 49 | } 50 | 51 | return output; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/Task.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * A naive implementation of {@link CountDownLatch} 10 | * to help in testing. 11 | */ 12 | public class Task { 13 | 14 | private CountDownLatch mLatch; 15 | private T mResult; 16 | private int mCount; 17 | 18 | public Task() { 19 | } 20 | 21 | public Task(boolean startListening) { 22 | if (startListening) listen(); 23 | } 24 | 25 | private boolean listening() { 26 | return mLatch != null; 27 | } 28 | 29 | void listen() { 30 | if (listening()) throw new RuntimeException("Should not happen."); 31 | mResult = null; 32 | mLatch = new CountDownLatch(1); 33 | } 34 | 35 | public void start() { 36 | if (!listening()) mCount++; 37 | } 38 | 39 | public void end(T result) { 40 | if (mCount > 0) { 41 | mCount--; 42 | return; 43 | } 44 | 45 | if (listening()) { // Should be always true. 46 | mResult = result; 47 | mLatch.countDown(); 48 | } 49 | } 50 | 51 | T await(long millis) { 52 | return await(millis, TimeUnit.MILLISECONDS); 53 | } 54 | 55 | T await() { 56 | return await(1, TimeUnit.MINUTES); 57 | } 58 | 59 | private T await(long time, @NonNull TimeUnit unit) { 60 | try { 61 | mLatch.await(time, unit); 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | } 65 | T result = mResult; 66 | mResult = null; 67 | mLatch = null; 68 | return result; 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/utils/WorkerHandler.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.utils; 2 | 3 | import android.os.Handler; 4 | import android.os.HandlerThread; 5 | import android.os.Looper; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import java.lang.ref.WeakReference; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | /** 13 | * Class holding a background handler. 14 | * We want them to survive configuration changes if there's still job to do. 15 | */ 16 | public class WorkerHandler { 17 | 18 | private final static CameraLogger LOG = CameraLogger.create(WorkerHandler.class.getSimpleName()); 19 | private final static ConcurrentHashMap> sCache = new ConcurrentHashMap<>(4); 20 | private HandlerThread mThread; 21 | private Handler mHandler; 22 | 23 | private WorkerHandler(@NonNull String name) { 24 | mThread = new HandlerThread(name); 25 | mThread.setDaemon(true); 26 | mThread.start(); 27 | mHandler = new Handler(mThread.getLooper()); 28 | } 29 | 30 | @NonNull 31 | public static WorkerHandler get(@NonNull String name) { 32 | if (sCache.containsKey(name)) { 33 | WorkerHandler cached = sCache.get(name).get(); 34 | if (cached != null) { 35 | HandlerThread thread = cached.mThread; 36 | if (thread.isAlive() && !thread.isInterrupted()) { 37 | LOG.w("get:", "Reusing cached worker handler.", name); 38 | return cached; 39 | } 40 | } 41 | LOG.w("get:", "Thread reference died, removing.", name); 42 | sCache.remove(name); 43 | } 44 | 45 | LOG.i("get:", "Creating new handler.", name); 46 | WorkerHandler handler = new WorkerHandler(name); 47 | sCache.put(name, new WeakReference<>(handler)); 48 | return handler; 49 | } 50 | 51 | // Handy util to perform action in a fallback thread. 52 | // Not to be used for long-running operations since they will 53 | // block the fallback thread. 54 | public static void run(@NonNull Runnable action) { 55 | get("FallbackCameraThread").post(action); 56 | } 57 | 58 | static void destroy() { 59 | for (String key : sCache.keySet()) { 60 | WeakReference ref = sCache.get(key); 61 | WorkerHandler handler = ref.get(); 62 | if (handler != null && handler.getThread().isAlive()) { 63 | handler.getThread().interrupt(); 64 | // handler.getThread().quit(); 65 | } 66 | ref.clear(); 67 | } 68 | sCache.clear(); 69 | } 70 | 71 | public Handler get() { 72 | return mHandler; 73 | } 74 | 75 | public void post(@NonNull Runnable runnable) { 76 | mHandler.post(runnable); 77 | } 78 | 79 | @NonNull 80 | public HandlerThread getThread() { 81 | return mThread; 82 | } 83 | 84 | @NonNull 85 | public Looper getLooper() { 86 | return mThread.getLooper(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/ByteBufferPool.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | class ByteBufferPool extends Pool { 6 | 7 | ByteBufferPool(final int bufferSize, int maxPoolSize) { 8 | super(maxPoolSize, new Pool.Factory() { 9 | @Override 10 | public ByteBuffer create() { 11 | return ByteBuffer.allocateDirect(bufferSize); 12 | } 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/EncoderThread.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | @interface EncoderThread { 4 | } 5 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/InputBuffer.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | class InputBuffer { 6 | ByteBuffer data; 7 | ByteBuffer source; 8 | int index; 9 | int length; 10 | long timestamp; 11 | boolean isEndOfStream; 12 | } 13 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/InputBufferPool.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | class InputBufferPool extends Pool { 4 | 5 | InputBufferPool() { 6 | super(Integer.MAX_VALUE, new Pool.Factory() { 7 | @Override 8 | public InputBuffer create() { 9 | return new InputBuffer(); 10 | } 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/MediaCodecBuffers.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | import android.media.MediaCodec; 4 | import android.os.Build; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * A Wrapper to MediaCodec that facilitates the use of API-dependent get{Input/Output}Buffer methods, 10 | * in order to prevent: http://stackoverflow.com/q/30646885 11 | */ 12 | class MediaCodecBuffers { 13 | 14 | private final MediaCodec mMediaCodec; 15 | private final ByteBuffer[] mInputBuffers; 16 | private ByteBuffer[] mOutputBuffers; 17 | 18 | MediaCodecBuffers(MediaCodec mediaCodec) { 19 | mMediaCodec = mediaCodec; 20 | 21 | if (Build.VERSION.SDK_INT < 21) { 22 | mInputBuffers = mediaCodec.getInputBuffers(); 23 | mOutputBuffers = mediaCodec.getOutputBuffers(); 24 | } else { 25 | mInputBuffers = mOutputBuffers = null; 26 | } 27 | } 28 | 29 | public ByteBuffer getInputBuffer(final int index) { 30 | if (Build.VERSION.SDK_INT >= 21) { 31 | return mMediaCodec.getInputBuffer(index); 32 | } 33 | ByteBuffer buffer = mInputBuffers[index]; 34 | buffer.clear(); 35 | return buffer; 36 | } 37 | 38 | public ByteBuffer getOutputBuffer(final int index) { 39 | if (Build.VERSION.SDK_INT >= 21) { 40 | return mMediaCodec.getOutputBuffer(index); 41 | } 42 | return mOutputBuffers[index]; 43 | } 44 | 45 | public void onOutputBuffersChanged() { 46 | if (Build.VERSION.SDK_INT < 21) { 47 | mOutputBuffers = mMediaCodec.getOutputBuffers(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/OutputBuffer.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | import android.media.MediaCodec; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | class OutputBuffer { 8 | MediaCodec.BufferInfo info; 9 | int trackIndex; 10 | ByteBuffer data; 11 | } 12 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/OutputBufferPool.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | import android.media.MediaCodec; 4 | 5 | class OutputBufferPool extends Pool { 6 | 7 | OutputBufferPool(final int trackIndex) { 8 | super(Integer.MAX_VALUE, new Pool.Factory() { 9 | @Override 10 | public OutputBuffer create() { 11 | OutputBuffer buffer = new OutputBuffer(); 12 | buffer.trackIndex = trackIndex; 13 | buffer.info = new MediaCodec.BufferInfo(); 14 | return buffer; 15 | } 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cameraview/video/Pool.java: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cameraview.video; 2 | 3 | import androidx.annotation.CallSuper; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | 7 | import com.lassi.presentation.cameraview.utils.CameraLogger; 8 | 9 | import java.util.concurrent.LinkedBlockingQueue; 10 | 11 | class Pool { 12 | 13 | private static final String TAG = Pool.class.getSimpleName(); 14 | private static final CameraLogger LOG = CameraLogger.create(TAG); 15 | 16 | private int maxPoolSize; 17 | private int activeCount; 18 | private LinkedBlockingQueue mQueue; 19 | private Factory factory; 20 | 21 | Pool(int maxPoolSize, Factory factory) { 22 | this.maxPoolSize = maxPoolSize; 23 | this.mQueue = new LinkedBlockingQueue<>(maxPoolSize); 24 | this.factory = factory; 25 | } 26 | 27 | boolean canGet() { 28 | return count() < maxPoolSize; 29 | } 30 | 31 | @Nullable 32 | T get() { 33 | T buffer = mQueue.poll(); 34 | if (buffer != null) { 35 | activeCount++; // poll decreases, this fixes 36 | LOG.v("GET: Reusing recycled item.", this); 37 | return buffer; 38 | } 39 | 40 | if (!canGet()) { 41 | LOG.v("GET: Returning null. Too much items requested.", this); 42 | return null; 43 | } 44 | 45 | activeCount++; 46 | LOG.v("GET: Creating a new item.", this); 47 | return factory.create(); 48 | } 49 | 50 | void recycle(@NonNull T item) { 51 | LOG.v("RECYCLE: Recycling item.", this); 52 | if (--activeCount < 0) { 53 | throw new IllegalStateException("Trying to recycle an item which makes activeCount < 0." + 54 | "This means that this or some previous items being recycled were not coming from " + 55 | "this pool, or some item was recycled more than once. " + this); 56 | } 57 | if (!mQueue.offer(item)) { 58 | throw new IllegalStateException("Trying to recycle an item while the queue is full. " + 59 | "This means that this or some previous items being recycled were not coming from " + 60 | "this pool, or some item was recycled more than once. " + this); 61 | } 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public String toString() { 67 | return getClass().getSimpleName() + " -- count:" + count() + ", active:" + activeCount() + ", cached:" + cachedCount(); 68 | } 69 | 70 | final int count() { 71 | return activeCount() + cachedCount(); 72 | } 73 | 74 | final int activeCount() { 75 | return activeCount; 76 | } 77 | 78 | final int cachedCount() { 79 | return mQueue.size(); 80 | } 81 | 82 | @CallSuper 83 | void clear() { 84 | mQueue.clear(); 85 | } 86 | 87 | interface Factory { 88 | T create(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/common/LassiBaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.common 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.annotation.CallSuper 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.viewbinding.ViewBinding 8 | import com.livefront.bridge.Bridge 9 | import io.reactivex.disposables.CompositeDisposable 10 | import io.reactivex.disposables.Disposable 11 | 12 | abstract class LassiBaseActivity : AppCompatActivity() { 13 | 14 | private var _binding: VB? = null 15 | protected val binding get() = _binding!! 16 | 17 | private val compositeDisposable = CompositeDisposable() 18 | 19 | abstract fun inflateLayout(layoutInflater: LayoutInflater): VB 20 | 21 | protected open fun getBundle() = Unit 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | getBundle() 26 | _binding = inflateLayout(layoutInflater) 27 | setContentView(binding.root) 28 | initViews() 29 | Bridge.restoreInstanceState(this, savedInstanceState) 30 | } 31 | 32 | @CallSuper 33 | protected open fun initViews() { 34 | } 35 | 36 | protected fun Disposable.collect() = compositeDisposable.add(this) 37 | 38 | fun hasExtra(key: String): Boolean { 39 | return intent.hasExtra(key) 40 | } 41 | 42 | override fun onSaveInstanceState(outState: Bundle) { 43 | super.onSaveInstanceState(outState) 44 | Bridge.saveInstanceState(this, outState) 45 | outState.clear() 46 | } 47 | 48 | override fun onDestroy() { 49 | super.onDestroy() 50 | Bridge.clear(this) 51 | _binding = null 52 | } 53 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/common/LassiBaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.common 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.CallSuper 8 | import androidx.fragment.app.Fragment 9 | import androidx.viewbinding.ViewBinding 10 | import com.livefront.bridge.Bridge 11 | import io.reactivex.disposables.CompositeDisposable 12 | import io.reactivex.disposables.Disposable 13 | 14 | abstract class LassiBaseFragment : Fragment() { 15 | 16 | private val compositeDisposable = CompositeDisposable() 17 | 18 | abstract fun inflateLayout(layoutInflater: LayoutInflater): VB 19 | 20 | open fun hasOptionMenu(): Boolean = false 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | initViews() 25 | } 26 | 27 | @CallSuper 28 | protected open fun initViews() { 29 | } 30 | 31 | protected fun Disposable.collect() = compositeDisposable.add(this) 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | Bridge.restoreInstanceState(this, savedInstanceState) 36 | savedInstanceState?.clear() 37 | } 38 | 39 | override fun onSaveInstanceState(outState: Bundle) { 40 | super.onSaveInstanceState(outState) 41 | Bridge.saveInstanceState(this, outState) 42 | outState.clear() 43 | } 44 | 45 | override fun onDestroy() { 46 | super.onDestroy() 47 | Bridge.clear(this) 48 | } 49 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/common/LassiBaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.common 2 | 3 | import androidx.annotation.CallSuper 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.disposables.CompositeDisposable 6 | import io.reactivex.disposables.Disposable 7 | 8 | abstract class LassiBaseViewModel : ViewModel() { 9 | 10 | private val compositeDisposable = CompositeDisposable() 11 | 12 | @CallSuper 13 | open fun loadPage() { 14 | 15 | } 16 | 17 | protected fun Disposable.collect() = compositeDisposable.add(this) 18 | 19 | override fun onCleared() { 20 | super.onCleared() 21 | compositeDisposable.dispose() 22 | } 23 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/common/LassiBaseViewModelActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.common 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.CallSuper 5 | import androidx.viewbinding.ViewBinding 6 | 7 | abstract class LassiBaseViewModelActivity : LassiBaseActivity() { 8 | 9 | protected val viewModel by lazy { buildViewModel() } 10 | 11 | protected abstract fun buildViewModel(): T 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | initLiveDataObservers() 16 | viewModel.loadPage() 17 | } 18 | 19 | @CallSuper 20 | protected open fun initLiveDataObservers() = Unit 21 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/common/LassiBaseViewModelFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.common 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.CallSuper 8 | import androidx.viewbinding.ViewBinding 9 | 10 | abstract class LassiBaseViewModelFragment : LassiBaseFragment() { 11 | private var _binding: VB? = null 12 | protected val binding get() = _binding!! 13 | 14 | protected val viewModel by lazy { buildViewModel() } 15 | 16 | protected abstract fun buildViewModel(): T 17 | 18 | protected open fun getBundle() = Unit 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View? { 25 | _binding = inflateLayout(inflater) 26 | setHasOptionsMenu(hasOptionMenu()) 27 | getBundle() 28 | return binding.root 29 | } 30 | 31 | override fun onActivityCreated(savedInstanceState: Bundle?) { 32 | super.onActivityCreated(savedInstanceState) 33 | initLiveDataObservers() 34 | viewModel.loadPage() 35 | } 36 | 37 | @CallSuper 38 | protected open fun initLiveDataObservers() { 39 | } 40 | 41 | override fun onDestroy() { 42 | super.onDestroy() 43 | _binding = null 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/common/decoration/GridSpacingItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.common.decoration 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.annotation.Px 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | class GridSpacingItemDecoration( 9 | private val columnCount: Int, 10 | @Px preferredSpace: Int, 11 | private val includeEdge: Boolean = true 12 | ) : RecyclerView.ItemDecoration() { 13 | 14 | /** 15 | * In this algorithm space should divide by 3 without remnant or width of 16 | * items can have a difference and we want them to be exactly the same 17 | */ 18 | private val space = 19 | if (preferredSpace % 3 == 0) preferredSpace else (preferredSpace + (3 - preferredSpace % 3)) 20 | 21 | override fun getItemOffsets( 22 | outRect: Rect, 23 | view: View, 24 | parent: RecyclerView, 25 | state: RecyclerView.State 26 | ) { 27 | val position = parent.getChildAdapterPosition(view) 28 | if (includeEdge) { 29 | when { 30 | position % columnCount == 0 -> { 31 | outRect.left = space 32 | outRect.right = space / 3 33 | } 34 | position % columnCount == columnCount - 1 -> { 35 | outRect.right = space 36 | outRect.left = space / 3 37 | } 38 | else -> { 39 | outRect.left = space * 2 / 3 40 | outRect.right = space * 2 / 3 41 | } 42 | } 43 | if (position < columnCount) { 44 | outRect.top = space 45 | } 46 | outRect.bottom = space 47 | } else { 48 | when { 49 | position % columnCount == 0 -> outRect.right = space * 2 / 3 50 | position % columnCount == columnCount - 1 -> outRect.left = space * 2 / 3 51 | else -> { 52 | outRect.left = space / 3 53 | outRect.right = space / 3 54 | } 55 | } 56 | if (position >= columnCount) { 57 | outRect.top = space 58 | } 59 | } 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/BitmapLoadingWorkerJob.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.isActive 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | import java.lang.ref.WeakReference 13 | import kotlin.coroutines.CoroutineContext 14 | 15 | internal class BitmapLoadingWorkerJob internal constructor( 16 | private val context: Context, 17 | cropImageView: CropImageView, 18 | internal val uri: Uri, 19 | ) : CoroutineScope { 20 | private val width: Int 21 | private val height: Int 22 | private val cropImageViewReference = WeakReference(cropImageView) 23 | private var job: Job = Job() 24 | 25 | override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job 26 | 27 | init { 28 | val metrics = cropImageView.resources.displayMetrics 29 | val densityAdjustment = if (metrics.density > 1) (1.0 / metrics.density) else 1.0 30 | width = (metrics.widthPixels * densityAdjustment).toInt() 31 | height = (metrics.heightPixels * densityAdjustment).toInt() 32 | } 33 | 34 | fun start() { 35 | job = launch(Dispatchers.Default) { 36 | try { 37 | if (isActive) { 38 | val decodeResult = BitmapUtils.decodeSampledBitmap( 39 | context = context, 40 | uri = uri, 41 | reqWidth = width, 42 | reqHeight = height, 43 | ) 44 | 45 | if (isActive) { 46 | val orientateResult = BitmapUtils.orientateBitmapByExif( 47 | bitmap = decodeResult.bitmap, 48 | context = context, 49 | uri = uri, 50 | ) 51 | 52 | onPostExecute( 53 | Result( 54 | uri = uri, 55 | bitmap = orientateResult.bitmap, 56 | loadSampleSize = decodeResult.sampleSize, 57 | degreesRotated = orientateResult.degrees, 58 | flipHorizontally = orientateResult.flipHorizontally, 59 | flipVertically = orientateResult.flipVertically, 60 | error = null, 61 | ), 62 | ) 63 | } 64 | } 65 | } catch (e: Exception) { 66 | onPostExecute( 67 | Result( 68 | uri = uri, 69 | bitmap = null, 70 | loadSampleSize = 0, 71 | degreesRotated = 0, 72 | flipHorizontally = false, 73 | flipVertically = false, 74 | error = e, 75 | ), 76 | ) 77 | } 78 | } 79 | } 80 | 81 | private suspend fun onPostExecute(result: Result) { 82 | withContext(Dispatchers.Main) { 83 | var completeCalled = false 84 | if (isActive) { 85 | cropImageViewReference.get()?.let { 86 | completeCalled = true 87 | it.onSetImageUriAsyncComplete(result) 88 | } 89 | } 90 | 91 | if (!completeCalled && result.bitmap != null) { 92 | // Fast release of unused bitmap. 93 | result.bitmap.recycle() 94 | } 95 | } 96 | } 97 | 98 | fun cancel() = job.cancel() 99 | 100 | internal data class Result( 101 | val uri: Uri, 102 | val bitmap: Bitmap?, 103 | val loadSampleSize: Int, 104 | val degreesRotated: Int, 105 | val flipHorizontally: Boolean, 106 | val flipVertically: Boolean, 107 | val error: Exception?, 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/CropException.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper 2 | 3 | import android.net.Uri 4 | 5 | sealed class CropException(message: String) : Exception(message) { 6 | class Cancellation : CropException("$EXCEPTION_PREFIX cropping has been cancelled by the user") { 7 | internal companion object { 8 | private const val serialVersionUID: Long = -6896269134508601990L 9 | } 10 | } 11 | 12 | class FailedToLoadBitmap(uri: Uri, message: String?) : CropException("$EXCEPTION_PREFIX Failed to load sampled bitmap: $uri\r\n$message") { 13 | internal companion object { 14 | private const val serialVersionUID: Long = 7791142932960927332L 15 | } 16 | } 17 | 18 | class FailedToDecodeImage(uri: Uri) : CropException("$EXCEPTION_PREFIX Failed to decode image: $uri") { 19 | internal companion object { 20 | private const val serialVersionUID: Long = 3516154387706407275L 21 | } 22 | } 23 | 24 | internal companion object { 25 | private const val serialVersionUID: Long = 4933890872862969613L 26 | const val EXCEPTION_PREFIX = "crop:" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/CropImageAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper 2 | 3 | import android.graphics.Matrix 4 | import android.graphics.RectF 5 | import android.view.animation.AccelerateDecelerateInterpolator 6 | import android.view.animation.Animation 7 | import android.view.animation.Animation.AnimationListener 8 | import android.view.animation.Transformation 9 | import android.widget.ImageView 10 | import com.lassi.presentation.cropper.CropOverlayView 11 | 12 | /** 13 | * Animation to handle smooth cropping image matrix transformation change, specifically for 14 | * zoom-in/out. 15 | */ 16 | internal class CropImageAnimation( 17 | private val imageView: ImageView, 18 | private val cropOverlayView: CropOverlayView, 19 | ) : Animation(), AnimationListener { 20 | 21 | private val startBoundPoints = FloatArray(8) 22 | private val endBoundPoints = FloatArray(8) 23 | private val startCropWindowRect = RectF() 24 | private val endCropWindowRect = RectF() 25 | private val startImageMatrix = FloatArray(9) 26 | private val endImageMatrix = FloatArray(9) 27 | 28 | init { 29 | duration = 300 30 | fillAfter = true 31 | interpolator = AccelerateDecelerateInterpolator() 32 | setAnimationListener(this) 33 | } 34 | 35 | fun setStartState(boundPoints: FloatArray, imageMatrix: Matrix) { 36 | reset() 37 | System.arraycopy(boundPoints, 0, startBoundPoints, 0, 8) 38 | startCropWindowRect.set(cropOverlayView.cropWindowRect) 39 | imageMatrix.getValues(startImageMatrix) 40 | } 41 | 42 | fun setEndState(boundPoints: FloatArray, imageMatrix: Matrix) { 43 | System.arraycopy(boundPoints, 0, endBoundPoints, 0, 8) 44 | endCropWindowRect.set(cropOverlayView.cropWindowRect) 45 | imageMatrix.getValues(endImageMatrix) 46 | } 47 | 48 | override fun applyTransformation(interpolatedTime: Float, t: Transformation) { 49 | val animRect = RectF().apply { 50 | left = (startCropWindowRect.left + (endCropWindowRect.left - startCropWindowRect.left) * interpolatedTime) 51 | top = (startCropWindowRect.top + (endCropWindowRect.top - startCropWindowRect.top) * interpolatedTime) 52 | right = (startCropWindowRect.right + (endCropWindowRect.right - startCropWindowRect.right) * interpolatedTime) 53 | bottom = (startCropWindowRect.bottom + (endCropWindowRect.bottom - startCropWindowRect.bottom) * interpolatedTime) 54 | } 55 | 56 | val animPoints = FloatArray(8) 57 | for (i in animPoints.indices) { 58 | animPoints[i] = (startBoundPoints[i] + (endBoundPoints[i] - startBoundPoints[i]) * interpolatedTime) 59 | } 60 | 61 | cropOverlayView.apply { 62 | cropWindowRect = animRect 63 | setBounds(animPoints, imageView.width, imageView.height) 64 | invalidate() 65 | } 66 | 67 | val animMatrix = FloatArray(9) 68 | for (i in animMatrix.indices) { 69 | animMatrix[i] = (startImageMatrix[i] + (endImageMatrix[i] - startImageMatrix[i]) * interpolatedTime) 70 | } 71 | 72 | imageView.apply { 73 | imageMatrix.setValues(animMatrix) 74 | invalidate() 75 | } 76 | } 77 | 78 | override fun onAnimationStart(animation: Animation) = Unit 79 | override fun onAnimationEnd(animation: Animation) { 80 | imageView.clearAnimation() 81 | } 82 | 83 | override fun onAnimationRepeat(animation: Animation) = Unit 84 | } 85 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/CropImageContract.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import com.lassi.common.utils.KeyUtils 9 | import com.lassi.data.media.MiMedia 10 | 11 | /** 12 | * An [ActivityResultContract] to start an activity that allows the user to crop an image. 13 | * The UI can be customized using [CropImageOptions]. 14 | * If you do not provide an [CropImageContractOptions.uri] in the input the user will be asked to pick an image before cropping. 15 | */ 16 | class CropImageContract : ActivityResultContract() { 17 | override fun createIntent(context: Context, input: CropImageContractOptions) = 18 | Intent(context, CropImageActivity::class.java).apply { 19 | putExtra( 20 | CropImage.CROP_IMAGE_EXTRA_BUNDLE, 21 | Bundle(2).apply { 22 | putParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE, input.uri) 23 | putParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS, input.cropImageOptions) 24 | }, 25 | ) 26 | } 27 | 28 | override fun parseResult(resultCode: Int, intent: Intent?): MiMedia? { 29 | if (resultCode != Activity.RESULT_OK) return null 30 | return intent?.getParcelableExtra(KeyUtils.MEDIA_PREVIEW) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/CropImageContractOptions.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper 2 | 3 | import android.net.Uri 4 | 5 | data class CropImageContractOptions( 6 | val uri: Uri?, 7 | val cropImageOptions: CropImageOptions, 8 | ) 9 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/ParcelableUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Parcelable 6 | 7 | inline fun Bundle.parcelable(key: String): T? = when { 8 | // Does not work yet, https://issuetracker.google.com/issues/240585930 9 | // SDK_INT >= 33 -> getParcelable(key, T::class.java) 10 | else -> @Suppress("DEPRECATION") getParcelable(key) as? T 11 | } 12 | 13 | inline fun Intent.parcelable(key: String): T? = when { 14 | // Does not work yet, https://issuetracker.google.com/issues/240585930 15 | // SDK_INT >= 33 -> getParcelable(key, T::class.java) 16 | else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T 17 | } 18 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/cropper/utils/GetFilePathFromUri.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.cropper.utils 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.webkit.MimeTypeMap 7 | import java.io.File 8 | import java.io.FileOutputStream 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.io.OutputStream 12 | import java.text.SimpleDateFormat 13 | import java.util.Date 14 | import java.util.Locale.getDefault 15 | 16 | internal fun getFilePathFromUri(context: Context, uri: Uri, uniqueName: Boolean): String = 17 | if (uri.path?.contains("file://") == true) { 18 | uri.path!! 19 | } else { 20 | getFileFromContentUri(context, uri, uniqueName).path 21 | } 22 | 23 | private fun getFileFromContentUri(context: Context, contentUri: Uri, uniqueName: Boolean): File { 24 | // Preparing Temp file name 25 | val fileExtension = getFileExtension(context, contentUri) ?: "" 26 | val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", getDefault()).format(Date()) 27 | val fileName = ("temp_file_" + if (uniqueName) timeStamp else "") + ".$fileExtension" 28 | // Creating Temp file 29 | val tempFile = File(context.cacheDir, fileName) 30 | tempFile.createNewFile() 31 | // Initialize streams 32 | var oStream: FileOutputStream? = null 33 | var inputStream: InputStream? = null 34 | 35 | try { 36 | oStream = FileOutputStream(tempFile) 37 | inputStream = context.contentResolver.openInputStream(contentUri) 38 | 39 | inputStream?.let { copy(inputStream, oStream) } 40 | oStream.flush() 41 | } catch (e: Exception) { 42 | e.printStackTrace() 43 | } finally { 44 | // Close streams 45 | inputStream?.close() 46 | oStream?.close() 47 | } 48 | 49 | return tempFile 50 | } 51 | 52 | private fun getFileExtension(context: Context, uri: Uri): String? = 53 | if (uri.scheme == ContentResolver.SCHEME_CONTENT) { 54 | MimeTypeMap.getSingleton().getExtensionFromMimeType(context.contentResolver.getType(uri)) 55 | } else { 56 | uri.path?.let { MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(it)).toString()) } 57 | } 58 | 59 | @Throws(IOException::class) 60 | private fun copy(source: InputStream, target: OutputStream) { 61 | val buf = ByteArray(8192) 62 | var length: Int 63 | while (source.read(buf).also { length = it } > 0) { 64 | target.write(buf, 0, length) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/docs/DocsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.docs 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.viewModelScope 5 | import com.lassi.data.common.Response 6 | import com.lassi.data.common.Result 7 | import com.lassi.data.media.MiMedia 8 | import com.lassi.domain.media.MediaRepository 9 | import com.lassi.presentation.common.LassiBaseViewModel 10 | import kotlinx.coroutines.flow.collect 11 | import kotlinx.coroutines.flow.onStart 12 | import kotlinx.coroutines.launch 13 | import java.util.* 14 | 15 | class DocsViewModel(private val mediaRepository: MediaRepository) : LassiBaseViewModel() { 16 | var fetchDocsLiveData = MutableLiveData>>() 17 | 18 | fun fetchDocs() { 19 | viewModelScope.launch { 20 | mediaRepository.fetchDocs() 21 | .onStart { 22 | fetchDocsLiveData.postValue(Response.Loading()) 23 | }.collect { result -> 24 | when (result) { 25 | is Result.Success -> { 26 | fetchDocsLiveData.postValue(Response.Success(result.data)) 27 | } 28 | is Result.Error -> { 29 | fetchDocsLiveData.postValue(Response.Error(result.throwable)) 30 | } 31 | else -> { 32 | /** 33 | * no need to implement 34 | */ 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/docs/DocsViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.docs 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.lassi.data.media.repository.MediaRepositoryImpl 7 | import com.lassi.domain.media.MediaRepository 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | class DocsViewModelFactory(val context: Context) : ViewModelProvider.Factory { 11 | private val mediaRepository: MediaRepository = MediaRepositoryImpl(context) 12 | 13 | override fun create(modelClass: Class): T { 14 | return DocsViewModel(mediaRepository) as T 15 | } 16 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/media/SelectedMediaViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.media 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MediatorLiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import com.lassi.data.common.Response 8 | import com.lassi.data.common.Result 9 | import com.lassi.data.media.MiMedia 10 | import com.lassi.domain.media.LassiConfig 11 | import com.lassi.domain.media.MediaType 12 | import com.lassi.domain.media.SelectedMediaRepository 13 | import com.lassi.presentation.common.LassiBaseViewModel 14 | import kotlinx.coroutines.flow.onStart 15 | import kotlinx.coroutines.launch 16 | 17 | class SelectedMediaViewModel( 18 | private val selectedMediaRepository: SelectedMediaRepository 19 | ) : LassiBaseViewModel() { 20 | val selectedMediaLiveData = MutableLiveData>() 21 | private var selectedMedias = arrayListOf() 22 | 23 | var fetchedMediaLiveData = MutableLiveData>>() 24 | 25 | private val _currentSortingOption: MediatorLiveData = 26 | MediatorLiveData(LassiConfig.getConfig().ascFlag) 27 | val currentSortingOption: LiveData = _currentSortingOption 28 | 29 | fun currentSortingOptionUpdater(currentSortingOption: Int) { 30 | _currentSortingOption.value = currentSortingOption 31 | } 32 | 33 | fun addAllSelectedMedia(selectedMedias: ArrayList) { 34 | this.selectedMedias = selectedMedias 35 | this.selectedMedias = this.selectedMedias.distinctBy { 36 | it.path 37 | } as ArrayList 38 | selectedMediaLiveData.value = this.selectedMedias 39 | } 40 | 41 | fun addSelectedMedia(selectedMedia: MiMedia) { 42 | this.selectedMedias.add(selectedMedia) 43 | this.selectedMedias = this.selectedMedias.distinctBy { it.path } as ArrayList 44 | selectedMediaLiveData.value = this.selectedMedias 45 | } 46 | 47 | fun getSelectedMediaData(bucket: String) { 48 | viewModelScope.launch { 49 | selectedMediaRepository.getSelectedMediaData(bucket).onStart { 50 | fetchedMediaLiveData.value = Response.Loading() 51 | }.collect { result -> 52 | when (result) { 53 | is Result.Success -> result.data.let { 54 | fetchedMediaLiveData.postValue(Response.Success(it)) 55 | } 56 | 57 | is Result.Error -> fetchedMediaLiveData.value = Response.Error(result.throwable) 58 | else -> {} 59 | } 60 | } 61 | } 62 | } 63 | 64 | fun getSortedDataFromDb(bucket: String, isAsc: Int, mediaType: MediaType) { 65 | viewModelScope.launch { 66 | selectedMediaRepository.getSortedDataFromDb(bucket, isAsc, mediaType).onStart { 67 | fetchedMediaLiveData.value = Response.Loading() 68 | }.collect { result -> 69 | when (result) { 70 | is Result.Success -> result.data.let { 71 | fetchedMediaLiveData.postValue(Response.Success(it)) 72 | } 73 | 74 | is Result.Error -> { 75 | fetchedMediaLiveData.value = Response.Error(result.throwable) 76 | } 77 | 78 | else -> {} 79 | } 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/mediadirectory/CropImageImpl.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.mediadirectory 2 | 3 | import android.net.Uri 4 | import com.lassi.presentation.cropper.CropImageActivity 5 | import com.lassi.presentation.cropper.CropImageView 6 | 7 | abstract class CropImageImpl: CropImageActivity() { 8 | override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) { 9 | super.onSetImageUriComplete(view, uri, error) 10 | } 11 | 12 | override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) { 13 | super.onCropImageComplete(view, result) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.mediadirectory 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.lassi.data.media.repository.MediaRepositoryImpl 7 | import com.lassi.domain.media.MediaRepository 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | class FolderViewModelFactory(val context: Context) : ViewModelProvider.Factory { 11 | private val mediaRepository: MediaRepository = MediaRepositoryImpl(context) 12 | 13 | override fun create(modelClass: Class): T { 14 | return FolderViewModel(mediaRepository) as T 15 | } 16 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/mediadirectory/SelectedMediaViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.mediadirectory 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.lassi.data.media.repository.SelectedMediaRepositoryImpl 7 | import com.lassi.domain.media.SelectedMediaRepository 8 | import com.lassi.presentation.media.SelectedMediaViewModel 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | class SelectedMediaViewModelFactory(val context: Context) : ViewModelProvider.Factory { 12 | private val selectedMediaRepository: SelectedMediaRepository = 13 | SelectedMediaRepositoryImpl(context) 14 | 15 | override fun create(modelClass: Class): T { 16 | return SelectedMediaViewModel(selectedMediaRepository) as T 17 | } 18 | } -------------------------------------------------------------------------------- /lassi/src/main/java/com/lassi/presentation/mediadirectory/adapter/FolderAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lassi.presentation.mediadirectory.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.lassi.R 6 | import com.lassi.common.extenstions.hide 7 | import com.lassi.common.extenstions.loadImage 8 | import com.lassi.common.extenstions.show 9 | import com.lassi.common.extenstions.toBinding 10 | import com.lassi.data.media.MiItemMedia 11 | import com.lassi.databinding.ItemMediaBinding 12 | 13 | class FolderAdapter( 14 | private val onItemClick: (bucket: MiItemMedia) -> Unit 15 | ) : RecyclerView.Adapter() { 16 | 17 | private var buckets = ArrayList() 18 | 19 | fun setList(buckets: ArrayList?) { 20 | buckets?.let { 21 | this.buckets.clear() 22 | this.buckets.addAll(it) 23 | } 24 | notifyDataSetChanged() 25 | } 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder { 28 | return FolderViewHolder(parent.toBinding()) 29 | } 30 | 31 | override fun getItemCount() = buckets.size 32 | 33 | override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { 34 | holder.bind(buckets[position]) 35 | } 36 | 37 | fun clear() { 38 | val size: Int = buckets.size 39 | buckets.clear() 40 | notifyItemRangeRemoved(0, size) 41 | } 42 | 43 | inner class FolderViewHolder(val binding: ItemMediaBinding) : 44 | RecyclerView.ViewHolder(binding.root) { 45 | fun bind(bucket: MiItemMedia) { 46 | with(bucket) { 47 | binding.apply { 48 | tvFolderName.show() 49 | tvDuration.hide() 50 | ivFolderThumbnail.loadImage(bucket.latestItemPathForBucket) 51 | tvFolderName.text = String.format( 52 | tvFolderName.context.getString(R.string.directory_with_item_count), 53 | bucketName, 54 | totalItemSizeForBucket.toString() 55 | ) 56 | itemView.setOnClickListener { 57 | onItemClick(bucket) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /lassi/src/main/res/anim/right_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/anim/right_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/focus_marker_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/focus_marker_outline.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_back_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_camera_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_checked_media.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_crop_image_menu_flip.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_crop_image_menu_rotate_left.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_crop_image_menu_rotate_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_done_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_flash_auto_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_flash_off_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_flash_on_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_flip_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_flip_camera_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_image_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_rotate_left_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_rotate_right_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_sorting_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/ic_tick_red.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/shape_circle_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/drawable/shape_circle_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/activity_media_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 23 | 24 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/activity_video_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 24 | 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/cameraview_gl_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/cameraview_layout_focus_marker.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/cameraview_surface_view.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/cameraview_texture_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/crop_image_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/crop_image_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 13 | 19 | 25 | 26 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/fragment_media_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 34 | 35 | 44 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/item_media.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 33 | 34 | 47 | 48 | 62 | 63 | 75 | 76 | -------------------------------------------------------------------------------- /lassi/src/main/res/layout/sorting_option.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 20 | 21 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lassi/src/main/res/menu/crop_image_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 10 | 15 | 20 | 21 | 24 | 27 | 28 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /lassi/src/main/res/menu/crop_image_menu_old.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 16 | 21 | 22 | 25 | 28 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /lassi/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 13 | 14 | 17 | 20 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /lassi/src/main/res/menu/media_picker_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /lassi/src/main/res/menu/video_preview_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /lassi/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1f33cb 4 | #041697 5 | #ec2b4e 6 | 7 | #80000000 8 | #50000000 9 | #FFA000 10 | 11 | -------------------------------------------------------------------------------- /lassi/src/main/res/values/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 400 4 | -------------------------------------------------------------------------------- /lassi/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | 19 | 20 | 33 | 34 | -------------------------------------------------------------------------------- /lassi/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /media/image-picker-camera.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/media/image-picker-camera.gif -------------------------------------------------------------------------------- /media/image-picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/media/image-picker.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | maven { url "https://jitpack.io" } 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | maven { url 'https://jitpack.io' } 14 | mavenCentral() 15 | } 16 | } 17 | rootProject.name = "Lassi sample" 18 | include ':app', ':lassi' --------------------------------------------------------------------------------