├── 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 |  |  |
17 |  |  | 
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 |
34 |
35 |
44 |
45 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/util/WaveFileErrorCodeMapper.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.util
2 |
3 | import androidx.annotation.StringRes
4 | import ir.mrahimy.conceal.R
5 | import ir.mrahimy.conceal.util.lowlevel.Wave.WavFile.*
6 |
7 | @StringRes
8 | fun Int.mapToErrorStringRes(): Int {
9 | return when (this) {
10 | ILLEGAL_NUMBER_OF_CHANNELS -> R.string.illegal_number_of_channels
11 | NUMBER_OF_FRAMES_MUST_BE_POSITIVE -> R.string.number_of_frames_must_be_positive
12 | ILLEGAL_NUMBER_OF_VALID_BITS -> R.string.illegal_number_of_valid_bits
13 | SAMPLE_RATE_MUST_BE_POSITIVE -> R.string.sample_rate_must_be_positive
14 | NOT_ENOUGH_WAV_FILE_BYTES_FOR_HEADER -> R.string.not_enough_wav_file_bytes_for_header
15 | INVALID_WAV_HEADER_DATA_INCORRECT_RIFF_CHUNK_ID -> R.string.invalid_wav_header_data_incorrect_riff_chunk_id
16 | INVALID_WAV_HEADER_DATA_INCORRECT_RIFF_TYPE_ID -> R.string.invalid_wav_header_data_incorrect_riff_type_id
17 | HEADER_CHUNK_SIZE_DOES_NOT_MATCH_FILE_SIZE_ -> R.string.header_chunk_size_does_not_match_file_size_
18 | REACHED_END_OF_FILE_WITHOUT_FINDING_FORMAT_CHUNK -> R.string.reached_end_of_file_without_finding_format_chunk
19 | COULD_NOT_READ_CHUNK_HEADER -> R.string.could_not_read_chunk_header
20 | COMPRESSION_CODE_NOT_SUPPORTED -> R.string.compression_code_not_supported
21 | NUMBER_OF_CHANNELS_SPECIFIED_IN_HEADER_IS_EQUAL_TO_ZERO -> R.string.number_of_channels_specified_in_header_is_equal_to_zero
22 | BLOCK_ALIGN_SPECIFIED_IN_HEADER_IS_EQUAL_TO_ZERO -> R.string.block_align_specified_in_header_is_equal_to_zero
23 | VALID_BITS_SPECIFIED_IN_HEADER_IS_LESS_THAN_2 -> R.string.valid_bits_specified_in_header_is_less_than_2
24 | VALID_BITS_SPECIFIED_IN_HEADER_IS_GREATER_THAN_64 -> R.string.valid_bits_specified_in_header_is_greater_than_64
25 | BLOCK_ALIGN_DOES_NOT_AGREE_WITH_BYTES_REQUIRED_FOR_VALIDBITS_AND_NUMBER_OF_CHANNELS -> R.string.block_align_does_not_agree_with_bytes_required_for_validbits_and_number_of_channels
26 | DATA_CHUNK_FOUND_BEFORE_FORMAT_CHUNK -> R.string.data_chunk_found_before_format_chunk
27 | DATA_CHUNK_SIZE_IS_NOT_MULTIPLE_OF_BLOCK_ALIGN -> R.string.data_chunk_size_is_not_multiple_of_block_align
28 | DID_NOT_FIND_A_DATA_CHUNK -> R.string.did_not_find_a_data_chunk
29 | NOT_ENOUGH_DATA_AVAILABLE -> R.string.not_enough_data_available
30 | else -> R.string.empty
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/util/cv/CameraCorner.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.util.cv
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.util.AttributeSet
8 | import android.view.View
9 | import ir.mrahimy.conceal.R
10 |
11 | /**
12 | * Simple view that shows a frame with transparent bg
13 | *
14 | * @property mLineColor: providing the color in xml, defaults to black
15 | * @property borderWidth: defines the line width to be drawn
16 | * @property lineLength: the length of lines defaults to 50
17 | */
18 | class CameraCorner(context: Context, attrs: AttributeSet) : View(context, attrs) {
19 |
20 | private var borderWidth = 4.0f
21 | private var lineLength = 50f
22 | private var mLineColor = Color.WHITE
23 |
24 | private val linesPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
25 | style = Paint.Style.STROKE
26 | color = mLineColor
27 | strokeWidth = borderWidth
28 | }
29 |
30 | init {
31 | init(context, attrs)
32 | }
33 |
34 | private fun init(context: Context, attrs: AttributeSet) {
35 | val typedArray =
36 | context.obtainStyledAttributes(attrs, R.styleable.CameraCorner, 0, 0)
37 |
38 | typedArray.apply {
39 | mLineColor = getColor(R.styleable.CameraCorner_lineColor, mLineColor)
40 | }
41 | linesPaint.color = mLineColor
42 | typedArray.recycle()
43 | }
44 |
45 | override fun draw(canvas: Canvas?) {
46 | super.draw(canvas)
47 | drawCameraCorner(canvas)
48 | }
49 |
50 | private fun drawCameraCorner(canvas: Canvas?) {
51 |
52 | val startX = paddingStart.toFloat()
53 | val startY = paddingTop.toFloat()
54 | val endX = width - paddingEnd.toFloat()
55 | val endY = height - paddingBottom.toFloat()
56 |
57 | canvas?.drawLine(startX, startY, startX + lineLength, startY, linesPaint)
58 | canvas?.drawLine(startX, startY, startX, startY + lineLength, linesPaint)
59 |
60 | canvas?.drawLine(startX, endY, startX + lineLength, endY, linesPaint)
61 | canvas?.drawLine(startX, endY, startX, endY - lineLength, linesPaint)
62 |
63 | canvas?.drawLine(endX, startY, endX - lineLength, startY, linesPaint)
64 | canvas?.drawLine(endX, startY, endX, startY + lineLength, linesPaint)
65 |
66 | canvas?.drawLine(endX, endY, endX - lineLength, endY, linesPaint)
67 | canvas?.drawLine(endX, endY, endX, endY - lineLength, linesPaint)
68 |
69 | }
70 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/test/java/ir/mrahimy/conceal/IntUnitTest.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal
2 |
3 | import ir.mrahimy.conceal.util.lowlevel.LowLevelIntOperations
4 | import ir.mrahimy.conceal.util.ktx.bitwiseOr
5 | import ir.mrahimy.conceal.util.ktx.getLsBits
6 | import ir.mrahimy.conceal.util.ktx.toBinString
7 | import org.junit.Test
8 |
9 | class IntUnitTest {
10 |
11 | @Test
12 | fun `check combine 2 lsb`() {
13 | val a = 3
14 | val b = 2
15 | val binA = a.toBinString()
16 | val binB = b.toBinString()
17 |
18 | assert(binA == "00000011")
19 | assert(binB == "00000010")
20 |
21 | val lsbA = binA.drop(6)
22 | val lsbB = binB.drop(6)
23 |
24 | assert(lsbA == "11")
25 | assert(lsbB == "10")
26 |
27 | val whole = lsbA + lsbB
28 |
29 | assert(whole == "1110")
30 |
31 | val int = whole.toInt(2)
32 |
33 | assert(int == 14)
34 | }
35 |
36 | @Test
37 | fun `check bits back with combine`() {
38 | val number = 6
39 | val carrierIn1 = 192 // 1100 0000
40 | val carrierIn2 = 208 // 1101 0000
41 |
42 | val numberBinString = number.toBinString(format = "%4s") // 0110
43 | assert(numberBinString == "0110")
44 |
45 | val binaryString2BitsChunk1 = numberBinString.substring(0, 2).toInt(2) // 01
46 | assert(binaryString2BitsChunk1 == 1)
47 | val binaryString2BitsChunk2 = numberBinString.substring(2, 4).toInt(2) // 10
48 | assert(binaryString2BitsChunk2 == 2)
49 |
50 | val carrierOut1 = carrierIn1.bitwiseOr(binaryString2BitsChunk1)
51 | assert(carrierOut1 == 193)
52 | val carrierOut2 = carrierIn2.bitwiseOr(binaryString2BitsChunk2)
53 | assert(carrierOut2 == 210)
54 |
55 | val binA = carrierOut1.toBinString()
56 | val binB = carrierOut2.toBinString()
57 |
58 | assert(binA == "11000001")
59 | assert(binB == "11010010")
60 |
61 | val lsbA = binA.drop(6)
62 | val lsbB = binB.drop(6)
63 |
64 | assert(lsbA == "01")
65 | assert(lsbB == "10")
66 |
67 | val whole = lsbA + lsbB
68 |
69 | assert(whole == "0110")
70 |
71 | val int = whole.toInt(2)
72 |
73 | assert(int == 6)
74 | }
75 |
76 | @Test
77 | fun `test getting 2 lsb`() {
78 | val number = 15
79 | val _2lsb = LowLevelIntOperations.get2LsBits(number)
80 | assert(_2lsb == 3)
81 | }
82 |
83 | @Test
84 | fun `test getting 3 lsb`() {
85 | val number = 15
86 | val _3lsb = LowLevelIntOperations.get3LsBits(number)
87 | assert(_3lsb == 7)
88 | }
89 |
90 | @Test
91 | fun `test getting lsb`() {
92 | assert(7 == 15.getLsBits(3))
93 | assert(3 == 15.getLsBits(2))
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/util/cv/VisualizerView.java:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.util.cv;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.graphics.Color;
6 | import android.graphics.Paint;
7 | import android.util.AttributeSet;
8 | import android.view.View;
9 |
10 | import java.util.Random;
11 |
12 | public class VisualizerView extends View {
13 | // private static final int MAX_AMPLITUDE = 32767;
14 |
15 | private final int mPoints = 66;
16 | private int mRadius;
17 | private int mPointRadius;
18 | protected final Paint mPaint;
19 | private final Paint mGPaint;
20 | private float[] mSrcY;
21 | private final Random random = new Random();
22 | int[] thresholds = new int[mPoints];
23 |
24 | public VisualizerView(Context context, AttributeSet attrs) {
25 | super(context, attrs);
26 | mPaint = new Paint();
27 | mPaint.setColor(Color.parseColor("#2196F3"));
28 | mPaint.setStrokeWidth(2);
29 | mPaint.setStyle(Paint.Style.FILL);
30 |
31 | mGPaint = new Paint();
32 | mGPaint.setAntiAlias(true);
33 |
34 | mSrcY = new float[mPoints];
35 | for (int i = 0; i < mPoints; i++) {
36 | thresholds[i] = random.nextInt(66) + 1;
37 | }
38 | }
39 |
40 | @Override
41 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
42 | mRadius = Math.min(w, h) / 8;
43 | mPointRadius = Math.abs((int) (2 * mRadius * Math.sin(Math.PI / mPoints / 3)));
44 | }
45 |
46 | /**
47 | * modifies draw arrays. cycles back to zero when amplitude samples reach max screen size
48 | */
49 | public void addAmplitude(int amplitude) {
50 | invalidate();
51 | float amp = amplitude / 10f;
52 | mSrcY = new float[mPoints];
53 | for (int i = 1; i <= mPoints; i++) {
54 | mSrcY[i - 1] = amp / (random.nextInt(thresholds[i - 1]) + 1);
55 | }
56 | }
57 |
58 | @Override
59 | public void onDraw(Canvas canvas) {
60 | for (int i = 0; i < 360; i = i + 360 / mPoints) {
61 | float cx = (float) (getWidth() / 2 + Math.cos(i * Math.PI / 180) * mRadius);
62 | float cy = (float) (getHeight() / 2 - Math.sin(i * Math.PI / 180) * mRadius);
63 | canvas.drawCircle(cx, cy, mPointRadius, mPaint);
64 | }
65 |
66 | for (int i = 0; i < 360; i = i + 360 / mPoints) {
67 | float value = mSrcY[i * mPoints / 360];
68 | if (value == 0) continue;
69 | if (value > 222) value = 222;
70 | canvas.save();
71 | float width = getWidth() / (float) 2;
72 | float height = getHeight()/ (float) 2;
73 | canvas.rotate(-90, width, height);
74 | canvas.rotate(-i, width, height);
75 | float cx = (float) (width + mRadius);
76 | if (value > 100) mGPaint.setColor(Color.RED);
77 | else mGPaint.setColor(Color.GREEN);
78 | canvas.drawCircle(cx + value, width, mPointRadius, mGPaint);
79 | canvas.drawRect(cx, width - mPointRadius, cx + value,
80 | width + mPointRadius, mPaint);
81 | canvas.restore();
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/base/BaseAdapter.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.base
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import androidx.lifecycle.Lifecycle
9 | import androidx.lifecycle.LifecycleOwner
10 | import androidx.lifecycle.LifecycleRegistry
11 | import androidx.recyclerview.widget.DiffUtil
12 | import androidx.recyclerview.widget.ListAdapter
13 | import androidx.recyclerview.widget.RecyclerView
14 | import ir.mrahimy.conceal.BR
15 |
16 | abstract class BaseAdapter(diff: DiffUtil.ItemCallback) :
17 | ListAdapter.DataBindingViewHolder>(diff) {
18 |
19 | /**
20 | * these functions should be assigned inside configEvents() of the activity/fragment
21 | */
22 | var onItemClicked: ((item: T, view: View) -> Unit)? = null
23 | var onItemLongClicked: ((item: T, view: View) -> Boolean)? = null
24 |
25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder {
26 | return DataBindingViewHolder(
27 | DataBindingUtil.inflate(
28 | LayoutInflater.from(parent.context),
29 | viewType,
30 | parent,
31 | false
32 | )
33 | )
34 | }
35 |
36 | override fun submitList(list: MutableList?) {
37 | super.submitList(if (list != null) ArrayList(list).toMutableList() else null)
38 | }
39 |
40 | override fun onBindViewHolder(holder: DataBindingViewHolder, position: Int) =
41 | holder.bind(getItem(position))
42 |
43 | abstract override fun getItemViewType(position: Int): Int
44 |
45 | override fun onViewAttachedToWindow(holder: DataBindingViewHolder) {
46 | super.onViewAttachedToWindow(holder)
47 | holder.onAppear()
48 | }
49 |
50 | override fun onViewDetachedFromWindow(holder: DataBindingViewHolder) {
51 | super.onViewDetachedFromWindow(holder)
52 | holder.onDisappear()
53 | }
54 |
55 | inner class DataBindingViewHolder(
56 | private val binding: ViewDataBinding
57 | ) : RecyclerView.ViewHolder(binding.root), LifecycleOwner {
58 |
59 | private val lifecycleRegistry = LifecycleRegistry(this)
60 |
61 | init {
62 | lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
63 | }
64 |
65 | fun onAppear() {
66 | lifecycleRegistry.currentState = Lifecycle.State.CREATED
67 | lifecycleRegistry.currentState = Lifecycle.State.STARTED
68 | }
69 |
70 | fun onDisappear() {
71 | lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
72 | }
73 |
74 | override fun getLifecycle(): Lifecycle {
75 | return lifecycleRegistry
76 | }
77 |
78 | fun bind(item: T) {
79 | binding.apply {
80 | lifecycleOwner = this@DataBindingViewHolder
81 | setVariable(BR.item, item)
82 | executePendingBindings()
83 | root.apply {
84 | setOnClickListener {
85 | onItemClicked?.invoke(item, this)
86 | }
87 |
88 | setOnLongClickListener {
89 | return@setOnLongClickListener onItemLongClicked?.invoke(item, this)
90 | ?: return@setOnLongClickListener true
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/util/arch/CombineLiveData.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.util.arch
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MediatorLiveData
5 |
6 | inline fun combine(
7 | source1: LiveData,
8 | source2: LiveData,
9 | crossinline combine: (data1: T1?, data2: T2?) -> R
10 | ): LiveData = MediatorLiveData().apply {
11 |
12 | var data1: T1? = null
13 | var data2: T2? = null
14 |
15 | addSource(source1) {
16 | data1 = it
17 | value = combine(data1, data2)
18 | }
19 |
20 | addSource(source2) {
21 | data2 = it
22 | value = combine(data1, data2)
23 | }
24 |
25 | }
26 |
27 | inline fun combine(
28 | source1: LiveData,
29 | source2: LiveData,
30 | source3: LiveData,
31 | crossinline combine: (data1: T1?, data2: T2?, data3: T3?) -> R
32 | ): LiveData = MediatorLiveData().apply {
33 |
34 | var data1: T1? = null
35 | var data2: T2? = null
36 | var data3: T3? = null
37 |
38 | addSource(source1) {
39 | data1 = it
40 | value = combine(data1, data2, data3)
41 | }
42 |
43 | addSource(source2) {
44 | data2 = it
45 | value = combine(data1, data2, data3)
46 | }
47 |
48 | addSource(source3) {
49 | data3 = it
50 | value = combine(data1, data2, data3)
51 | }
52 |
53 | }
54 |
55 |
56 | inline fun combine(
57 | source1: LiveData,
58 | source2: LiveData,
59 | source3: LiveData,
60 | source4: LiveData,
61 | crossinline combine: (data1: T1?, data2: T2?, data3: T3?, data4: T4?) -> R
62 | ): LiveData = MediatorLiveData().apply {
63 |
64 | var data1: T1? = null
65 | var data2: T2? = null
66 | var data3: T3? = null
67 | var data4: T4? = null
68 |
69 | addSource(source1) {
70 | data1 = it
71 | value = combine(data1, data2, data3, data4)
72 | }
73 |
74 | addSource(source2) {
75 | data2 = it
76 | value = combine(data1, data2, data3, data4)
77 | }
78 |
79 | addSource(source3) {
80 | data3 = it
81 | value = combine(data1, data2, data3, data4)
82 | }
83 |
84 | addSource(source4) {
85 | data4 = it
86 | value = combine(data1, data2, data3, data4)
87 | }
88 |
89 | }
90 |
91 |
92 | inline fun combine(
93 | source1: LiveData,
94 | source2: LiveData,
95 | source3: LiveData,
96 | source4: LiveData,
97 | source5: LiveData,
98 | crossinline combine: (data1: T1?, data2: T2?, data3: T3?, data4: T4?, data5: T5?) -> R
99 | ): LiveData = MediatorLiveData().apply {
100 |
101 | var data1: T1? = null
102 | var data2: T2? = null
103 | var data3: T3? = null
104 | var data4: T4? = null
105 | var data5: T5? = null
106 |
107 | addSource(source1) {
108 | data1 = it
109 | value = combine(data1, data2, data3, data4, data5)
110 | }
111 |
112 | addSource(source2) {
113 | data2 = it
114 | value = combine(data1, data2, data3, data4, data5)
115 | }
116 |
117 | addSource(source3) {
118 | data3 = it
119 | value = combine(data1, data2, data3, data4, data5)
120 | }
121 |
122 | addSource(source4) {
123 | data4 = it
124 | value = combine(data1, data2, data3, data4, data5)
125 | }
126 | addSource(source5) {
127 | data5 = it
128 | value = combine(data1, data2, data3, data4, data5)
129 | }
130 |
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_slide.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
20 |
21 |
36 |
37 |
49 |
50 |
63 |
64 |
73 |
74 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/ir/mrahimy/conceal/LowLevelOperationsManipulationInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.graphics.Color
6 | import androidx.core.graphics.set
7 | import androidx.test.platform.app.InstrumentationRegistry
8 | import ir.mrahimy.conceal.data.Rgb
9 | import ir.mrahimy.conceal.util.remove3Lsb
10 | import ir.mrahimy.conceal.util.ktx.toRgb
11 | import ir.mrahimy.conceal.util.writeBitmap
12 | import org.junit.Before
13 | import org.junit.Test
14 | import java.io.File
15 | import java.util.*
16 |
17 |
18 | class LowLevelOperationsManipulationInstrumentedTest {
19 | private val rgbList = mutableListOf()
20 | private val removedLsb = mutableListOf()
21 |
22 | private val image_width = 8
23 | private val image_height = 5
24 |
25 | private lateinit var context: Context
26 | @Before
27 | fun t() {
28 | context = InstrumentationRegistry.getInstrumentation()
29 | .targetContext.applicationContext
30 | }
31 |
32 | @Before
33 | fun initRgbList() {
34 | rgbList.clear()
35 | rgbList.apply {
36 | add(Rgb(192, 117, 115))
37 | add(Rgb(180, 215, 216))
38 | add(Rgb(181, 25, 26))
39 | add(Rgb(81, 250, 16))
40 | add(Rgb(50, 200, 19))
41 | add(Rgb(150, 200, 190))
42 | add(Rgb(90, 51, 17))
43 | add(Rgb(190, 251, 217))
44 | add(Rgb(170, 190, 151))
45 | add(Rgb(240, 151, 117))
46 | add(Rgb(17, 90, 51))
47 | add(Rgb(100, 90, 101))
48 | add(Rgb(192, 117, 115))
49 | add(Rgb(192, 117, 115))
50 | add(Rgb(180, 215, 216))
51 | add(Rgb(181, 25, 26))
52 | add(Rgb(81, 250, 16))
53 | add(Rgb(50, 200, 19))
54 | add(Rgb(170, 190, 151))
55 | add(Rgb(150, 200, 190))
56 | add(Rgb(50, 200, 19))
57 | add(Rgb(150, 200, 190))
58 | add(Rgb(90, 51, 17))
59 | add(Rgb(190, 251, 217))
60 | add(Rgb(170, 190, 151))
61 | add(Rgb(240, 151, 117))
62 | add(Rgb(17, 90, 51))
63 | add(Rgb(100, 90, 101))
64 | add(Rgb(192, 117, 115))
65 | add(Rgb(180, 215, 216))
66 | add(Rgb(181, 25, 26))
67 | add(Rgb(81, 250, 16))
68 | add(Rgb(50, 200, 19))
69 | add(Rgb(150, 200, 190))
70 | add(Rgb(90, 51, 17))
71 | add(Rgb(190, 251, 217))
72 | add(Rgb(170, 190, 151))
73 | add(Rgb(240, 151, 117))
74 | add(Rgb(17, 90, 51))
75 | add(Rgb(100, 90, 101))
76 | }//40
77 |
78 | removedLsb.clear()
79 | removedLsb.addAll(rgbList.remove3Lsb())
80 | }
81 |
82 | @Test
83 | fun convertToRgbAndInitBack() {
84 | val pixel = -4013374
85 | val rgb = pixel.toRgb()
86 | /**
87 | android.graphics.Color must be used in instrumented test
88 | */
89 | val fromRgb = Color.rgb(
90 | rgb.r,
91 | rgb.g,
92 | rgb.b
93 | )
94 | assert(pixel == fromRgb)
95 | }
96 |
97 | @Test
98 | fun saveBitmap() {
99 |
100 | val bitmap = Bitmap.createBitmap(image_width, image_height, Bitmap.Config.ARGB_8888)
101 |
102 | var x = 0
103 | var y = 0
104 | repeat(rgbList.size) { l ->
105 | val rgb = rgbList[l]
106 | bitmap[x, y] = Color.rgb(rgb.r, rgb.g, rgb.b)
107 | x++
108 | if (x >= image_width) {
109 | y++
110 | x = 0
111 | }
112 | }
113 |
114 | val date = Date()
115 | File(context.externalCacheDir?.absolutePath + "/0test_img_${date.time}.png")
116 | .writeBitmap(bitmap, Bitmap.CompressFormat.PNG, 100)
117 |
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Conceal
3 | 选择输入图片
4 | 打开一个 wave 文件或录些什么
5 | 点按选择输入图片
6 | 选择图片
7 | 选择音频文件
8 | 录制一段音频
9 | 选择图片
10 | 选择音频文件
11 | 点按打开文件
12 | 将音频隐藏在图片内
13 | ٪%1$3.0f
14 | 百分比
15 | 输入图片
16 | 输出图片
17 | 选择
18 | 一个过程正在进行中,请确保它先被取消。
19 | 取消
20 | 录音列表为空
21 |
22 | 输入图片
23 | 输出图片
24 | 选择输入图片
25 | 选择输入音频
26 | 从输入到输出
27 | 开始录音
28 | 绿色检查
29 | 保存中
30 | 音频数据超过图像尺寸
31 | 数据在索引 %1$d 上超出
32 | 数据在索引 %1$d 后无法被解析
33 | 非法通道数
34 | 帧数必须为正
35 |
36 | 非法的有效比特数
37 | 采样率必须为正
38 | wav 文件标头字节数不足
39 | 无效的 wav 头数据,不正确的资源交换档案标准(riff)区块 id
40 | 无效的 wav 头数据,不正确的 riff 类型 id
41 | wav 文件头区块大小与文件大小不匹配
42 | 到达文件结尾而没有找到格式区块
43 | 无法读取区块头
44 | 压缩代码不受支持
45 | 文件头中指定的通道数等于零
46 | 文件头中指定的块对齐等于零
47 | 文件头中指定的有效比特小于 2
48 | 文件头中指定的有效比特大于 64
49 | 块对齐和有效比特与通道数量所需字节不一致
50 | 在格式取块之前找到了数据区块
51 | 数据区块大小不是块对齐的倍数
52 | 未找到一个数据区块
53 | 没有足够的数据
54 | wave 文件输出路径
55 | 选择载体图片
56 | wave 文件输出路径
57 | 数据大小不匹配
58 | 未应用解析过程
59 | 有一个未保存的过程。请再按一次返回以退出。
60 | 网络错误
61 | 分享
62 | 发送到
63 | 解析图片时出错
64 | 请注意:一些社交媒体应用程序,如 Telegram 和 Instagram 会改变图片的内容,并在发送前重新压缩它们。将生成的图像共享给这些应用程序可能会删除隐藏在图像中的音频数据。我们建议将结果图像作为未压缩文件在 Telegram 上发送,而不是照片。\n\n没有解决方案时,你可以将你的图像上传到图像托管网站共享。另外,将它们作为电子邮件附件发送会保留原始数据。从图像中删除任何元数据并不会破坏隐藏过程。
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/util/ktx/MediaStore.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.util.ktx
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import ir.mrahimy.conceal.util.ktx.FileUtils.getRealPath
6 |
7 | /*
8 | fun Uri.getPath(context: Context): String? {
9 | when {
10 | DocumentsContract.isDocumentUri(context, this) -> // ExternalStorageProvider
11 | when {
12 | isExternalStorageDocument(this) -> {
13 | val docId = DocumentsContract.getDocumentId(this)
14 | val split =
15 | docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
16 | val type = split[0]
17 |
18 | if ("primary".equals(type, ignoreCase = true)) {
19 | return context.getExternalFilesDir(null)?.absolutePath + "/" + split[1]
20 | }
21 | }
22 | isDownloadsDocument(this) -> {// DownloadsProvider
23 | val id = DocumentsContract.getDocumentId(this)
24 | if (id.startsWith("raw:"))
25 | return id.replaceFirst("raw:", "")
26 | val contentUri = ContentUris.withAppendedId(
27 | Uri.parse("content://downloads/public_downloads"),
28 | java.lang.Long.valueOf(id)
29 | )
30 | return getDataColumn(context, contentUri, null, null)
31 |
32 | }
33 | isMediaDocument(this) -> { // MediaProvider
34 | val docId = DocumentsContract.getDocumentId(this)
35 | val split =
36 | docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
37 | val type = split[0]
38 | var contentUri: Uri? = null
39 | when (type) {
40 | "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
41 | "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
42 | "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
43 | }
44 | val selection = "_id=?"
45 | val selectionArgs = arrayOf(split[1])
46 | return getDataColumn(context, contentUri, selection, selectionArgs)
47 |
48 | }
49 | }
50 | "content".equals(scheme, ignoreCase = true) -> // MediaStore (and general)
51 | // Return the remote address
52 | return if (isGooglePhotosUri(this)) lastPathSegment else getDataColumn(
53 | context,
54 | this,
55 | null,
56 | null
57 | )
58 | "file".equals(scheme, ignoreCase = true) -> // File
59 | return path
60 | }
61 | return null
62 | }
63 | */
64 | /*
65 | fun getDataColumn(
66 | context: Context,
67 | inUri: Uri?,
68 | selection: String?,
69 | selectionArgs: Array?
70 | ): String? {
71 | var cursor: Cursor? = null
72 | val column = "_data"
73 | val projection = arrayOf(column)
74 | inUri?.let { uri ->
75 | try {
76 | cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
77 | cursor?.let { cursor ->
78 | if (cursor.moveToFirst()) {
79 | val index = cursor.getColumnIndexOrThrow(column)
80 | return cursor.getString(index)
81 | }
82 | }
83 | } finally {
84 | cursor?.close()
85 | }
86 | }
87 |
88 | return null
89 | }
90 |
91 | fun isExternalStorageDocument(uri: Uri): Boolean {
92 | return "com.android.externalstorage.documents" == uri.authority
93 | } */
94 |
95 | /**
96 | * @param uri The Uri to check.
97 | * @return Whether the Uri authority is DownloadsProvider.
98 |
99 | fun isDownloadsDocument(uri: Uri): Boolean {
100 | return "com.android.providers.downloads.documents" == uri.authority
101 | } */
102 |
103 | /**
104 | * @param uri The Uri to check.
105 | * @return Whether the Uri authority is MediaProvider.
106 |
107 | fun isMediaDocument(uri: Uri): Boolean {
108 | return "com.android.providers.media.documents" == uri.authority
109 | } */
110 |
111 | /**
112 | * @param uri The Uri to check.
113 | * @return Whether the Uri authority is Google Photos.
114 |
115 | fun isGooglePhotosUri(uri: Uri): Boolean {
116 | return "com.google.android.apps.photos.content" == uri.authority
117 | } */
118 |
119 | fun Uri.getPathJava(context: Context): String = getRealPath(context, this)
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_recording.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
17 |
18 |
28 |
29 |
38 |
39 |
51 |
52 |
64 |
65 |
77 |
78 |
89 |
90 |
91 |
92 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fa/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Conceal
3 | تصویر خروجی
4 | ابتدا فایل صوتی باز کنید یا صدایی ضبط کنید
5 | انتخاب تصویر ورودی
6 | Select Image
7 | Select Audio
8 | یا صدایی ضبط کنید
9 | ابتدا عکسی انتخاب کنید
10 | میتوانید فایلی از گوشی انتخاب کنید
11 | باز کردن فایل از گوشی
12 | صدا رو داخل تصویر مخفی کن
13 | ٪%1$3.0f
14 | درصد
15 | تصویر ورودی
16 | تصویر نهایی
17 | انتخاب کنید
18 | عمل پردازشی در حال انجام است، ابتدا از لغو شدن آن مطمئن شوید.
19 | لغو
20 | لیست نهاننگاریهای قبلی خالی است
21 |
22 | تصویر ورودی
23 | تصویر خروجی
24 | انتخاب تصویر ورودی
25 | انتخاب صوت ورودی
26 | از یک به دو
27 | شروع ضبط صدا
28 | تیک سبز
29 | در حال ذخیره صدا
30 | این حجم از صوت داخل تصویر جا نمیشود.
31 | دادههای فایل صوتی از %1$d درصد به بعد در تصویر جا نشد.
32 | دادههای فایل صوتی از %1$d درصد به بعد خوانده نشد
33 | تعداد کانالها بین ۱ تا ۶۵۵۳۶ نیست.
34 | تعداد فریمها باید یک عدد مثبت باشد.
35 |
36 | تعداد بیتهای بااعتبار باید بین ۲ تا ۶۵۵۳۶ باشد.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | نوع فشردهسازی فایل صوتی ورودی در حال حاضر پشتیبانی نمیشود.
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | تعداد دادههای داخل فایل صوتی با اندازهٔ فایل برابر نیست.
54 | مسیر صوت بازیابی شده
55 | عکسی را انتخاب کنید که در آن دادههای مخفی وجود دارد .
56 | مسیر فایل صوتی بازیابی شده
57 | حجم تصویر برای بازیابی صوت مناسب نیست
58 | عملیات بازیابی صدا انجام نشده است
59 | یک پردازش انجام شده اما ذخیره نشدهاست. برای حذف دوباره دکمه بازگشت را بزنید.
60 | network error
61 | بهاشتراکگذاری
62 | ارسال به
63 | "در استخراج صدا از تصویر مشکلی پیش آمد. "
64 | نکته: برخی شبکههای اجتماعی مثل تلگرام و اینستاگرام محتوای عکس رو تغییر میدن و بعد ارسال میکنن. بهاشتراکگذاری عکس نهایی در این شبکهها باعث میشه محتوای فایل صوتی که در داخل عکس گذاشتید از بین بره. توصیه اول اینه که در تلگرام به صورت فایل بفرستید نه تصویر. \n جایی که هیچ راه حلی وجود نداره میتونید توی سایتهای به اشترکگذاری تصویر آپلود کنید و سپس لینکش رو بفرستید. تا الآن هیچ گزارشی مبنی بر تغییر اطلاعات تصویر در زمان پیوستکردنش به ایمیل نداشتیم. هر گونه تغییر در متا-دادههای تصویر و هرجایی جز لایههای رنگی (آرجیبی) مشکلی در بازیابی دادههای صوتی مخفی شده در آن ندارد.
65 |
66 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 |
6 | android {
7 | compileSdkVersion 29
8 | buildToolsVersion "29.0.2"
9 | defaultConfig {
10 | applicationId "ir.mrahimy.conceal"
11 | minSdkVersion 21
12 | targetSdkVersion 29
13 | versionCode 5
14 | versionName "1.4"
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled true
20 | shrinkResources true
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | dataBinding {
32 | enabled = true
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation fileTree(dir: 'libs', include: ['*.jar'])
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
39 | implementation 'androidx.appcompat:appcompat:1.1.0'
40 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
41 | implementation 'androidx.core:core-ktx:1.2.0'
42 | implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
43 | implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"
44 | kapt "com.android.databinding:compiler:$gradlePluginVersion"
45 | implementation "com.jakewharton.timber:timber:$timberVersion"
46 |
47 | // Room
48 | implementation "androidx.room:room-runtime:$roomVersion"
49 | kapt "androidx.room:room-compiler:$roomVersion"
50 | implementation "androidx.room:room-ktx:$roomVersion"
51 |
52 | //UI
53 | implementation "com.github.ybq:Android-SpinKit:$spinKitVersion"
54 | implementation "com.github.douglasjunior:android-simple-tooltip:$simpleTooltipVersion"
55 | implementation "com.google.android.material:material:$googleMaterialVersion"
56 | implementation "com.cleveroad:audiovisualization:$audiovisualizationVersion"
57 | implementation "com.gauravk.audiovisualizer:audiovisualizer:$audiovisualizerVersion"
58 |
59 | // Network
60 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
61 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
62 | implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion"
63 | implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
64 |
65 | //Test
66 | testImplementation 'junit:junit:4.12'
67 | testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
68 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3'
69 | androidTestImplementation "androidx.test:rules:$testRunnerVersion"
70 | androidTestImplementation "androidx.test.ext:junit:$testRunnerVersion"
71 | androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0"
72 | androidTestImplementation "androidx.test:runner:$testRunnerVersion"
73 | androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', {
74 | exclude group: 'com.android.support', module: 'support-annotations'
75 | })
76 | androidTestImplementation('com.android.support.test.espresso:espresso-contrib:2.0') {
77 | exclude group: 'com.android.support', module: 'appcompat'
78 | exclude group: 'com.android.support', module: 'support-v4'
79 | exclude module: 'recyclerview-v7'
80 | }
81 | androidTestImplementation "com.android.support.test:rules:1.0.2"
82 | androidTestImplementation "com.android.support.test:runner:1.0.2"
83 |
84 | // Koin
85 | implementation "org.koin:koin-android:$koinVersion"
86 | implementation "org.koin:koin-androidx-scope:$koinVersion"
87 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion"
88 |
89 | // Coroutine
90 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
91 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
92 |
93 | // LifeCycles
94 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
95 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
96 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
97 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
98 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
99 |
100 | //Permissions
101 | implementation "org.permissionsdispatcher:permissionsdispatcher:${dispatcherVersion}"
102 | kapt "org.permissionsdispatcher:permissionsdispatcher-processor:${dispatcherVersion}"
103 |
104 | //Audio
105 | implementation 'com.github.squti:Android-Wave-Recorder:1.3.0'
106 |
107 | //Localization
108 | implementation "com.github.YarikSOffice:lingver:$lingverVersion"
109 |
110 | //Time
111 | implementation "com.github.samanzamani.persiandate:PersianDate:$persianDateVersion"
112 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/ui/parse/ParseActivity.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.ui.parse
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.media.MediaPlayer
7 | import android.net.Uri
8 | import androidx.core.net.toUri
9 | import com.cleveroad.audiovisualization.AudioVisualization
10 | import com.cleveroad.audiovisualization.DbmHandler
11 | import com.google.android.material.snackbar.Snackbar
12 | import ir.mrahimy.conceal.R
13 | import ir.mrahimy.conceal.base.BaseActivity
14 | import ir.mrahimy.conceal.data.MediaState
15 | import ir.mrahimy.conceal.databinding.ActivityParseBinding
16 | import ir.mrahimy.conceal.data.enums.ChooserType
17 | import ir.mrahimy.conceal.util.arch.EventObsrver
18 | import kotlinx.android.synthetic.main.activity_main.visualizer_view
19 | import kotlinx.android.synthetic.main.activity_parse.*
20 | import org.koin.androidx.viewmodel.ext.android.viewModel
21 | import permissions.dispatcher.NeedsPermission
22 | import permissions.dispatcher.RuntimePermissions
23 |
24 | private const val PICK_IMAGE = 1000
25 |
26 | @RuntimePermissions
27 | class ParseActivity : BaseActivity() {
28 |
29 | override val layoutRes = R.layout.activity_parse
30 | override val viewModel: ParseActivityViewModel by viewModel()
31 |
32 | private var audioVisualization: AudioVisualization? = null
33 |
34 | private var mediaPlayer: MediaPlayer? = null
35 |
36 | override fun bindObservables() {
37 | viewModel.onChooseImage.observe(this, EventObsrver {
38 | chooseMediaWithPermissionCheck(
39 | ChooserType.Image,
40 | getString(R.string.select_image_title),
41 | PICK_IMAGE
42 | )
43 | })
44 |
45 | viewModel.snackMessage.observe(this, EventObsrver {
46 | Snackbar.make(root_view, it, Snackbar.LENGTH_LONG).show()
47 | })
48 |
49 | viewModel.onStopPlaying.observe(this, EventObsrver {
50 | stopPlaying()
51 | })
52 |
53 | viewModel.onPlayOutputAudio.observe(this, EventObsrver {
54 | if (it == "stop") stopPlaying()
55 | else play(it.toUri())
56 | })
57 |
58 | viewModel.onDoneInserting.observe(this, EventObsrver {
59 | finish()
60 | })
61 | }
62 |
63 | private fun stopPlaying() {
64 | if (mediaPlayer != null) {
65 | mediaPlayer?.stop()
66 | mediaPlayer?.release()
67 | mediaPlayer = null
68 | }
69 | viewModel.onMediaStateChanged(MediaState.STOP)
70 | }
71 |
72 | override fun initBinding() {
73 | binding.apply {
74 | lifecycleOwner = this@ParseActivity
75 | vm = viewModel
76 | executePendingBindings()
77 | }
78 | }
79 |
80 | @NeedsPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
81 | fun chooseMedia(type: ChooserType, title: String, requestCode: Int) {
82 | val chooserIntent =
83 | createPickerIntent(type, title)
84 | startActivityForResult(chooserIntent, requestCode)
85 | }
86 |
87 | override fun configCreationEvents() {
88 | initializeVisualizerEngineWithPermissionCheck()
89 | }
90 |
91 |
92 | //private fun play(rec: Recording) = rec.parsedWavePath?.toUri()?.let { uri -> play(uri) }
93 |
94 |
95 | private fun play(uri: Uri) {
96 | stopPlaying()
97 | mediaPlayer = MediaPlayer.create(this, uri)
98 | mediaPlayer?.setOnCompletionListener {
99 | viewModel.onMediaStateChanged(MediaState.STOP)
100 | }
101 | viewModel.onMediaStateChanged(MediaState.PLAY)
102 | mediaPlayer?.start()
103 | }
104 |
105 | @NeedsPermission(Manifest.permission.RECORD_AUDIO)
106 | fun initializeVisualizerEngine() {
107 | audioVisualization = visualizer_view
108 | val visualizerHandler = DbmHandler.Factory.newVisualizerHandler(this, 0)
109 | audioVisualization?.linkTo(visualizerHandler)
110 | }
111 |
112 | public override fun onResume() {
113 | super.onResume()
114 | audioVisualization?.onResume()
115 | }
116 |
117 | public override fun onPause() {
118 | audioVisualization?.onPause()
119 | super.onPause()
120 | }
121 |
122 | override fun onDestroy() {
123 | audioVisualization?.release()
124 | super.onDestroy()
125 | }
126 |
127 | override fun configResumeEvents() = Unit
128 |
129 | override fun onRequestPermissionsResult(
130 | requestCode: Int,
131 | permissions: Array,
132 | grantResults: IntArray
133 | ) {
134 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
135 | onRequestPermissionsResult(requestCode, grantResults)
136 | }
137 |
138 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
139 | super.onActivityResult(requestCode, resultCode, data)
140 |
141 | when (requestCode) {
142 | PICK_IMAGE -> {
143 | if (resultCode == Activity.RESULT_CANCELED) return
144 | viewModel.selectImageFile(data)
145 | }
146 | }
147 | }
148 |
149 | override fun onBackPressed() {
150 | viewModel.onBackPressed()
151 | }
152 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Conceal
3 | choose input image
4 | Open a wave file or record something
5 | Tap to choose input image
6 | Select Image
7 | Select Audio
8 | Record an audio
9 | Select image
10 | Select audio file
11 | Tap to open file
12 | Conceal audio inside image
13 | ٪%1$3.0f
14 | percent
15 | input image
16 | output image
17 | Choose
18 | A progress is ongoing already, please ensure it is cancelled first
19 | Cancel
20 | Recording list is empty
21 |
22 | input image
23 | output image
24 | select input image
25 | select input audio
26 | from input to output
27 | start recording
28 | green check
29 | saving
30 | Audio data exceeds image dimensions
31 | data exceeds on index %1$d
32 | data cannot be parsed after index %1$d
33 | illegal number of channels
34 | number of frames must be positive
35 |
36 | illegal number of valid bits
37 | sample rate must be positive
38 | not enough wav file bytes for header
39 | invalid wav header data incorrect riff chunk id
40 | invalid wav header data incorrect riff type id
41 | header chunk size does not match file size
42 | reached end of file without finding format chunk
43 | could not read chunk header
44 | compression code not supported
45 | number of channels specified in header is equal to zero
46 | block align specified in header is equal to zero
47 | valid bits specified in header is less than 2
48 | valid bits specified in header is greater than 64
49 | block align does not agree with bytes required for validbits and number of channels
50 | data chunk found before format chunk
51 | data chunk size is not multiple of block align
52 | did not find a data chunk
53 | not enough data available
54 | output wave path
55 | select carrier image
56 | output wave path
57 | data size does not match
58 | No parsing progress applied
59 | There is an unsaved progress. Please press back again to exit.
60 | network error
61 | share
62 | send to
63 | error in parsing image
64 | Please note: 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.\n\nWhen 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 concealing process.
65 |
66 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/ui/home/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.ui.home
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.media.MediaPlayer
7 | import android.net.Uri
8 | import android.view.View
9 | import androidx.core.net.toUri
10 | import androidx.lifecycle.Observer
11 | import com.cleveroad.audiovisualization.AudioVisualization
12 | import com.cleveroad.audiovisualization.DbmHandler
13 | import com.google.android.material.snackbar.Snackbar
14 | import ir.mrahimy.conceal.R
15 | import ir.mrahimy.conceal.base.BaseActivity
16 | import ir.mrahimy.conceal.data.MediaState
17 | import ir.mrahimy.conceal.data.Recording
18 | import ir.mrahimy.conceal.databinding.ActivityMainBinding
19 | import ir.mrahimy.conceal.data.enums.ChooserType
20 | import ir.mrahimy.conceal.ui.parse.ParseActivity
21 | import ir.mrahimy.conceal.ui.slide.SlideShowActivity
22 | import ir.mrahimy.conceal.util.arch.EventObsrver
23 | import ir.mrahimy.conceal.util.putAllSignedIntegers
24 | import kotlinx.android.synthetic.main.activity_main.*
25 | import org.koin.android.ext.android.inject
26 | import org.koin.androidx.viewmodel.ext.android.viewModel
27 | import permissions.dispatcher.NeedsPermission
28 | import permissions.dispatcher.RuntimePermissions
29 |
30 | const val PICK_IMAGE = 1000
31 | const val PICK_AUDIO = 2000
32 | const val IMAGE_PATH_KEY = "path"
33 |
34 | @RuntimePermissions
35 | class MainActivity : BaseActivity() {
36 |
37 | override val layoutRes = R.layout.activity_main
38 | override val viewModel: MainActivityViewModel by viewModel()
39 |
40 | private val adapter: RecordingsAdapter by inject()
41 |
42 | private var audioVisualization: AudioVisualization? = null
43 |
44 | private var mediaPlayer: MediaPlayer? = null
45 |
46 | override fun bindObservables() {
47 | viewModel.onStartRecording.observe(this, EventObsrver {
48 | startRecordingWithPermissionCheck()
49 | })
50 |
51 | viewModel.onChooseImage.observe(this, EventObsrver {
52 | chooseMediaWithPermissionCheck(
53 | ChooserType.Image,
54 | getString(R.string.select_image_title),
55 | PICK_IMAGE
56 | )
57 | })
58 |
59 | viewModel.onChooseAudio.observe(this, EventObsrver {
60 | chooseMediaWithPermissionCheck(
61 | ChooserType.Audio,
62 | getString(R.string.select_audio_title),
63 | PICK_AUDIO
64 | )
65 | })
66 |
67 | viewModel.onStartRgbListPutAll.observe(this,
68 | EventObsrver { input ->
69 | input.apply {
70 | rgbList.putAllSignedIntegers(position, audioDataAsRgbList, refImage, job)
71 | .observe(this@MainActivity, Observer {
72 | viewModel.onUpdateInserting(it)
73 | })
74 | }
75 | })
76 |
77 | viewModel.snackMessage.observe(this, EventObsrver {
78 | Snackbar.make(recordings_list, it, Snackbar.LENGTH_LONG).show()
79 | })
80 |
81 | viewModel.onAddingMaxAmplitude.observe(this, EventObsrver {
82 | recording_visualizer_view?.addAmplitude(it)
83 | })
84 |
85 | viewModel.onDataExceeds.observe(this, EventObsrver {
86 | Snackbar.make(recordings_list, R.string.data_exceeds, Snackbar.LENGTH_LONG).show()
87 | })
88 |
89 | viewModel.onStopPlaying.observe(this, EventObsrver {
90 | stopPlaying()
91 | })
92 |
93 | viewModel.onNavigateToReveal.observe(this, EventObsrver {
94 | startActivity(Intent(this, ParseActivity::class.java))
95 | })
96 |
97 | viewModel.onStartResultActivity.observe(this, EventObsrver {
98 | startActivity(Intent(this, SlideShowActivity::class.java).apply {
99 | putExtra(IMAGE_PATH_KEY, it)
100 | })
101 | })
102 | }
103 |
104 | private fun stopPlaying() {
105 | if (mediaPlayer != null) {
106 | mediaPlayer?.stop()
107 | mediaPlayer?.release()
108 | mediaPlayer = null
109 | }
110 | viewModel.onMediaStateChanged(MediaState.STOP)
111 | }
112 |
113 | override fun initBinding() {
114 | binding.apply {
115 | lifecycleOwner = this@MainActivity
116 | vm = viewModel
117 | executePendingBindings()
118 | }
119 | }
120 |
121 | @NeedsPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
122 | fun chooseMedia(type: ChooserType, title: String, requestCode: Int) {
123 | val chooserIntent =
124 | createPickerIntent(type, title)
125 | startActivityForResult(chooserIntent, requestCode)
126 | }
127 |
128 | override fun configCreationEvents() {
129 | recordings_list?.adapter = adapter
130 |
131 | adapter.onItemClicked = { recording: Recording, _: View ->
132 | viewModel.setRecording(recording)
133 | }
134 |
135 | adapter.onDelete = { rec: Recording, _: View ->
136 | viewModel.delete(rec)
137 | }
138 |
139 | adapter.onStop = { _: Recording, _: View ->
140 | stopPlaying()
141 | }
142 |
143 | adapter.onPlay = { recording: Recording, _: View ->
144 | play(recording)
145 | }
146 |
147 | initializeVisualizerEngineWithPermissionCheck()
148 | }
149 |
150 | private fun play(rec: Recording) = rec.parsedWavePath?.toUri()?.let { uri -> play(uri) }
151 |
152 | private fun play(uri: Uri) {
153 | stopPlaying()
154 | mediaPlayer = MediaPlayer.create(this, uri)
155 | mediaPlayer?.setOnCompletionListener {
156 | viewModel.onMediaStateChanged(MediaState.STOP)
157 | }
158 | viewModel.onMediaStateChanged(MediaState.PLAY)
159 | mediaPlayer?.start()
160 | }
161 |
162 | @NeedsPermission(Manifest.permission.RECORD_AUDIO)
163 | fun initializeVisualizerEngine() {
164 | audioVisualization = visualizer_view
165 | val visualizerHandler = DbmHandler.Factory.newVisualizerHandler(this, 0)
166 | audioVisualization?.linkTo(visualizerHandler)
167 | }
168 |
169 | public override fun onResume() {
170 | super.onResume()
171 | audioVisualization?.onResume()
172 | }
173 |
174 | public override fun onPause() {
175 | audioVisualization?.onPause()
176 | super.onPause()
177 | }
178 |
179 | override fun onDestroy() {
180 | audioVisualization?.release()
181 | super.onDestroy()
182 | }
183 |
184 | override fun configResumeEvents() = Unit
185 |
186 | @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)
187 | fun startRecording() {
188 | viewModel.startRecordingWave()
189 | }
190 |
191 | override fun onRequestPermissionsResult(
192 | requestCode: Int,
193 | permissions: Array,
194 | grantResults: IntArray
195 | ) {
196 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
197 | onRequestPermissionsResult(requestCode, grantResults)
198 | }
199 |
200 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
201 | super.onActivityResult(requestCode, resultCode, data)
202 |
203 | when (requestCode) {
204 | PICK_IMAGE -> {
205 | if (resultCode == Activity.RESULT_CANCELED) return
206 | viewModel.activateConceal(true)
207 | viewModel.selectImageFile(data)
208 | }
209 |
210 | PICK_AUDIO -> {
211 | if (resultCode == Activity.RESULT_CANCELED) return
212 | viewModel.activateConceal(true)
213 | viewModel.selectAudioFile(data)
214 | }
215 | }
216 | }
217 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/mrahimy/conceal/util/ktx/FileUtils.java:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal.util.ktx;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.ContentUris;
5 | import android.content.Context;
6 | import android.database.Cursor;
7 | import android.net.Uri;
8 | import android.os.Environment;
9 | import android.provider.DocumentsContract;
10 | import android.provider.MediaStore;
11 |
12 |
13 | import java.io.File;
14 |
15 | public class FileUtils {
16 |
17 | public static String getRealPath(Context context, Uri fileUri) {
18 | String realPath;
19 | /*
20 | // SDK < API11
21 | if (Build.VERSION.SDK_INT < 11) {
22 | realPath = FileUtils.getRealPathFromURI_BelowAPI11(context, fileUri);
23 | }
24 | // SDK >= 11 && SDK < 19
25 | else if (Build.VERSION.SDK_INT>=11 && Build.VERSION.SDK_INT < 19) {
26 | realPath = FileUtils.getRealPathFromURI_API11to18(context, fileUri);
27 | }
28 | // SDK > 19 (Android 4.4) and up
29 | else {
30 | realPath = FileUtils.getRealPathFromURI_API19(context, fileUri);
31 | }
32 | */
33 | // Currently the MIN SDK is 21 in gradle file
34 | realPath = FileUtils.getRealPathFromURI_API19(context, fileUri);
35 |
36 | return realPath;
37 | }
38 |
39 | /*
40 | @SuppressLint("NewApi")
41 | public static String getRealPathFromURI_API11to18(Context context, Uri contentUri) {
42 | String[] proj = {MediaStore.Images.Media.DATA};
43 | String result = null;
44 |
45 | CursorLoader cursorLoader = new CursorLoader(context, contentUri, proj, null, null, null);
46 | Cursor cursor = cursorLoader.loadInBackground();
47 |
48 | if (cursor != null) {
49 | int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
50 | cursor.moveToFirst();
51 | result = cursor.getString(column_index);
52 | cursor.close();
53 | }
54 | return result;
55 | }
56 |
57 | public static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri) {
58 | String[] proj = {MediaStore.Images.Media.DATA};
59 | Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
60 | int column_index = 0;
61 | String result = "";
62 | if (cursor != null) {
63 | column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
64 | cursor.moveToFirst();
65 | result = cursor.getString(column_index);
66 | cursor.close();
67 | return result;
68 | }
69 | return result;
70 | }
71 | */
72 |
73 | @SuppressLint("NewApi")
74 | public static String getRealPathFromURI_API19(final Context context, final Uri uri) {
75 | //Currntly the minSdk is 21
76 | //final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
77 |
78 | // DocumentProvider
79 | if (DocumentsContract.isDocumentUri(context, uri)) {
80 | // ExternalStorageProvider
81 | if (isExternalStorageDocument(uri)) {
82 | final String docId = DocumentsContract.getDocumentId(uri);
83 | final String[] split = docId.split(":");
84 | final String type = split[0];
85 |
86 | // This is for checking Main Memory
87 | if ("primary".equalsIgnoreCase(type)) {
88 | if (split.length > 1) {
89 | return Environment.getExternalStorageDirectory() + "/" + split[1];
90 | } else {
91 | return Environment.getExternalStorageDirectory() + "/";
92 | }
93 | // This is for checking SD Card
94 | } else {
95 | return "storage" + "/" + docId.replace(":", "/");
96 | }
97 |
98 | }
99 | // DownloadsProvider
100 | else if (isDownloadsDocument(uri)) {
101 | String fileName = getFilePath(context, uri);
102 | if (fileName != null) {
103 | return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName;
104 | }
105 |
106 | String id = DocumentsContract.getDocumentId(uri);
107 | if (id.startsWith("raw:")) {
108 | id = id.replaceFirst("raw:", "");
109 | File file = new File(id);
110 | if (file.exists())
111 | return id;
112 | }
113 |
114 | final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.parseLong(id));
115 | return getDataColumn(context, contentUri, null, null);
116 | }
117 | // MediaProvider
118 | else if (isMediaDocument(uri)) {
119 | final String docId = DocumentsContract.getDocumentId(uri);
120 | final String[] split = docId.split(":");
121 | final String type = split[0];
122 |
123 | Uri contentUri = null;
124 | if ("image".equals(type)) {
125 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
126 | } else if ("video".equals(type)) {
127 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
128 | } else if ("audio".equals(type)) {
129 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
130 | }
131 |
132 | final String selection = "_id=?";
133 | final String[] selectionArgs = new String[]{
134 | split[1]
135 | };
136 |
137 | return getDataColumn(context, contentUri, selection, selectionArgs);
138 | }
139 | }
140 | // MediaStore (and general)
141 | else if ("content".equalsIgnoreCase(uri.getScheme())) {
142 |
143 | // Return the remote address
144 | if (isGooglePhotosUri(uri))
145 | return uri.getLastPathSegment();
146 |
147 | return getDataColumn(context, uri, null, null);
148 | }
149 | // File
150 | else if ("file".equalsIgnoreCase(uri.getScheme())) {
151 | return uri.getPath();
152 | }
153 |
154 | return null;
155 | }
156 |
157 | public static String getDataColumn(Context context, Uri uri, String selection,
158 | String[] selectionArgs) {
159 |
160 | Cursor cursor = null;
161 | final String column = "_data";
162 | final String[] projection = {
163 | column
164 | };
165 |
166 | try {
167 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
168 | null);
169 | if (cursor != null && cursor.moveToFirst()) {
170 | final int index = cursor.getColumnIndexOrThrow(column);
171 | return cursor.getString(index);
172 | }
173 | } finally {
174 | if (cursor != null)
175 | cursor.close();
176 | }
177 | return null;
178 | }
179 |
180 |
181 | public static String getFilePath(Context context, Uri uri) {
182 |
183 | final String[] projection = {
184 | MediaStore.MediaColumns.DISPLAY_NAME
185 | };
186 | //uses try with Resources construct to automatically close resources at the end of the code block, this avoids using finally block
187 | try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null,
188 | null)) {
189 | if (cursor != null && cursor.moveToFirst()) {
190 | final int index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME);
191 | return cursor.getString(index);
192 | }
193 | }
194 | return null;
195 | }
196 |
197 | /**
198 | * @param uri The Uri to check.
199 | * @return Whether the Uri authority is ExternalStorageProvider.
200 | */
201 | public static boolean isExternalStorageDocument(Uri uri) {
202 | return "com.android.externalstorage.documents".equals(uri.getAuthority());
203 | }
204 |
205 | /**
206 | * @param uri The Uri to check.
207 | * @return Whether the Uri authority is DownloadsProvider.
208 | */
209 | public static boolean isDownloadsDocument(Uri uri) {
210 | return "com.android.providers.downloads.documents".equals(uri.getAuthority());
211 | }
212 |
213 | /**
214 | * @param uri The Uri to check.
215 | * @return Whether the Uri authority is MediaProvider.
216 | */
217 | public static boolean isMediaDocument(Uri uri) {
218 | return "com.android.providers.media.documents".equals(uri.getAuthority());
219 | }
220 |
221 | /**
222 | * @param uri The Uri to check.
223 | * @return Whether the Uri authority is Google Photos.
224 | */
225 | public static boolean isGooglePhotosUri(Uri uri) {
226 | return "com.google.android.apps.photos.content".equals(uri.getAuthority());
227 | }
228 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_parse.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
20 |
21 |
33 |
34 |
45 |
46 |
53 |
54 |
65 |
66 |
80 |
81 |
96 |
97 |
105 |
106 |
114 |
115 |
129 |
130 |
142 |
143 |
151 |
152 |
163 |
164 |
173 |
174 |
188 |
189 |
197 |
198 |
211 |
212 |
219 |
220 |
232 |
233 |
234 |
--------------------------------------------------------------------------------
/app/src/test/java/ir/mrahimy/conceal/LowLevelOperationsManipulationUnitTest.kt:
--------------------------------------------------------------------------------
1 | package ir.mrahimy.conceal
2 |
3 | import ir.mrahimy.conceal.data.Rgb
4 | import ir.mrahimy.conceal.data.toSeparatedDigits
5 | import ir.mrahimy.conceal.util.*
6 | import ir.mrahimy.conceal.util.ktx.bitwiseOr
7 | import ir.mrahimy.conceal.util.ktx.toBinString
8 | import junit.framework.Assert.assertEquals
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | class LowLevelOperationsManipulationUnitTest {
13 |
14 | private val sampleRate = 44100
15 | private val rgbList = mutableListOf()
16 | private val removedLsb = mutableListOf()
17 |
18 | private val image_width = 8
19 | private val image_height = 5
20 |
21 | @Before
22 | fun initRgbList() {
23 | rgbList.clear()
24 | rgbList.apply {
25 | add(Rgb(192, 117, 115))
26 | add(Rgb(180, 215, 216))
27 | add(Rgb(181, 25, 26))
28 | add(Rgb(81, 250, 16))
29 | add(Rgb(50, 200, 19))
30 | add(Rgb(150, 200, 190))
31 | add(Rgb(90, 51, 17))
32 | add(Rgb(190, 251, 217))
33 | add(Rgb(170, 190, 151))
34 | add(Rgb(240, 151, 117))
35 | add(Rgb(17, 90, 51))
36 | add(Rgb(100, 90, 101))
37 | add(Rgb(192, 117, 115))
38 | add(Rgb(192, 117, 115))
39 | add(Rgb(180, 215, 216))
40 | add(Rgb(181, 25, 26))
41 | add(Rgb(81, 250, 16))
42 | add(Rgb(50, 200, 19))
43 | add(Rgb(170, 190, 151))
44 | add(Rgb(150, 200, 190))
45 | add(Rgb(50, 200, 19))
46 | add(Rgb(150, 200, 190))
47 | add(Rgb(90, 51, 17))
48 | add(Rgb(190, 251, 217))
49 | add(Rgb(170, 190, 151))
50 | add(Rgb(240, 151, 117))
51 | add(Rgb(17, 90, 51))
52 | add(Rgb(100, 90, 101))
53 | add(Rgb(192, 117, 115))
54 | add(Rgb(180, 215, 216))
55 | add(Rgb(181, 25, 26))
56 | add(Rgb(81, 250, 16))
57 | add(Rgb(50, 200, 19))
58 | add(Rgb(150, 200, 190))
59 | add(Rgb(90, 51, 17))
60 | add(Rgb(190, 251, 217))
61 | add(Rgb(170, 190, 151))
62 | add(Rgb(240, 151, 117))
63 | add(Rgb(17, 90, 51))
64 | add(Rgb(100, 90, 101))
65 | }//40
66 |
67 | removedLsb.clear()
68 | removedLsb.addAll(rgbList.remove3Lsb())
69 | }
70 |
71 | private fun `test removing 3 lsb of index`(index: Int, vararg intArray: Int) {
72 | assert(removedLsb[index].r == intArray[0]) //0
73 | assert(removedLsb[index].g == intArray[1])
74 | assert(removedLsb[index].b == intArray[2])
75 | }
76 |
77 | @Test
78 | fun `test injected sample rate position`() {
79 | val returnedPosition = removedLsb.map { it }.putSampleRate(sampleRate)
80 | val audioSampleRate = sampleRate.toString().toSeparatedDigits()
81 | assert(returnedPosition == (audioSampleRate.elementCount + 1) * 2)
82 | }
83 |
84 | @Test
85 | fun `test removing 3 lsb`() {
86 | var index = 0
87 | `test removing 3 lsb of index`(index++, 192, 112, 112) //0
88 | `test removing 3 lsb of index`(index++, 176, 208, 216) //0
89 | `test removing 3 lsb of index`(index, 176, 24, 24) //0
90 | `test removing 3 lsb of index`(8, 168, 184, 144) //8
91 | }
92 |
93 | @Test
94 | fun `test putting sampleRate after removing LSB`() {
95 | val audioSampleRate = sampleRate.toString().toSeparatedDigits()
96 | assert(audioSampleRate.elementCount == audioSampleRate.digits.size)
97 |
98 | val sampleRateElementCount = audioSampleRate.elementCount.toBinString(format = "%4s")
99 | assert(sampleRateElementCount == "0101")
100 |
101 | var position = 0
102 |
103 | var binaryString2BitsChunkStr = sampleRateElementCount.substring(0, 2)
104 | assert(binaryString2BitsChunkStr == "01")
105 |
106 | var binaryString2BitsChunk = binaryString2BitsChunkStr.toInt(2)
107 | assert(binaryString2BitsChunk == 1)
108 |
109 | var data = removedLsb[position].r.bitwiseOr(binaryString2BitsChunk)
110 | assert(data == 193)
111 | removedLsb[position].r = data
112 | position += 1
113 |
114 | binaryString2BitsChunkStr = sampleRateElementCount.substring(2, 4)
115 | assert(binaryString2BitsChunkStr == "01")
116 | binaryString2BitsChunk = binaryString2BitsChunkStr.toInt(2)
117 | assert(binaryString2BitsChunk == 1)
118 |
119 | data = removedLsb[position].r.bitwiseOr(binaryString2BitsChunk)
120 | assert(data == 177)
121 | removedLsb[position].r = data
122 | position += 1
123 |
124 | audioSampleRate.digits.forEach {
125 | val element = it.toBinString(format = "%4s")
126 | binaryString2BitsChunk = element.substring(0, 2).toInt(2)
127 | removedLsb[position].r = removedLsb[position].r.bitwiseOr(binaryString2BitsChunk)
128 | position += 1
129 |
130 | binaryString2BitsChunk = element.substring(2, 4).toInt(2)
131 | removedLsb[position].r = removedLsb[position].r.bitwiseOr(binaryString2BitsChunk)
132 | position += 1
133 | }
134 | }
135 |
136 | @Test
137 | fun `test injected sample rate`() {
138 | val injected = mutableListOf().apply {
139 | addAll(removedLsb.map { it })
140 | putSampleRate(sampleRate)
141 | }
142 |
143 | var i = 0
144 | // 5 = 0101
145 | assert(injected[i++].r == 193) //01 + 192
146 | assert(injected[i++].r == 177) // 01 + 176
147 |
148 | //4 = 0100
149 | assert(injected[i++].r == 177) // 01 + 176
150 | assert(injected[i++].r == 80) // 00 + 80
151 |
152 | //4 = 0100
153 | assert(injected[i++].r == 49) // 01 + 48
154 | assert(injected[i++].r == 144) // 00 + 144
155 |
156 | //1 = 0001
157 | assert(injected[i++].r == 88) // 00 + 88
158 | assert(injected[i++].r == 185) // 01 + 184
159 |
160 | //0 = 0000
161 | assert(injected[i++].r == 168) // 00 + 168
162 | assert(injected[i++].r == 240) // 00 + 240
163 |
164 | //0 = 0000
165 | assert(injected[i++].r == 16) // 00 + 16
166 | assert(injected[i++].r == 96) // 00 + 96
167 |
168 | assert(i == 12)
169 | }
170 |
171 | @Test
172 | fun `test retrieving sample rate`() {
173 | val injected = mutableListOf().apply {
174 | addAll(removedLsb.map { it })
175 | putSampleRate(sampleRate)
176 | }
177 |
178 | val res = injected.getSampleRate()
179 | assert(res.number == sampleRate)
180 | assert(res.position == 12)
181 | }
182 |
183 | @Test
184 | fun `test putting signed integer`() {
185 | val position = rgbList.putSignedInteger(0, 251, Layer.R)
186 | assert(position == 4)
187 | }
188 | //
189 | // @Test
190 | // fun `test putting signed integer array`() {
191 | // val array = intArrayOf(
192 | // 250, 151, -200, -60, 25, 14, 1, 36, 19, 255,
193 | // 250, 151, -200, -60, 25, 14, 1, 36, 19, 255,
194 | // 30, 39, 255
195 | // )
196 | // val full = array.size
197 | // val threshold = (image_height) * (image_width)
198 | // assert(threshold == 40)
199 | // val position = rgbList.putAllSignedIntegers(0, array, image_width, image_height)
200 | // println(position)
201 | // println(threshold)
202 | // println(position % threshold)
203 | // assert(position == (position % threshold))
204 | // }
205 |
206 | @Test
207 | fun `test getting back positive integer inside rgbList layer red`() {
208 | val rgbList = listOf(
209 | Rgb(192, 117, 115),
210 | Rgb(180, 215, 216),
211 | Rgb(181, 25, 26),
212 | Rgb(81, 250, 16),
213 | Rgb(50, 200, 19),
214 | Rgb(150, 200, 190),
215 | Rgb(90, 51, 17),
216 | Rgb(190, 251, 217),
217 | Rgb(170, 190, 151),
218 | Rgb(240, 151, 117),
219 | Rgb(17, 90, 51)
220 | )
221 | val parsed = rgbList.getSignedInteger(0, Layer.R)
222 | assert(parsed.toInt() == 5)
223 | }
224 |
225 | @Test
226 | fun `test getting back negative integer inside rgbList layer red`() {
227 | val rgbList = listOf(
228 | Rgb(196, 117, 115),
229 | Rgb(180, 215, 216),
230 | Rgb(181, 25, 26),
231 | Rgb(81, 250, 16),
232 | Rgb(50, 200, 19),
233 | Rgb(150, 200, 190),
234 | Rgb(90, 51, 17),
235 | Rgb(190, 251, 217),
236 | Rgb(170, 190, 151),
237 | Rgb(240, 151, 117),
238 | Rgb(17, 90, 51)
239 | )
240 | val parsed = rgbList.getSignedInteger(0, Layer.R)
241 | assert(parsed.toInt() == -5)
242 | }
243 |
244 | @Test
245 | fun `test getting back negative integer inside rgbList layer green`() {
246 | val rgbList = listOf(
247 | Rgb(196, 117, 115),
248 | Rgb(180, 215, 216),
249 | Rgb(181, 25, 26),
250 | Rgb(81, 250, 16),
251 | Rgb(50, 200, 19),
252 | Rgb(150, 200, 190),
253 | Rgb(90, 51, 17),
254 | Rgb(190, 251, 217),
255 | Rgb(170, 190, 151),
256 | Rgb(240, 151, 117),
257 | Rgb(17, 90, 51)
258 | )
259 | val parsed = rgbList.getSignedInteger(0, Layer.G)
260 | assert(parsed.toInt() == -118)
261 | }
262 |
263 | @Test
264 | fun `test getting back integer inside rgbList layer green middle`() {
265 | val rgbList = listOf(
266 | Rgb(196, 117, 115),
267 | Rgb(180, 215, 216),
268 | Rgb(181, 25, 26),
269 | Rgb(81, 250, 16),
270 | Rgb(50, 200, 19),
271 | Rgb(150, 200, 190),
272 | Rgb(90, 51, 17),
273 | Rgb(190, 251, 217),
274 | Rgb(170, 190, 151),
275 | Rgb(240, 151, 117),
276 | Rgb(17, 90, 51)
277 | )
278 | val int = rgbList.getSignedInteger(6, Layer.G)
279 | assert(int == 251)
280 | }
281 |
282 | @Test
283 | fun `test getting back all signed integers inside rgbList`() {
284 | val rgbList = listOf(
285 | Rgb(196, 117, 115),
286 | Rgb(180, 215, 216),
287 | Rgb(181, 25, 26),
288 | Rgb(81, 250, 16),
289 | Rgb(50, 200, 19),
290 | Rgb(150, 200, 190),
291 | Rgb(90, 51, 17),
292 | Rgb(190, 251, 217),
293 | Rgb(170, 190, 151),
294 | Rgb(240, 151, 117),
295 | Rgb(17, 90, 51)
296 | )
297 |
298 | val list = rgbList.getAllSignedIntegers(6)
299 | assert(list.containsAll(listOf(168, -118, 15, 200, 229)))
300 | }
301 |
302 | @Test
303 | fun `test putting signed in and getting it back`() {
304 | val input = -251
305 | removedLsb.putSignedInteger(0, input, Layer.R)
306 | removedLsb.putSignedInteger(4, input / 2, Layer.R)
307 | removedLsb.putSignedInteger(8, -input / 2, Layer.R)
308 | assert(removedLsb.subList(0, 4).map { it.r }.containsAll(listOf(199, 179, 178, 83)))
309 |
310 | var parsed = removedLsb.getSignedInteger(0, Layer.R)
311 | assert(parsed == input)
312 |
313 | parsed = removedLsb.getSignedInteger(4, Layer.R)
314 | assert(parsed == -125)
315 |
316 | parsed = removedLsb.getSignedInteger(8, Layer.R)
317 | assert(parsed == 125)
318 | }
319 |
320 | @Test
321 | fun `test getting back all signed integers inside rgbList 3 layers`() {
322 | val rgbList = listOf(
323 | Rgb(196, 117, 115),
324 | Rgb(180, 215, 216),
325 | Rgb(181, 25, 26),
326 | Rgb(81, 250, 16),
327 | Rgb(50, 200, 19),
328 | Rgb(150, 200, 190),
329 | Rgb(90, 51, 17),
330 | Rgb(190, 251, 217),
331 | Rgb(170, 190, 151),
332 | Rgb(240, 151, 117),
333 | Rgb(17, 90, 51)
334 | )
335 |
336 | val list = rgbList.getAllSignedIntegers(1)
337 | assert(list.containsAll(listOf(-22, -170, 15, 200, 229)))
338 | }
339 | //
340 | // @Test
341 | // fun `test putting all signed in and getting them back`() {
342 | // val input = arrayOf(210, -230, 90, -60, -80, -30, 240).toIntArray()
343 | // val put = removedLsb.putAllSignedIntegersNormal(0, input, image_width, image_height)
344 | // var get = removedLsb.parWa(0)
345 | // assert(parsed == input)
346 | //
347 | // parsed = removedLsb.getSignedInteger(4, Layer.R)
348 | // assert(parsed == -125)
349 | //
350 | // parsed = removedLsb.getSignedInteger(8, Layer.R)
351 | // assert(parsed == 125)
352 | // }
353 | }
354 |
--------------------------------------------------------------------------------