├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── raw │ │ │ │ └── pink.wav │ │ │ ├── font │ │ │ │ ├── sans.ttf │ │ │ │ ├── sans_b.ttf │ │ │ │ ├── sans_bl.ttf │ │ │ │ ├── sans_l.ttf │ │ │ │ ├── sans_m.ttf │ │ │ │ └── sans_ul.ttf │ │ │ ├── drawable │ │ │ │ ├── mic.png │ │ │ │ ├── wav.png │ │ │ │ ├── arrow.png │ │ │ │ ├── empty.png │ │ │ │ ├── image.png │ │ │ │ ├── background_stroke.xml │ │ │ │ ├── ic_stop_fill.xml │ │ │ │ ├── ic_stop_stroke.xml │ │ │ │ ├── ic_play_fill.xml │ │ │ │ ├── ic_play_stroke.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── ic_pause_stroke.xml │ │ │ │ ├── ic_trash_stroke.xml │ │ │ │ ├── ic_checkmark_green.xml │ │ │ │ └── ic_launcher_background.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 │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── colors.xml │ │ │ │ └── strings.xml │ │ │ ├── xml │ │ │ │ └── provider_paths.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── item_sample.xml │ │ │ │ ├── custome_primary_button.xml │ │ │ │ ├── activity_sample.xml │ │ │ │ ├── activity_slide.xml │ │ │ │ ├── item_recording.xml │ │ │ │ └── activity_parse.xml │ │ │ ├── values-zh-rCN │ │ │ │ └── strings.xml │ │ │ └── values-fa │ │ │ │ └── strings.xml │ │ ├── web_hi_res_512.png │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ │ └── ir │ │ │ │ └── mrahimy │ │ │ │ └── conceal │ │ │ │ ├── data │ │ │ │ ├── MediaState.kt │ │ │ │ ├── Sample.kt │ │ │ │ ├── Rgb.kt │ │ │ │ ├── enums │ │ │ │ │ ├── RevealState.kt │ │ │ │ │ ├── FileSavingState.kt │ │ │ │ │ └── ChooserType.kt │ │ │ │ ├── capsules │ │ │ │ │ ├── TwoParts.kt │ │ │ │ │ ├── ConcealPercentage.kt │ │ │ │ │ ├── SaveWaveInfoCapsule.kt │ │ │ │ │ ├── SaveBitmapInfoCapsule.kt │ │ │ │ │ └── ConcealInputData.kt │ │ │ │ ├── LocalResult.kt │ │ │ │ ├── SeparatedDigits.kt │ │ │ │ ├── Waver.kt │ │ │ │ └── Recording.kt │ │ │ │ ├── base │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── BaseAndroidViewModel.kt │ │ │ │ ├── BaseActivity.kt │ │ │ │ └── BaseAdapter.kt │ │ │ │ ├── net │ │ │ │ ├── error │ │ │ │ │ └── ApiException.kt │ │ │ │ ├── BaseUrl.kt │ │ │ │ ├── res │ │ │ │ │ └── ApiResponse.kt │ │ │ │ ├── ApiResult.kt │ │ │ │ ├── api │ │ │ │ │ └── InfoApi.kt │ │ │ │ ├── Exclude.kt │ │ │ │ ├── Util.kt │ │ │ │ ├── ApiInterceptor.kt │ │ │ │ └── req │ │ │ │ │ ├── AudioInfo.kt │ │ │ │ │ └── ImageInfo.kt │ │ │ │ ├── repository │ │ │ │ ├── SampleRepository.kt │ │ │ │ ├── InfoRepository.kt │ │ │ │ ├── RecordingRepository.kt │ │ │ │ ├── SampleRepositoryImpl.kt │ │ │ │ ├── RecordingRepositoryImpl.kt │ │ │ │ └── InfoRepositoryImpl.kt │ │ │ │ ├── di │ │ │ │ ├── ApiModule.kt │ │ │ │ ├── AdapterModule.kt │ │ │ │ ├── RepositoryModule.kt │ │ │ │ ├── ViewModelModule.kt │ │ │ │ ├── DbModule.kt │ │ │ │ └── NetworkModule.kt │ │ │ │ ├── util │ │ │ │ ├── ktx │ │ │ │ │ ├── view │ │ │ │ │ │ └── View.kt │ │ │ │ │ ├── Time.kt │ │ │ │ │ ├── Gson.kt │ │ │ │ │ ├── String.kt │ │ │ │ │ ├── Bitmap.kt │ │ │ │ │ ├── LowLevelInt.kt │ │ │ │ │ ├── Compat.kt │ │ │ │ │ ├── MediaStore.kt │ │ │ │ │ └── FileUtils.java │ │ │ │ ├── ba │ │ │ │ │ ├── RecyclerView.kt │ │ │ │ │ ├── ImageView.kt │ │ │ │ │ └── View.kt │ │ │ │ ├── lowlevel │ │ │ │ │ ├── LowLevelIntOperations.java │ │ │ │ │ ├── LowLevelRgbOperations.java │ │ │ │ │ └── WavUtil.java │ │ │ │ ├── Bitmap.kt │ │ │ │ ├── File.kt │ │ │ │ ├── arch │ │ │ │ │ ├── Event.kt │ │ │ │ │ └── CombineLiveData.kt │ │ │ │ ├── cv │ │ │ │ │ ├── PrimaryButton.kt │ │ │ │ │ ├── CameraCorner.kt │ │ │ │ │ └── VisualizerView.java │ │ │ │ └── WaveFileErrorCodeMapper.kt │ │ │ │ ├── db │ │ │ │ ├── migrations │ │ │ │ │ └── Migrations_1_10.kt │ │ │ │ ├── ConcealDb.kt │ │ │ │ └── dao │ │ │ │ │ └── RecordingDao.kt │ │ │ │ ├── ui │ │ │ │ ├── sample │ │ │ │ │ ├── SampleAdapter.kt │ │ │ │ │ ├── SampleActivity.kt │ │ │ │ │ └── SampleViewModel.kt │ │ │ │ ├── slide │ │ │ │ │ ├── SlideShowViewModel.kt │ │ │ │ │ └── SlideShowActivity.kt │ │ │ │ ├── home │ │ │ │ │ ├── RecordingsAdapter.kt │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── parse │ │ │ │ │ └── ParseActivity.kt │ │ │ │ └── app │ │ │ │ └── ConcealApplication.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── ir │ │ │ └── mrahimy │ │ │ └── conceal │ │ │ ├── RegexUnitTest.kt │ │ │ ├── LoopTest.kt │ │ │ ├── SubstringUnitTest.kt │ │ │ ├── WaveManipulationUnitTest.kt │ │ │ ├── IntUnitTest.kt │ │ │ └── LowLevelOperationsManipulationUnitTest.kt │ └── androidTest │ │ └── java │ │ └── ir │ │ └── mrahimy │ │ └── conceal │ │ ├── ExampleInstrumentedTest.kt │ │ └── LowLevelOperationsManipulationInstrumentedTest.kt ├── proguard-rules.pro ├── google-services.json └── build.gradle ├── fastlane └── metadata │ └── android │ ├── fa │ ├── video.txt │ ├── changelogs │ │ └── 4.txt │ ├── title.txt │ ├── short_description.txt │ ├── images │ │ └── phoneScreenshots │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ ├── 5.jpg │ │ │ ├── 6.jpg │ │ │ ├── 7.jpg │ │ │ └── 8.jpg │ └── full_description.txt │ └── en-US │ ├── video.txt │ ├── changelogs │ └── 4.txt │ ├── title.txt │ ├── short_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ ├── 04.png │ │ ├── 05.png │ │ └── 06.png │ └── full_description.txt ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── local.properties ├── gradle.properties ├── README.md ├── .gitignore ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/video.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/video.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Conceal 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/title.txt: -------------------------------------------------------------------------------- 1 | پنهان‌سازی 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name='Conceal' 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/short_description.txt: -------------------------------------------------------------------------------- 1 | مخفی کردن صدا داخل تصاویر 2 | 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Concealing WAVE audio files inside images 2 | -------------------------------------------------------------------------------- /app/src/main/res/raw/pink.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/raw/pink.wav -------------------------------------------------------------------------------- /app/src/main/res/font/sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/font/sans.ttf -------------------------------------------------------------------------------- /app/src/main/web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/web_hi_res_512.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/drawable/mic.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/wav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/drawable/wav.png -------------------------------------------------------------------------------- /app/src/main/res/font/sans_b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/font/sans_b.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/sans_bl.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/font/sans_bl.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/sans_l.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/font/sans_l.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/sans_m.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/font/sans_m.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/sans_ul.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/font/sans_ul.ttf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/drawable/arrow.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/drawable/empty.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/drawable/image.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/MediaState.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | enum class MediaState { 3 | STOP, PLAY 4 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/Sample.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | 3 | data class Sample(val id: Int, val text: String) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/Rgb.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | 3 | data class Rgb( 4 | var r: Int, 5 | var g: Int, 6 | var b: Int 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/enums/RevealState.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.enums 2 | 3 | enum class RevealState { 4 | IDLE, REVEALING, DONE 5 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/enums/FileSavingState.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.enums 2 | 3 | enum class FileSavingState { 4 | SAVING, IDLE, DONE 5 | } -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4D5EFF 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/1.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/2.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/3.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/4.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/5.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/6.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/7.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/images/phoneScreenshots/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/fa/images/phoneScreenshots/8.jpg -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | abstract class BaseViewModel : ViewModel() -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/conceal/fdroid/fastlane/metadata/android/en-US/images/phoneScreenshots/06.png -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/capsules/TwoParts.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.capsules 2 | 3 | data class TwoParts( 4 | val number: T1, 5 | val position: T2 6 | ) -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/error/ApiException.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net.error 2 | 3 | import java.io.IOException 4 | 5 | class ApiException(val statusCode: Int, e: Throwable? = null) : IOException(e) -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64dp 4 | 20dp 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Useful for hiding wave audio data inside the least significant bits of an image. The resulting image can be shared in social media and the received images can be parsed by this app. 2 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/BaseUrl.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net 2 | 3 | object BaseUrl { 4 | // private const val VERSION = 1 5 | //const val BASE_URL = "https://conceal.ir/api/v$VERSION/" 6 | const val BASE_URL = "https://conceal.ir/" 7 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/repository/SampleRepository.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.repository 2 | 3 | import ir.mrahimy.conceal.data.Sample 4 | 5 | interface SampleRepository { 6 | fun getSampleInitList(): List 7 | fun getRandomSample(size: Int): Sample 8 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/di/ApiModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.di 2 | 3 | import ir.mrahimy.conceal.net.api.InfoApi 4 | import org.koin.dsl.module 5 | import retrofit2.Retrofit 6 | import retrofit2.create 7 | 8 | val apiModule = module { 9 | factory { get(RetrofitServiceQ).create() } 10 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/repository/InfoRepository.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.repository 2 | 3 | import ir.mrahimy.conceal.net.ApiResult 4 | 5 | interface InfoRepository { 6 | suspend fun putImageInfo(params: Map): ApiResult 7 | suspend fun putAudioInfo(params: Map): ApiResult 8 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/di/AdapterModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.di 2 | 3 | import ir.mrahimy.conceal.ui.home.RecordingsAdapter 4 | import ir.mrahimy.conceal.ui.sample.SampleAdapter 5 | import org.koin.dsl.module 6 | 7 | val adapterModule = module { 8 | factory { SampleAdapter() } 9 | factory { RecordingsAdapter() } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/view/View.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx.view 2 | 3 | import android.view.View 4 | 5 | fun View.visible() { 6 | this.visibility = View.VISIBLE 7 | } 8 | 9 | fun View.invisible() { 10 | this.visibility = View.INVISIBLE 11 | } 12 | 13 | fun View.gone() { 14 | this.visibility = View.GONE 15 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 30 14:10:45 IRST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/res/ApiResponse.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net.res 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ApiResponse( 6 | @SerializedName("status_code") 7 | val statusCode: Int, 8 | @SerializedName("status_txt") 9 | val statusText: String, 10 | @SerializedName("data") 11 | val data: Any? 12 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_stroke.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/ApiResult.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net 2 | 3 | import androidx.annotation.StringRes 4 | 5 | sealed class ApiResult { 6 | 7 | data class Success(val data: T) : ApiResult() 8 | data class Error( 9 | @StringRes val stringRes: Int, 10 | val errorCode: Int 11 | ) : ApiResult() 12 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/enums/ChooserType.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.enums 2 | 3 | import android.net.Uri 4 | import android.provider.MediaStore 5 | 6 | enum class ChooserType(val typeString: String, val externalContentUri: Uri) { 7 | Image("image/*", MediaStore.Images.Media.EXTERNAL_CONTENT_URI), 8 | Audio("audio/x-wav", MediaStore.Audio.Media.EXTERNAL_CONTENT_URI), 9 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/db/migrations/Migrations_1_10.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.db.migrations 2 | 3 | import androidx.room.migration.Migration 4 | import androidx.sqlite.db.SupportSQLiteDatabase 5 | 6 | val migration1to2 = object : Migration(1,2) { 7 | override fun migrate(database: SupportSQLiteDatabase) { 8 | database.execSQL("DROP TABLE IF EXISTS EMPTY") 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/api/InfoApi.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net.api 2 | 3 | import retrofit2.http.POST 4 | import retrofit2.http.QueryMap 5 | 6 | interface InfoApi { 7 | 8 | @POST("image.php") 9 | suspend fun putImageInfo(@QueryMap params: Map): Any 10 | 11 | @POST("audio.php") 12 | suspend fun putAudioInfo(@QueryMap params: Map): Any 13 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/repository/RecordingRepository.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import ir.mrahimy.conceal.data.Recording 5 | 6 | interface RecordingRepository { 7 | 8 | suspend fun addRecording(recording: Recording) 9 | suspend fun deleteRecording(recording: Recording) 10 | fun getAllRecordings(): LiveData> 11 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/Time.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx 2 | 3 | import saman.zamani.persiandate.PersianDate 4 | import saman.zamani.persiandate.PersianDateFormat 5 | 6 | 7 | /** 8 | * converts epoch millis to persian date format 9 | */ 10 | fun Long.toPersianFormat(format: String = "Y-m-d H:i:s"): String { 11 | return PersianDateFormat(format).format(PersianDate(this)) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/LocalResult.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | 3 | import androidx.annotation.StringRes 4 | 5 | sealed class LocalResult { 6 | 7 | data class Success(val data: T) : LocalResult() 8 | data class Error( 9 | @StringRes val stringRes: Int, 10 | val errorCode: Int, 11 | val e: Exception 12 | ) : LocalResult() 13 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_fill.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/capsules/ConcealPercentage.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.capsules 2 | 3 | import android.graphics.Bitmap 4 | 5 | data class ConcealPercentage( 6 | val id: Int, 7 | val percent: Float, 8 | val data: Bitmap?, 9 | val positionOnList: Int, 10 | val lastWaveArrayIndexChecked: Int, 11 | val done: Boolean 12 | 13 | ) 14 | 15 | fun empty() = ConcealPercentage(1, 0f, null, 0, 0, false) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_stroke.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/db/ConcealDb.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import ir.mrahimy.conceal.data.Recording 6 | import ir.mrahimy.conceal.db.dao.RecordingDao 7 | 8 | @Database( 9 | entities = [Recording::class], 10 | version = 1, exportSchema = false 11 | ) 12 | abstract class ConcealDb : RoomDatabase() { 13 | 14 | abstract fun recordingDao(): RecordingDao 15 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/mrahimy/conceal/RegexUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal 2 | 3 | import ir.mrahimy.conceal.util.ktx.getNameFromPath 4 | import org.junit.Test 5 | 6 | import org.junit.Assert.* 7 | 8 | class RegexUnitTest { 9 | 10 | @Test 11 | fun `test if file name is correct on getting name`(){ 12 | val str = "/storage/hello / but wow!/ nook$.mp3" 13 | val name = str.getNameFromPath() 14 | assert(name == " nook\$") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fa/full_description.txt: -------------------------------------------------------------------------------- 1 | با این برنامه می‌تونید یک فایل صوتی از نوع wave رو در داخل یک عکس پنهان کنید بدون اینکه حجم عکس تغیر خاصی بکنه یا ظاهر عکس عوض بشه. 2 | 3 | 4 | عکس‌هایی رو که به این صورت تغییر داده‌شدن می‌تونید توی همین برنامه باز کنید تا فایل صوتی از داخلش استخراج بشه. 5 | 6 | 7 | در حال حاضر بهترین روش استفاده، ضبط صدا از طریق برنامه است و البته فایل‌های با فرمت wav هم پشتیبانی می‌شوند. فرمت‌های دیگر مثل mp3 پشتیبانی نمی‌شوند. 8 | 9 | 10 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file should *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | sdk.dir=/home/vincent/Android/Sdk 11 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ba/RecyclerView.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ba 2 | 3 | 4 | import androidx.databinding.BindingAdapter 5 | import androidx.recyclerview.widget.RecyclerView 6 | import ir.mrahimy.conceal.base.BaseAdapter 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | @BindingAdapter("data") 10 | fun RecyclerView.setData(data: MutableList?) { 11 | if (adapter is BaseAdapter<*>) { 12 | (adapter as BaseAdapter).submitList(data) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/Gson.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx 2 | 3 | import com.google.gson.Gson 4 | import ir.mrahimy.conceal.net.error.ApiException 5 | import ir.mrahimy.conceal.net.res.ApiResponse 6 | 7 | fun Gson.extractData(data: String): String { 8 | val res = fromJson(data, ApiResponse::class.java) 9 | if (res.statusCode == 200) { 10 | return if (res.data == null) "{}" else toJson(res.data) 11 | } else throw ApiException(res.statusCode) 12 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_fill.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/repository/SampleRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.repository 2 | 3 | import ir.mrahimy.conceal.data.Sample 4 | import kotlin.random.Random 5 | 6 | class SampleRepositoryImpl : SampleRepository { 7 | override fun getSampleInitList(): List = 8 | listOf( 9 | Sample(1, "one"), 10 | Sample(2, "two"), 11 | Sample(3, "three") 12 | ) 13 | 14 | override fun getRandomSample(size: Int) = 15 | Sample( 16 | Random.nextInt(size, 100), "Random ##" 17 | ) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/String.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import java.io.File 6 | 7 | fun String.toValidPath(): String { 8 | return if (this.endsWith('/')) this else "${this}/" 9 | } 10 | 11 | fun String.getNameFromPath() = File(this).name.split('.')[0] 12 | 13 | fun String.removeEmulatedPath() = replace("/storage/emulated/0/", "") 14 | 15 | //fun String.removeNumbers() = replace("\\d+", "") 16 | 17 | fun String.loadBitmap(): Bitmap = BitmapFactory.decodeFile(this) -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/capsules/SaveWaveInfoCapsule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.capsules 2 | 3 | import ir.mrahimy.conceal.data.Waver 4 | import ir.mrahimy.conceal.util.ktx.toValidPath 5 | import ir.mrahimy.conceal.util.writeWave 6 | import java.io.File 7 | import java.util.* 8 | 9 | data class SaveWaveInfoCapsule( 10 | val name: String?, 11 | val time: Date?, 12 | val data: Waver 13 | ) 14 | 15 | fun SaveWaveInfoCapsule.save(path: String): String { 16 | val filePath = path.toValidPath() + "${name}_${time?.time}.wav" 17 | File(filePath).writeWave(data) 18 | return filePath 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/lowlevel/LowLevelIntOperations.java: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.lowlevel; 2 | 3 | public class LowLevelIntOperations { 4 | public static int removeLsBits(int in) { 5 | return in & 248; 6 | } 7 | 8 | public static int get2LsBits(int in) { 9 | return in & 3; 10 | } 11 | 12 | public static int get3LsBits(int in) { 13 | return in & 7; 14 | } 15 | 16 | public static int and(int in1, int in2) { 17 | return in1 & in2; 18 | } 19 | 20 | public static int or(int in1, int in2) { 21 | return in1 | in2; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ba/ImageView.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ba 2 | 3 | import android.graphics.Bitmap 4 | import android.widget.ImageView 5 | import androidx.annotation.DrawableRes 6 | import androidx.databinding.BindingAdapter 7 | import ir.mrahimy.conceal.util.ktx.getDrawableCompat 8 | 9 | @BindingAdapter("bitmap") 10 | fun ImageView.setBitmap(bitmap: Bitmap?) = bitmap?.let { 11 | setImageBitmap(bitmap) 12 | } 13 | 14 | @BindingAdapter("drawableCompat") 15 | fun ImageView.setDrawableCompat(@DrawableRes resId: Int?) { 16 | resId?.let { 17 | setImageDrawable(context.getDrawableCompat(it)) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/repository/RecordingRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.repository 2 | 3 | import ir.mrahimy.conceal.data.Recording 4 | import ir.mrahimy.conceal.db.dao.RecordingDao 5 | 6 | class RecordingRepositoryImpl(private val recordingDao: RecordingDao) : RecordingRepository { 7 | override suspend fun addRecording(recording: Recording) { 8 | recordingDao.upsertRecording(recording) 9 | } 10 | 11 | override suspend fun deleteRecording(recording: Recording) { 12 | recordingDao.deleteRecording(recording) 13 | } 14 | 15 | override fun getAllRecordings() = recordingDao.getRecordings() 16 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/db/dao/RecordingDao.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.db.dao 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.* 5 | import ir.mrahimy.conceal.data.Recording 6 | 7 | @Dao 8 | interface RecordingDao { 9 | @Query("SELECT * FROM recording") 10 | fun getRecordings(): LiveData> 11 | 12 | 13 | @Insert(onConflict = OnConflictStrategy.REPLACE) 14 | suspend fun upsertRecording(item: Recording) 15 | 16 | @Insert(onConflict = OnConflictStrategy.REPLACE) 17 | suspend fun upsertRecordings(items: List) 18 | 19 | @Delete 20 | suspend fun deleteRecording(item: Recording) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/Exclude.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net 2 | 3 | import com.google.gson.ExclusionStrategy 4 | import com.google.gson.FieldAttributes 5 | 6 | /** 7 | * Excluding a field from gson serialization 8 | */ 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @Target(AnnotationTarget.FIELD) 11 | annotation class Exclude 12 | 13 | object AnnotationExclusionStrategy : ExclusionStrategy { 14 | 15 | override fun shouldSkipField(f: FieldAttributes): Boolean { 16 | return f.getAnnotation(Exclude::class.java) != null 17 | } 18 | 19 | override fun shouldSkipClass(clazz: Class<*>): Boolean { 20 | return false 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/repository/InfoRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.repository 2 | 3 | import ir.mrahimy.conceal.net.ApiResult 4 | import ir.mrahimy.conceal.net.api.InfoApi 5 | import ir.mrahimy.conceal.net.safeApiCall 6 | 7 | class InfoRepositoryImpl(val api: InfoApi) : InfoRepository { 8 | override suspend fun putImageInfo(params: Map): ApiResult = safeApiCall { 9 | return@safeApiCall ApiResult.Success(api.putImageInfo(params)) 10 | } 11 | 12 | override suspend fun putAudioInfo(params: Map): ApiResult = safeApiCall { 13 | return@safeApiCall ApiResult.Success(api.putAudioInfo(params)) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.di 2 | 3 | import ir.mrahimy.conceal.repository.InfoRepository 4 | import ir.mrahimy.conceal.repository.InfoRepositoryImpl 5 | import ir.mrahimy.conceal.repository.RecordingRepository 6 | import ir.mrahimy.conceal.repository.RecordingRepositoryImpl 7 | import ir.mrahimy.conceal.repository.SampleRepository 8 | import ir.mrahimy.conceal.repository.SampleRepositoryImpl 9 | import org.koin.dsl.module 10 | 11 | val repositoryModule = module { 12 | factory { RecordingRepositoryImpl(get()) } 13 | factory { InfoRepositoryImpl(get()) } 14 | factory { SampleRepositoryImpl() } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_stroke.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util 2 | 3 | /* 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | 7 | fun rescaleImage(path: String, width: Int, height: Int): Bitmap { 8 | 9 | val scaleOptions = BitmapFactory.Options() 10 | scaleOptions.inJustDecodeBounds = true 11 | BitmapFactory.decodeFile(path, scaleOptions) 12 | var scale = 1 13 | while (scaleOptions.outWidth / scale / 2 >= width && scaleOptions.outHeight / scale / 2 >= height) { 14 | scale *= 2 15 | } 16 | 17 | val outOptions = BitmapFactory.Options() 18 | outOptions.inSampleSize = scale 19 | return BitmapFactory.decodeFile(path, outOptions) 20 | } 21 | */ -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/capsules/SaveBitmapInfoCapsule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.capsules 2 | 3 | import android.graphics.Bitmap 4 | import ir.mrahimy.conceal.util.ktx.toValidPath 5 | import ir.mrahimy.conceal.util.writeBitmap 6 | import java.io.File 7 | import java.util.* 8 | 9 | data class SaveBitmapInfoCapsule( 10 | val name: String?, 11 | val time: Date?, 12 | val bitmap: Bitmap, 13 | val format: Bitmap.CompressFormat 14 | ) 15 | 16 | fun SaveBitmapInfoCapsule.save(path: String): String { 17 | val filePath = path.toValidPath() + "${name}_${time?.time}." + 18 | format.name.toLowerCase(Locale.ENGLISH) 19 | File(filePath).writeBitmap(bitmap, format, 100) 20 | return filePath 21 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/mrahimy/conceal/LoopTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal 2 | 3 | import org.junit.Test 4 | 5 | class LoopTest { 6 | 7 | @Test 8 | fun `test forEach continue`() { 9 | listOf(1, 2, 3, 4, 5).forEach{ 10 | if (it == 3) return@forEach // local return to the caller of the lambda, i.e. the forEach loop 11 | print(it) 12 | } 13 | print(" done with implicit label") 14 | } 15 | 16 | @Test 17 | fun `test forEach break`() { 18 | each() 19 | print(" done with explicit label") 20 | } 21 | 22 | private fun each(){ 23 | listOf(1, 2, 3, 4, 5).forEach{ 24 | if (it == 3) return 25 | print(it) 26 | } 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/di/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.di 2 | 3 | import ir.mrahimy.conceal.ui.home.MainActivityViewModel 4 | import ir.mrahimy.conceal.ui.parse.ParseActivityViewModel 5 | import ir.mrahimy.conceal.ui.sample.SampleViewModel 6 | import ir.mrahimy.conceal.ui.slide.SlideShowViewModel 7 | import org.koin.android.ext.koin.androidApplication 8 | import org.koin.androidx.viewmodel.dsl.viewModel 9 | import org.koin.dsl.module 10 | 11 | val viewModelModule = module { 12 | viewModel { SampleViewModel(get()) } 13 | viewModel { MainActivityViewModel(androidApplication(), get(), get()) } 14 | viewModel { ParseActivityViewModel(androidApplication(), get(), get()) } 15 | viewModel { SlideShowViewModel(androidApplication()) } 16 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/mrahimy/conceal/SubstringUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | class SubstringUnitTest { 8 | 9 | @Test 10 | fun `check exclusive substring index 1`() { 11 | val string = "1234" 12 | val sub12 = string.substring(0, 2) 13 | assert(sub12 == "12") 14 | } 15 | 16 | @Test 17 | fun `check exclusive substring index 2`() { 18 | val string = "1234" 19 | val sub12 = string.substring(2, 4) 20 | assert(sub12 == "34") 21 | } 22 | 23 | @Test 24 | fun `check inclusive substring by length`() { 25 | val string = "01234567" 26 | val sub2End = string.drop(6) 27 | assert(sub2End == "67") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/di/DbModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.di 2 | 3 | import androidx.room.Room 4 | import ir.mrahimy.conceal.db.ConcealDb 5 | import ir.mrahimy.conceal.db.migrations.migration1to2 6 | import org.koin.android.ext.koin.androidContext 7 | import org.koin.dsl.module 8 | 9 | const val DB_NAME = "conceal_db" 10 | 11 | val dbModule = module { 12 | single { 13 | Room.databaseBuilder( 14 | androidContext(), 15 | ConcealDb::class.java, 16 | DB_NAME 17 | ) 18 | .fallbackToDestructiveMigration() 19 | .addMigrations( 20 | migration1to2 21 | ) 22 | .build() 23 | } 24 | 25 | factory { 26 | get().recordingDao() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/androidTest/java/ir/mrahimy/conceal/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("ir.mrahimy.conceal", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/capsules/ConcealInputData.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data.capsules 2 | 3 | import android.graphics.Bitmap 4 | import ir.mrahimy.conceal.data.Rgb 5 | import kotlinx.coroutines.Job 6 | 7 | data class ConcealInputData( 8 | val rgbList: List, 9 | val position: Int, 10 | val audioDataAsRgbList: IntArray, 11 | val refImage: Bitmap, 12 | val job: Job 13 | ) { 14 | override fun equals(other: Any?): Boolean { 15 | if (this === other) return true 16 | if (javaClass != other?.javaClass) return false 17 | 18 | other as ConcealInputData 19 | 20 | if (!audioDataAsRgbList.contentEquals(other.audioDataAsRgbList)) return false 21 | 22 | return true 23 | } 24 | 25 | override fun hashCode(): Int { 26 | return audioDataAsRgbList.contentHashCode() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/SeparatedDigits.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | 3 | data class SeparatedDigits( 4 | val elementCount: Int, 5 | val digits: IntArray 6 | ) { 7 | override fun equals(other: Any?): Boolean { 8 | if (this === other) return true 9 | if (javaClass != other?.javaClass) return false 10 | 11 | other as SeparatedDigits 12 | 13 | if (!digits.contentEquals(other.digits)) return false 14 | 15 | return true 16 | } 17 | 18 | override fun hashCode(): Int { 19 | return digits.contentHashCode() 20 | } 21 | } 22 | 23 | fun String.toSeparatedDigits(): SeparatedDigits { 24 | val count = this.length 25 | val digits = mutableListOf() 26 | this.iterator().forEach { 27 | digits.add(it.toInt()) 28 | } 29 | 30 | return SeparatedDigits(count, digits.toIntArray()) 31 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/ui/sample/SampleAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.ui.sample 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import ir.mrahimy.conceal.R 5 | import ir.mrahimy.conceal.base.BaseAdapter 6 | import ir.mrahimy.conceal.data.Sample 7 | 8 | class SampleAdapter : BaseAdapter(DIFF_CALLBACK) { 9 | 10 | companion object { 11 | private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { 12 | override fun areContentsTheSame(oldItem: Sample, newItem: Sample): Boolean { 13 | return oldItem == newItem 14 | } 15 | 16 | override fun areItemsTheSame(oldItem: Sample, newItem: Sample): Boolean { 17 | return oldItem.id == newItem.id 18 | } 19 | } 20 | } 21 | 22 | override fun getItemViewType(position: Int): Int { 23 | return R.layout.item_sample 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/File.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util 2 | 3 | import android.graphics.Bitmap 4 | import ir.mrahimy.conceal.data.Waver 5 | import ir.mrahimy.conceal.util.lowlevel.WavUtil 6 | import ir.mrahimy.conceal.util.lowlevel.Wave 7 | import java.io.File 8 | 9 | fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int = 100) { 10 | outputStream().use { out -> 11 | bitmap.compress(format, quality, out) 12 | out.flush() 13 | } 14 | } 15 | 16 | fun File.writeWave(waver: Waver) { 17 | waver.apply { 18 | Wave.WavFile.newWavFile( 19 | this@writeWave, 20 | channelCount, 21 | frameCount, 22 | validBits, 23 | sampleRate 24 | ).writeAllFrames(this) 25 | } 26 | } 27 | 28 | fun Wave.WavFile.writeAllFrames(waver: Waver) { 29 | WavUtil.writeAllFrames(this, waver) 30 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/Util.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net 2 | 3 | import ir.mrahimy.conceal.R 4 | import ir.mrahimy.conceal.net.error.ApiException 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | 8 | /** 9 | * a func to handle network errors 10 | * Safely Calls the suspend function inside a co-routine context and returns an error if exception occurs 11 | */ 12 | suspend fun safeApiCall( 13 | call: suspend () -> ApiResult 14 | ): ApiResult { 15 | return withContext(Dispatchers.Main) { 16 | try { 17 | withContext(Dispatchers.IO) { 18 | call() 19 | } 20 | } catch (e: Exception) { 21 | val jsonError = R.string.network_error 22 | val errorCode = if (e is ApiException) e.statusCode else -1 23 | ApiResult.Error(jsonError, errorCode) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_stroke.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | 23 | -keep class ir.mrahimy.conceal.data.** { *;} 24 | -keep interface ir.mrahimy.conceal.data.** { *;} 25 | -keepnames interface ir.mrahimy.conceal.data.** { *;} -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ff8577 4 | #8f367c 5 | #D81B60 6 | #efefef 7 | @color/white 8 | #fff 9 | #001f3f 10 | #0074d9 11 | #7fdbff 12 | #39cccc 13 | #3d9970 14 | #2ecc40 15 | #01ff70 16 | #ffdc00 17 | #ff851b 18 | #ff4136 19 | #85144b 20 | #f012be 21 | #b10dc9 22 | #111111 23 | #aaaaaa 24 | #dddddd 25 | #0000 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/lowlevel/LowLevelRgbOperations.java: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.lowlevel; 2 | 3 | import ir.mrahimy.conceal.data.Rgb; 4 | 5 | public class LowLevelRgbOperations { 6 | 7 | /** 8 | * A pixel from a Bitmap.getPixel(x,y) is an integer which contains all RGB values inside. 9 | * This is a method to get the rgb values from this integer 10 | * 11 | * @param pixel a signed integer to get rgb values 12 | * @return a data class with separated RGB values 13 | */ 14 | public static Rgb getRgb(int pixel) { 15 | int r = (pixel & 0xff0000) >> 16; 16 | int g = (pixel & 0x00ff00) >> 8; 17 | int b = (pixel & 0x0000ff) >> 0; 18 | 19 | return new Rgb(r, g, b); 20 | } 21 | 22 | /** 23 | * puts RGB values inside a signed integer 24 | * 25 | * @param in the separated RGB values to be put inside the integer. 26 | * @return a signed integer 27 | */ 28 | public static int parseRgb(Rgb in) { 29 | int rgb = in.getR(); 30 | rgb = (rgb << 8) + in.getG(); 31 | rgb = (rgb << 8) + in.getB(); 32 | return rgb; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/app/ConcealApplication.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.app 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import com.yariksoffice.lingver.Lingver 6 | import ir.mrahimy.conceal.BuildConfig 7 | import ir.mrahimy.conceal.di.* 8 | import org.koin.android.ext.koin.androidContext 9 | import org.koin.android.ext.koin.androidLogger 10 | import org.koin.core.context.startKoin 11 | import org.koin.core.logger.Level 12 | import timber.log.Timber 13 | 14 | class ConcealApplication : Application() { 15 | var currentActivity: Activity? = null 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) 20 | 21 | Lingver.init(this, "en") 22 | 23 | startKoin { 24 | androidContext(this@ConcealApplication) 25 | androidLogger(Level.DEBUG) 26 | modules( 27 | adapterModule, 28 | viewModelModule, 29 | dbModule, 30 | repositoryModule, 31 | networkModule, 32 | apiModule 33 | ) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/test/java/ir/mrahimy/conceal/WaveManipulationUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal 2 | 3 | import ir.mrahimy.conceal.data.absolute 4 | import ir.mrahimy.conceal.data.mapToUniformDouble 5 | import org.junit.Test 6 | 7 | class WaveManipulationUnitTest { 8 | 9 | val data = LongArray(11) { 10 | (it * 10).toLong() 11 | } 12 | 13 | val negs = LongArray(11) { 14 | (it * -10).toLong() 15 | } 16 | 17 | @Test 18 | fun mapToUniformDouble() { 19 | val double = data.mapToUniformDouble() 20 | assert(100L == data.max()) 21 | assert(double[0] == 0.0) 22 | assert(double[1] == data[1].toDouble() / (data.max()?.toDouble() ?: 0.0)) 23 | assert(double[1] == 0.1) 24 | assert(double.max() ?: 0.0 <= 1.0) 25 | } 26 | 27 | @Test 28 | fun mapToUniformDoubleNegative() { 29 | val data = negs 30 | val double = data.mapToUniformDouble() 31 | assert(100L == data.absolute().max()) 32 | assert(double[0] == 0.0) 33 | assert(double[1] == data[1].toDouble() / (data.absolute().max()?.toDouble() ?: 0.0)) 34 | assert(double[1] == -0.1) 35 | assert(double.max() ?: 0.0 <= 1.0) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "113445267419", 4 | "firebase_url": "https://conceal-d27dd.firebaseio.com", 5 | "project_id": "conceal-d27dd", 6 | "storage_bucket": "conceal-d27dd.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:113445267419:android:71ae80b7f20f98cff129fe", 12 | "android_client_info": { 13 | "package_name": "ir.mrahimy.conceal" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "113445267419-vsdgqvgejova3qssjhcesuagaq4n6m6h.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyAeMpPM7XGLe7S63cK7v5CvL6UdT4wdu6M" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "113445267419-vsdgqvgejova3qssjhcesuagaq4n6m6h.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/Waver.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | 3 | data class Waver( 4 | val data: LongArray, 5 | val sampleRate: Long, 6 | val channelCount: Int, 7 | val frameCount: Long, 8 | val validBits: Int 9 | ) { 10 | var maxValue: Long = 1 11 | override fun equals(other: Any?): Boolean { 12 | if (this === other) return true 13 | if (javaClass != other?.javaClass) return false 14 | 15 | other as Waver 16 | 17 | if (!data.contentEquals(other.data)) return false 18 | 19 | return true 20 | } 21 | 22 | override fun hashCode(): Int { 23 | return data.contentHashCode() 24 | } 25 | } 26 | 27 | fun LongArray.mapToUniformDouble(): DoubleArray { 28 | val max = absolute().max()?.toDouble() ?: 1.0 29 | return map { 30 | (it.toDouble() / max) 31 | }.toDoubleArray() 32 | } 33 | 34 | fun LongArray.maxValue() = absolute().max() ?: 1 35 | 36 | fun DoubleArray.mapToRgbValue(): IntArray { 37 | return map { 38 | (it * 255).toInt() 39 | }.toIntArray() 40 | } 41 | 42 | fun LongArray.absolute(): LongArray { 43 | return map { 44 | if (it < 0) it * -1 else it 45 | }.toLongArray() 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/ApiInterceptor.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net 2 | 3 | import com.google.gson.Gson 4 | import ir.mrahimy.conceal.net.error.ApiException 5 | import ir.mrahimy.conceal.util.ktx.extractData 6 | import okhttp3.Interceptor 7 | import okhttp3.Response 8 | import okhttp3.ResponseBody 9 | import java.net.UnknownHostException 10 | 11 | class ApiInterceptor( 12 | private val gson: Gson 13 | ) : Interceptor { 14 | override fun intercept(chain: Interceptor.Chain): Response { 15 | val reqBuilder = chain.request().newBuilder() 16 | try { 17 | val response = chain.proceed(reqBuilder.build()) 18 | 19 | val data = response.body()?.string()?.let { 20 | gson.extractData(it) 21 | } ?: "" 22 | 23 | val contentType = response.body()?.contentType() 24 | val body = ResponseBody.create(contentType, data) 25 | 26 | return response.newBuilder().body(body).build() 27 | 28 | } catch (e: UnknownHostException) { 29 | throw e 30 | } catch (e: ApiException) { 31 | throw e 32 | } catch (e: Exception) { 33 | throw ApiException(-1, e) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/base/BaseAndroidViewModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.base 2 | 3 | import android.app.Application 4 | import androidx.annotation.AttrRes 5 | import androidx.annotation.ColorRes 6 | import androidx.annotation.DimenRes 7 | import androidx.annotation.StringRes 8 | import ir.mrahimy.conceal.app.ConcealApplication 9 | import ir.mrahimy.conceal.util.ktx.getColorCompat 10 | import ir.mrahimy.conceal.util.ktx.getColorCompatFromAttr 11 | 12 | abstract class BaseAndroidViewModel(private val application: Application) : BaseViewModel() { 13 | 14 | protected fun getApplication() = application as ConcealApplication 15 | 16 | protected fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { 17 | return getApplication().resources.getString(resId, *formatArgs) 18 | } 19 | 20 | protected fun getDimension(@DimenRes resId: Int): Float { 21 | return getApplication().resources.getDimension(resId) 22 | } 23 | 24 | protected fun getColorFromAttr(@AttrRes resId: Int): Int { 25 | return getApplication().applicationContext.getColorCompatFromAttr(resId) 26 | } 27 | 28 | protected fun getColor(@ColorRes resId: Int): Int { 29 | return getApplication().applicationContext.getColorCompat(resId) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/ui/slide/SlideShowViewModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.ui.slide 2 | 3 | import android.app.Application 4 | import android.net.Uri 5 | import androidx.core.content.FileProvider 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MutableLiveData 8 | import androidx.lifecycle.map 9 | import ir.mrahimy.conceal.base.BaseAndroidViewModel 10 | import ir.mrahimy.conceal.util.arch.Event 11 | import ir.mrahimy.conceal.util.ktx.loadBitmap 12 | import java.io.File 13 | 14 | class SlideShowViewModel(application: Application) : BaseAndroidViewModel(application) { 15 | 16 | private val _imagePath = MutableLiveData() 17 | 18 | val bitmap = _imagePath.map { 19 | it.loadBitmap() 20 | } 21 | 22 | fun setImagePath(path: String) { 23 | _imagePath.postValue(path) 24 | } 25 | 26 | private val _onShare = MutableLiveData>() 27 | val onShare: LiveData> 28 | get() = _onShare 29 | 30 | fun share() { 31 | val path = _imagePath.value ?: return 32 | val content = FileProvider.getUriForFile( 33 | getApplication(), 34 | getApplication().applicationContext.packageName + ".provider", 35 | File(path) 36 | ) ?: return 37 | _onShare.postValue(Event(content)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | # When configured, Gradle will run in incubating parallel mode. 10 | # This option should only be used with decoupled projects. More details, visit 11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 12 | # org.gradle.parallel=true 13 | # AndroidX package structure to make it clearer which packages are bundled with the 14 | # Android operating system, and which are packaged with your app's APK 15 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 16 | android.useAndroidX=true 17 | # Automatically convert third-party libraries to use AndroidX 18 | android.enableJetifier=true 19 | # Kotlin code style for this project: "official" or "obsolete": 20 | kotlin.code.style=official 21 | kapt.incremental.apt=true 22 | org.gradle.jvmargs=-Xms2g -Xmx8g -XX:MaxPermSize=2g -XX:MaxMetaspaceSize=2g -Dkotlin.daemon.jvm.options="-Xmx4g" 23 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/data/Recording.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.data 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Ignore 6 | import androidx.room.PrimaryKey 7 | import ir.mrahimy.conceal.util.ktx.removeEmulatedPath 8 | import ir.mrahimy.conceal.util.ktx.toPersianFormat 9 | 10 | @Entity 11 | data class Recording( 12 | @PrimaryKey(autoGenerate = true) 13 | @ColumnInfo(name = "id") 14 | val id: Long, 15 | @ColumnInfo(name = "inputImagePath") 16 | val inputImagePath: String?, 17 | @ColumnInfo(name = "outputImagePath") 18 | val outputImagePath: String, 19 | @ColumnInfo(name = "inputWavePath") 20 | val inputWavePath: String, 21 | /** 22 | * After recording is done, we parse data on the run 23 | * But if they have received an image, there would be no parsedWavePath 24 | */ 25 | @ColumnInfo(name = "parsedWavePath") 26 | val parsedWavePath: String?, 27 | @ColumnInfo(name = "date") 28 | val date: Long 29 | ) { 30 | @Ignore 31 | var shownImagePath: String = "" 32 | 33 | @Ignore 34 | var persianDate: String = "" 35 | } 36 | 37 | fun Recording.fill(): Recording { 38 | shownImagePath = inputImagePath?.removeEmulatedPath() ?: outputImagePath.removeEmulatedPath() 39 | persianDate = date.toPersianFormat("Y/m/d H:i:s") 40 | return this 41 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/ui/sample/SampleActivity.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.ui.sample 2 | 3 | import androidx.lifecycle.Observer 4 | import ir.mrahimy.conceal.R 5 | import ir.mrahimy.conceal.base.BaseActivity 6 | import ir.mrahimy.conceal.databinding.ActivitySampleBinding 7 | import kotlinx.android.synthetic.main.activity_sample.* 8 | import org.koin.android.ext.android.inject 9 | import org.koin.androidx.viewmodel.ext.android.viewModel 10 | import timber.log.Timber 11 | 12 | class SampleActivity : BaseActivity() { 13 | 14 | override val layoutRes = R.layout.activity_sample 15 | override val viewModel: SampleViewModel by viewModel() 16 | private val adapter : SampleAdapter by inject() 17 | 18 | override fun configCreationEvents() { 19 | sample_list.adapter = adapter 20 | } 21 | 22 | override fun configResumeEvents() { 23 | //TODO("not implemented") 24 | } 25 | 26 | override fun bindObservables() { 27 | viewModel.sampleList.observe(this, Observer { 28 | it.forEach { 29 | Timber.d(it.text) 30 | } 31 | }) 32 | } 33 | 34 | override fun initBinding() { 35 | binding.apply { 36 | lifecycleOwner = this@SampleActivity 37 | vm = viewModel 38 | executePendingBindings() 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/req/AudioInfo.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net.req 2 | 3 | import ir.mrahimy.conceal.data.Waver 4 | import java.io.File 5 | 6 | const val DATE_KEY = "date" 7 | const val NAME_KEY = "name" 8 | const val EXT_KEY = "ext" 9 | const val SIZE_KEY = "size" 10 | const val IS_PARSED_KEY = "is_parsed" 11 | 12 | private const val SAMPLE_RATE_KEY = "sample_rate" 13 | private const val VALID_BITS_KEY = "valid_bits" 14 | private const val CHANNEL_COUNT_KEY = "channel_count" 15 | private const val FRAME_COUNT_KEY = "frame_count" 16 | 17 | fun Waver.makeAudioInfoMap( 18 | isParsed: Boolean, 19 | file: File 20 | ): Map { 21 | val name = file.name 22 | val ext = file.extension 23 | val size = file.length().toString() 24 | val date = file.lastModified() 25 | val sampleRate = sampleRate 26 | val validBits = validBits 27 | val channelCount = channelCount 28 | val frameCount = frameCount 29 | 30 | val map = mutableMapOf() 31 | map[NAME_KEY] = name 32 | map[EXT_KEY] = ext 33 | map[SIZE_KEY] = size 34 | map[DATE_KEY] = date.toString() 35 | 36 | map[IS_PARSED_KEY] = isParsed.toString() 37 | map[SAMPLE_RATE_KEY] = sampleRate.toString() 38 | map[VALID_BITS_KEY] = validBits.toString() 39 | map[CHANNEL_COUNT_KEY] = channelCount.toString() 40 | map[FRAME_COUNT_KEY] = frameCount.toString() 41 | 42 | return map 43 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/arch/Event.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.arch 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | open class Event(private val content: T) { 6 | private var isConsumed = false 7 | 8 | fun consume(): T? { 9 | return if (isConsumed) null else { 10 | isConsumed = true 11 | content 12 | } 13 | } 14 | 15 | fun peek(): T = content 16 | 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (javaClass != other?.javaClass) return false 20 | 21 | other as Event<*> 22 | 23 | if (content != other.content) return false 24 | if (isConsumed != other.isConsumed) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var result = content?.hashCode() ?: 0 31 | result = 31 * result + isConsumed.hashCode() 32 | return result 33 | } 34 | } 35 | 36 | class StatelessEvent : Event(0) 37 | 38 | /** 39 | * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has 40 | * already been consumed. 41 | * 42 | * [onContentUnconsumed] is only called if the [Event]'s contents has not been consumed. 43 | */ 44 | class EventObsrver(private val onContentUnconsumed: (T) -> Unit) : Observer> { 45 | override fun onChanged(event: Event?) { 46 | event?.consume()?.run(onContentUnconsumed) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/net/req/ImageInfo.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.net.req 2 | 3 | import android.graphics.Bitmap 4 | import java.io.File 5 | 6 | private const val WIDTH_KEY = "width" 7 | private const val HEIGHT_KEY = "height" 8 | 9 | internal fun makeImageInfoMap( 10 | name: String? = null, 11 | ext: String? = null, 12 | size: String? = null, 13 | w: Int? = 0, 14 | h: Int? = 0, 15 | date: Long? = 0L 16 | ): Map { 17 | val map = mutableMapOf() 18 | name?.let { map.put(NAME_KEY, name) } 19 | ext?.let { map.put(EXT_KEY, ext) } 20 | size?.let { map.put(SIZE_KEY, size) } 21 | date?.let { map.put(DATE_KEY, date.toString()) } 22 | w?.let { map.put(WIDTH_KEY, w.toString()) } 23 | h?.let { map.put(HEIGHT_KEY, h.toString()) } 24 | return map 25 | } 26 | 27 | fun Bitmap.makeImageInfoMap( 28 | isParsed: Boolean, 29 | file: File 30 | ): Map { 31 | val name = file.name 32 | val ext = file.extension 33 | val size = file.length().toString() 34 | val date = file.lastModified() 35 | val width = width 36 | val height = height 37 | 38 | val map = mutableMapOf() 39 | map[NAME_KEY] = name 40 | map[EXT_KEY] = ext 41 | map[SIZE_KEY] = size 42 | map[DATE_KEY] = date.toString() 43 | 44 | map[IS_PARSED_KEY] = isParsed.toString() 45 | map[WIDTH_KEY] = width.toString() 46 | map[HEIGHT_KEY] = height.toString() 47 | return map 48 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conceal 2 | Concealing WAVE audio files inside images 3 | 4 | ## Description 5 | This application is useful for hiding wave audio data inside the least significant bits of an image. 6 | The resulting image can be shared and received images can be parsed by this app. 7 | 8 | ## Sharing Note 9 | Some social media applications like Telegram and Instagram change the content of images and re-compress them before sending. Sharing the resulting image to those applications would probably remove audio data that is concealed inside the image. We suggest sending the resulting image on Telegram as un-compressed file instead of photo. 10 | 11 | When no solution is available, you can upload your image to an image hosting website for sharing. Sending them as email attachment is known to keep the original data. Removing any meta-data from the image does not break the conceal/reveal process. 12 | 13 | ## Screenshots 14 | l | l | l 15 | :-------------------------:|:-------------------------:|:-------------------------: 16 | ![start](http://conceal.ir/shots/01.png "start") | ![chosen input image](http://conceal.ir/shots/02.png "chosen input image") |![recording list](http://conceal.ir/shots/03.png "recording list") 17 | ![progress](http://conceal.ir/shots/04.png "progress") | ![progress near finished](http://conceal.ir/shots/05.png "progress near finished") | ![reveal](http://conceal.ir/shots/06.png "reveal page") 18 | 19 | ## Release 20 | Available in fdroid: https://f-droid.org/en/packages/ir.mrahimy.conceal/ 21 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx 2 | 3 | import android.graphics.Bitmap 4 | import ir.mrahimy.conceal.data.Rgb 5 | import ir.mrahimy.conceal.data.Waver 6 | import ir.mrahimy.conceal.util.* 7 | 8 | fun Bitmap.getRgb(x: Int, y: Int): Rgb = this.getPixel(x, y).toRgb() 9 | 10 | fun Bitmap.getRgbArray(): List { 11 | val rgb = mutableListOf() 12 | (0 until height).forEach { y -> 13 | (0 until width).forEach { x -> 14 | rgb.add(getRgb(x, y)) 15 | } 16 | } 17 | return rgb 18 | } 19 | 20 | fun Bitmap.parseWaver(): Waver { 21 | val list = getRgbArray() 22 | val parsedSampleRate = list.getSampleRate() 23 | val parsedChannelCount = list.getChannelCount(parsedSampleRate.position) 24 | val parsedFrameCount = list.getFrameCount(parsedChannelCount.position) 25 | val parsedValidBits = list.getValidBits(parsedFrameCount.position) 26 | val parsedMaxValue = list.getMaxValue(parsedValidBits.position) 27 | 28 | val parsedWaveData = 29 | list.getAllSignedIntegers(parsedMaxValue.position) 30 | .map { n -> n.toDouble() / 255.0 } 31 | .map { n -> n * parsedMaxValue.number } 32 | .map { n -> n.toLong() } 33 | .toLongArray() 34 | 35 | return Waver( 36 | parsedWaveData, 37 | parsedSampleRate.number.toLong(), 38 | parsedChannelCount.number, 39 | parsedFrameCount.number.toLong(), 40 | parsedValidBits.number 41 | ) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/LowLevelInt.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx 2 | 3 | import ir.mrahimy.conceal.data.Rgb 4 | import ir.mrahimy.conceal.util.lowlevel.LowLevelIntOperations 5 | import ir.mrahimy.conceal.util.lowlevel.LowLevelRgbOperations 6 | 7 | fun Int.toRgb():Rgb = LowLevelRgbOperations.getRgb(this) 8 | fun Rgb.parse() = LowLevelRgbOperations.parseRgb(this) 9 | 10 | //TODO: decide by howMany here 11 | fun Int.removeLsBits(howMany: Int) = LowLevelIntOperations.removeLsBits(this) 12 | 13 | fun Int.getLsBits(howMany: Int) = when (howMany) { 14 | 3 -> LowLevelIntOperations.get3LsBits(this) 15 | 2 -> LowLevelIntOperations.get2LsBits(this) 16 | else -> throw RuntimeException("Not implemented YET") 17 | } 18 | 19 | /** 20 | * This method does not respect the sign of concealed number 21 | */ 22 | fun Int.combineBits(vararg others: Int): Int { 23 | val binA = this.toBinString() 24 | val lsbA = binA.drop(6) 25 | val builder = StringBuilder().apply { append(lsbA) } 26 | others.forEach { 27 | val binB = it.toBinString() 28 | val lsbB = binB.drop(6) 29 | builder.append(lsbB) 30 | } 31 | return builder.toString().toInt(2) 32 | } 33 | 34 | //fun Int.bitwiseAnd(other: Int) = LowLevelIntOperations.and(this, other) 35 | 36 | fun Int.bitwiseOr(other: Int) = LowLevelIntOperations.or(this, other) 37 | 38 | fun Int.toBinString(format: String = "%8s") = 39 | String.format(format, this.toString(2)) 40 | .replace(' ', '0') 41 | // Integer.toBinaryString(this) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_trash_stroke.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_checkmark_green.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custome_primary_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 27 | 28 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ktx/Compat.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ktx 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import android.util.TypedValue 6 | import androidx.annotation.AttrRes 7 | import androidx.annotation.ColorRes 8 | import androidx.annotation.DrawableRes 9 | import androidx.appcompat.content.res.AppCompatResources 10 | import androidx.core.content.ContextCompat 11 | 12 | fun Context.getColorCompat(@ColorRes color: Int): Int { 13 | return ContextCompat.getColor(this, color) 14 | } 15 | 16 | 17 | fun Context.getColorCompatFromAttr(@AttrRes color: Int): Int { 18 | val typedValue = TypedValue() 19 | theme.resolveAttribute(color, typedValue, true) 20 | return typedValue.data 21 | } 22 | 23 | fun Context.getDrawableCompat(@DrawableRes drawableId: Int): Drawable? { 24 | return AppCompatResources.getDrawable(this, drawableId) 25 | } 26 | 27 | /* 28 | fun Context.getDrawableCompatFromAttr(@AttrRes drawable: Int): Drawable? { 29 | val typedValue = TypedValue() 30 | theme.resolveAttribute(drawable, typedValue, true) 31 | val imageResId = typedValue.resourceId 32 | return getDrawableCompat(imageResId) 33 | } 34 | 35 | fun Context.getFontCompatFromAttr(@AttrRes font: Int): Typeface? { 36 | val typedValue = TypedValue() 37 | theme.resolveAttribute(font, typedValue, true) 38 | val fontResource = typedValue.resourceId 39 | return ResourcesCompat.getFont(this, fontResource) 40 | } 41 | 42 | 43 | fun Drawable.setTintDrawable(colors: ColorStateList) { 44 | val d = DrawableCompat.wrap(this).mutate() 45 | d.let { 46 | DrawableCompat.setTintList(it, colors) 47 | } 48 | } 49 | */ 50 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/ba/View.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.ba 2 | 3 | import android.view.Gravity 4 | import android.view.View 5 | import android.view.ViewGroup.MarginLayoutParams 6 | import androidx.annotation.StringRes 7 | import androidx.databinding.BindingAdapter 8 | import io.github.douglasjunior.androidSimpleTooltip.SimpleTooltip 9 | import ir.mrahimy.conceal.R 10 | import kotlin.math.roundToInt 11 | 12 | 13 | @BindingAdapter("app:isVisible") 14 | fun View.setIsVisible(boolean: Boolean?) { 15 | boolean?.let { 16 | visibility = if (it) View.VISIBLE 17 | else View.INVISIBLE 18 | } 19 | } 20 | 21 | @BindingAdapter("app:isGone") 22 | fun View.setIsGone(boolean: Boolean?) { 23 | boolean?.let { 24 | visibility = if (it) View.GONE 25 | else View.VISIBLE 26 | } 27 | } 28 | 29 | @BindingAdapter("tooltip") 30 | fun View.setTooltip(@StringRes tooltip: Int?) { 31 | val tooltipView = SimpleTooltip.Builder(this.context) 32 | .anchorView(this) 33 | .text(context.getString(tooltip ?: R.string.click_to_open_file)) 34 | .gravity(Gravity.TOP) 35 | .animated(true) 36 | .transparentOverlay(false) 37 | .padding(32f) 38 | .build() 39 | 40 | if (tooltip != null) tooltipView.show() 41 | else tooltipView.dismiss() 42 | 43 | } 44 | 45 | @BindingAdapter("android:layout_marginBottom") 46 | fun View.setBottomMargin(bottomMargin: Float) { 47 | val mLayoutParams = layoutParams as MarginLayoutParams 48 | mLayoutParams.setMargins( 49 | mLayoutParams.leftMargin, mLayoutParams.topMargin, 50 | mLayoutParams.rightMargin, bottomMargin.roundToInt() 51 | ) 52 | layoutParams = mLayoutParams 53 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/ui/slide/SlideShowActivity.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.ui.slide 2 | 3 | import android.content.Intent 4 | import android.text.method.ScrollingMovementMethod 5 | import ir.mrahimy.conceal.R 6 | import ir.mrahimy.conceal.base.BaseActivity 7 | import ir.mrahimy.conceal.databinding.ActivitySlideBinding 8 | import ir.mrahimy.conceal.ui.home.IMAGE_PATH_KEY 9 | import ir.mrahimy.conceal.util.arch.EventObsrver 10 | import kotlinx.android.synthetic.main.activity_slide.* 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class SlideShowActivity : BaseActivity() { 14 | override val viewModel: SlideShowViewModel by viewModel() 15 | override val layoutRes = R.layout.activity_slide 16 | 17 | override fun bindObservables() { 18 | viewModel.onShare.observe(this, EventObsrver { 19 | val shareIntent: Intent = Intent().apply { 20 | action = Intent.ACTION_SEND 21 | putExtra(Intent.EXTRA_STREAM, it) 22 | type = "image/jpeg" 23 | } 24 | startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) 25 | }) 26 | } 27 | 28 | override fun configCreationEvents() { 29 | intent?.extras?.get(IMAGE_PATH_KEY)?.let { 30 | viewModel.setImagePath(it as String) 31 | } 32 | 33 | sharing_hint?.movementMethod = ScrollingMovementMethod() 34 | } 35 | 36 | override fun configResumeEvents() { 37 | 38 | } 39 | 40 | override fun initBinding() { 41 | binding.apply { 42 | lifecycleOwner = this@SlideShowActivity 43 | vm = viewModel 44 | executePendingBindings() 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/ui/home/RecordingsAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.ui.home 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.DiffUtil 5 | import ir.mrahimy.conceal.R 6 | import ir.mrahimy.conceal.base.BaseAdapter 7 | import ir.mrahimy.conceal.data.Recording 8 | import kotlinx.android.synthetic.main.item_recording.view.* 9 | 10 | class RecordingsAdapter : BaseAdapter(DIFF_CALLBACK) { 11 | 12 | var onStop: ((item: Recording, v: View) -> Unit)? = null 13 | var onPlay: ((item: Recording, v: View) -> Unit)? = null 14 | var onDelete: ((item: Recording, v: View) -> Unit)? = null 15 | 16 | companion object { 17 | private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { 18 | override fun areContentsTheSame(oldItem: Recording, newItem: Recording): Boolean { 19 | return oldItem == newItem 20 | } 21 | 22 | override fun areItemsTheSame(oldItem: Recording, newItem: Recording): Boolean { 23 | return oldItem.id == newItem.id 24 | } 25 | } 26 | } 27 | 28 | override fun getItemViewType(position: Int): Int { 29 | return R.layout.item_recording 30 | } 31 | 32 | override fun onBindViewHolder(holder: DataBindingViewHolder, position: Int) { 33 | super.onBindViewHolder(holder, position) 34 | holder.itemView.stop?.setOnClickListener { v -> 35 | onStop?.invoke(getItem(holder.adapterPosition), v) 36 | } 37 | holder.itemView.play?.setOnClickListener { v -> 38 | onPlay?.invoke(getItem(holder.adapterPosition), v) 39 | } 40 | holder.itemView.delete?.setOnClickListener { v -> 41 | onDelete?.invoke(getItem(holder.adapterPosition), v) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .gradle/* 12 | .gradle/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | app/release/ 34 | # Android related 35 | **/android/**/gradle-wrapper.jar 36 | **/android/.gradle 37 | **/android/captures/ 38 | **/android/gradlew 39 | **/android/gradlew.bat 40 | **/android/local.properties 41 | **/android/**/GeneratedPluginRegistrant.java 42 | 43 | # iOS/XCode related 44 | **/ios/**/*.mode1v3 45 | **/ios/**/*.mode2v3 46 | **/ios/**/*.moved-aside 47 | **/ios/**/*.pbxuser 48 | **/ios/**/*.perspectivev3 49 | **/ios/**/*sync/ 50 | **/ios/**/.sconsign.dblite 51 | **/ios/**/.tags* 52 | **/ios/**/.vagrant/ 53 | **/ios/**/DerivedData/ 54 | **/ios/**/Icon? 55 | **/ios/**/Pods/ 56 | **/ios/**/.symlinks/ 57 | **/ios/**/profile 58 | **/ios/**/xcuserdata 59 | **/ios/.generated/ 60 | **/ios/Flutter/App.framework 61 | **/ios/Flutter/Flutter.framework 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.base 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.databinding.ViewDataBinding 8 | import ir.mrahimy.conceal.app.ConcealApplication 9 | import ir.mrahimy.conceal.data.enums.ChooserType 10 | 11 | abstract class BaseActivity : AppCompatActivity() { 12 | 13 | abstract val viewModel: VM 14 | 15 | abstract val layoutRes: Int 16 | 17 | val binding by lazy { 18 | DataBindingUtil.setContentView(this, layoutRes) as DB 19 | } 20 | 21 | abstract fun configCreationEvents() 22 | abstract fun configResumeEvents() 23 | abstract fun bindObservables() 24 | abstract fun initBinding() 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | initBinding() 29 | configCreationEvents() 30 | bindObservables() 31 | } 32 | 33 | override fun onResume() { 34 | super.onResume() 35 | (application as? ConcealApplication)?.currentActivity = this 36 | configResumeEvents() 37 | } 38 | 39 | protected fun createPickerIntent(type: ChooserType, title: String): Intent? { 40 | val getIntent = Intent(Intent.ACTION_GET_CONTENT) 41 | getIntent.type = type.typeString 42 | 43 | val pickIntent = Intent( 44 | Intent.ACTION_PICK, 45 | type.externalContentUri 46 | ) 47 | pickIntent.type = type.typeString 48 | 49 | val chooserIntent = Intent.createChooser(getIntent, title) 50 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(pickIntent)) 51 | return chooserIntent 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/cv/PrimaryButton.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.cv 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import androidx.constraintlayout.widget.ConstraintLayout 7 | import ir.mrahimy.conceal.R 8 | import ir.mrahimy.conceal.util.ktx.view.invisible 9 | import ir.mrahimy.conceal.util.ktx.view.visible 10 | import kotlinx.android.synthetic.main.custome_primary_button.view.* 11 | 12 | class PrimaryButton @JvmOverloads constructor( 13 | context: Context?, attributes: AttributeSet? = null, def: Int = 0 14 | ) : ConstraintLayout(context, attributes, def) { 15 | 16 | private val layout: ConstraintLayout = 17 | LayoutInflater.from(context).inflate( 18 | R.layout.custome_primary_button, 19 | this, 20 | true 21 | ) as ConstraintLayout 22 | 23 | var text: String = "" 24 | set(value) { 25 | field = value 26 | btn_title?.text = text 27 | } 28 | 29 | var isLoading: Boolean = false 30 | set(value) { 31 | field = value 32 | setState(if (value) State.Loading else State.Idle) 33 | } 34 | 35 | var isButtonEnabled = true 36 | set(value) { 37 | field = value 38 | layout.isEnabled = value 39 | btn_title?.isEnabled = value 40 | } 41 | 42 | private fun setState(state: State) { 43 | when (state) { 44 | State.Loading -> { 45 | progress_bar?.visible() 46 | btn_title?.invisible() 47 | } 48 | 49 | State.Idle -> { 50 | progress_bar?.invisible() 51 | btn_title?.visible() 52 | } 53 | } 54 | } 55 | 56 | init { 57 | // layout.background 58 | } 59 | 60 | enum class State { 61 | Loading, Idle 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/util/lowlevel/WavUtil.java: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.util.lowlevel; 2 | 3 | import java.io.IOException; 4 | 5 | import ir.mrahimy.conceal.data.Waver; 6 | 7 | public class WavUtil { 8 | public static Waver fromWaveData(Wave.WavFile file) { 9 | 10 | final int BUFFER_SIZE = (int) file.getNumFrames(); 11 | final int CHANNEL_COUNT = file.getNumChannels(); 12 | long[] buffer = new long[BUFFER_SIZE * CHANNEL_COUNT]; 13 | 14 | int framesRead = 0; 15 | int offset = 0; 16 | 17 | do { 18 | try { 19 | framesRead = file.readFrames(buffer, offset, BUFFER_SIZE); 20 | offset += framesRead; 21 | } catch (Wave.WavFileException | IOException e) { 22 | e.printStackTrace(); 23 | } 24 | } while (framesRead != 0); 25 | 26 | try { 27 | file.close(); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } 31 | 32 | return new Waver(buffer, 33 | file.getSampleRate(), 34 | file.getNumChannels(), 35 | file.getNumFrames(), 36 | file.getValidBits()); 37 | } 38 | 39 | public static void writeAllFrames(Wave.WavFile file, Waver waver) { 40 | 41 | final int BUFFER_SIZE = 1024; 42 | 43 | int framesWritten = 0; 44 | int offset = 0; 45 | 46 | do { 47 | try { 48 | framesWritten = file.writeFrames(waver.getData(), offset, BUFFER_SIZE); 49 | offset += framesWritten; 50 | } catch (Wave.WavFileException | IOException e) { 51 | e.printStackTrace(); 52 | } 53 | } while (framesWritten != 0); 54 | 55 | try { 56 | file.close(); 57 | } catch (IOException e) { 58 | e.printStackTrace(); 59 | } 60 | 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.di 2 | 3 | import com.google.gson.GsonBuilder 4 | import ir.mrahimy.conceal.net.AnnotationExclusionStrategy 5 | import ir.mrahimy.conceal.net.ApiInterceptor 6 | import ir.mrahimy.conceal.net.BaseUrl 7 | import okhttp3.Interceptor 8 | import okhttp3.OkHttpClient 9 | import okhttp3.logging.HttpLoggingInterceptor 10 | import org.koin.core.qualifier.Qualifier 11 | import org.koin.dsl.module 12 | import retrofit2.Retrofit 13 | import retrofit2.converter.gson.GsonConverterFactory 14 | import timber.log.Timber 15 | 16 | object LogInterceptorQ : Qualifier 17 | object ApiInterceptorQ : Qualifier 18 | object RetrofitServiceQ : Qualifier 19 | object OkHttpServiceQ : Qualifier 20 | 21 | val networkModule = module { 22 | factory(LogInterceptorQ) { 23 | HttpLoggingInterceptor { log -> 24 | Timber.d(log) 25 | }.apply { 26 | level = HttpLoggingInterceptor.Level.BODY 27 | } 28 | } 29 | 30 | factory { 31 | GsonBuilder() 32 | .setExclusionStrategies(AnnotationExclusionStrategy) 33 | .disableHtmlEscaping() 34 | //.registerTypeAdapter( 35 | //TransactionHistoryRes::class.java, 36 | //TransactionHistoryDeserializer() 37 | //) 38 | .create() 39 | } 40 | 41 | factory(ApiInterceptorQ) { 42 | ApiInterceptor(get()) 43 | } 44 | 45 | single(OkHttpServiceQ) { 46 | OkHttpClient.Builder().apply { 47 | addInterceptor(get(ApiInterceptorQ)) 48 | addInterceptor(get(LogInterceptorQ)) 49 | }.build() 50 | } 51 | 52 | single(RetrofitServiceQ) { 53 | Retrofit.Builder() 54 | .baseUrl(BaseUrl.BASE_URL) 55 | .client(get(OkHttpServiceQ)) 56 | .addConverterFactory(GsonConverterFactory.create(get())) 57 | .build() 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 42 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/ir/mrahimy/conceal/ui/sample/SampleViewModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mrahimy.conceal.ui.sample 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import ir.mrahimy.conceal.base.BaseViewModel 7 | import ir.mrahimy.conceal.data.Sample 8 | import ir.mrahimy.conceal.repository.SampleRepository 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.launch 11 | 12 | class SampleViewModel(private val sampleRepository: SampleRepository) : BaseViewModel() { 13 | 14 | val sampleList = MutableLiveData>() 15 | 16 | private val _isLoading = MutableLiveData(false) 17 | val isLoading: LiveData 18 | get() = _isLoading 19 | 20 | init { 21 | viewModelScope.launch { 22 | _isLoading.postValue(true) 23 | delay(100) 24 | sampleList.postValue(sampleRepository.getSampleInitList()) 25 | _isLoading.postValue(false) 26 | } 27 | } 28 | 29 | fun addRandomSample() = viewModelScope.launch { 30 | _isLoading.postValue(true) 31 | delay(50) 32 | val samples = sampleList.value?.toMutableList() ?: mutableListOf() 33 | sampleList.postValue(samples.apply { add(sampleRepository.getRandomSample(samples.size)) }) 34 | _isLoading.postValue(false) 35 | } 36 | 37 | fun addRandomSamples() = viewModelScope.launch { 38 | _isLoading.postValue(true) 39 | repeat(10) { 40 | delay(100) 41 | addRandomSample() 42 | } 43 | 44 | _isLoading.postValue(false) 45 | } 46 | 47 | fun clearSamples() = viewModelScope.launch { 48 | _isLoading.postValue(true) 49 | val list = sampleList.value?.toMutableList() ?: return@launch 50 | val iter = list.iterator() 51 | while (iter.hasNext()) { 52 | iter.apply { 53 | next() 54 | remove() 55 | } 56 | sampleList.postValue(list) 57 | delay(50) 58 | } 59 | 60 | _isLoading.postValue(false) 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 24 | 25 |