├── .fleet └── run.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── ci-gradle.properties ├── config │ └── configuration.json └── workflows │ └── ci.yml ├── .gitignore ├── .idea ├── artifacts │ └── stackzy_main_jar.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinc.xml ├── libraries-with-intellij-classes.xml ├── misc.xml ├── prettier.xml └── vcs.xml ├── LICENSE ├── META-INF └── MANIFEST.MF ├── README.md ├── build.gradle.kts ├── data ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── brut │ └── androlib │ │ └── meta │ │ └── MetaInfo.kt │ └── com │ └── theapache64 │ └── stackzy │ ├── data │ ├── local │ │ ├── .gitkeep │ │ ├── AlphabetCircle.kt │ │ ├── AnalysisReport.kt │ │ ├── AndroidApp.kt │ │ ├── AndroidDevice.kt │ │ ├── AppArgs.kt │ │ ├── GradleInfo.kt │ │ ├── LibResult.kt │ │ └── Platform.kt │ ├── remote │ │ ├── ApiInterface.kt │ │ ├── Config.kt │ │ ├── FunFact.kt │ │ ├── Library.kt │ │ ├── OptionalResult.kt │ │ ├── Result.kt │ │ └── UntrackedLibrary.kt │ ├── repo │ │ ├── AdbRepo.kt │ │ ├── ApkAnalyzerRepo.kt │ │ ├── ApkToolRepo.kt │ │ ├── AuthRepo.kt │ │ ├── ConfigRepo.kt │ │ ├── FunFactsRepo.kt │ │ ├── JadxRepo.kt │ │ ├── LibrariesRepo.kt │ │ ├── PlayStoreRepo.kt │ │ ├── ResultsRepo.kt │ │ └── UntrackedLibsRepo.kt │ └── util │ │ ├── AndroidVersionIdentifier.kt │ │ ├── CommandExecutor.kt │ │ ├── Crypto.kt │ │ ├── FileExt.kt │ │ ├── OsCheck.kt │ │ ├── ResDir.kt │ │ ├── StringUtils.kt │ │ ├── UnZip.kt │ │ └── calladapter │ │ └── flow │ │ ├── FlowResourceCallAdapter.kt │ │ ├── FlowResourceCallAdapterFactory.kt │ │ └── Resource.kt │ └── di │ ├── Qualifiers.kt │ └── module │ ├── ApkToolModule.kt │ ├── CryptoModule.kt │ ├── JadxModule.kt │ ├── NetworkModule.kt │ └── PreferenceModule.kt ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── USAGE.md └── algorithms │ ├── browse_code_algo.png │ └── result_cache_algo.png ├── extras ├── cover.jpeg ├── libs.png ├── meta.png ├── pathway.png └── select_app.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── theapache64 │ │ └── stackzy │ │ ├── App.kt │ │ ├── di │ │ └── AppComponent.kt │ │ ├── model │ │ ├── AlphabetCircle.kt │ │ ├── AnalysisReportWrapper.kt │ │ ├── AndroidAppWrapper.kt │ │ ├── AndroidDeviceWrapper.kt │ │ └── LibraryWrapper.kt │ │ ├── ui │ │ ├── common │ │ │ ├── AlphabetCircle.kt │ │ │ ├── Badge.kt │ │ │ ├── BottomGradient.kt │ │ │ ├── CenterBox.kt │ │ │ ├── CustomScaffold.kt │ │ │ ├── ErrorSnackBar.kt │ │ │ ├── FullScreenError.kt │ │ │ ├── LoadingText.kt │ │ │ ├── Logo.kt │ │ │ ├── Selectable.kt │ │ │ └── loading │ │ │ │ ├── LoadingAnimation.kt │ │ │ │ └── funfact │ │ │ │ ├── FunFact.kt │ │ │ │ └── FunFactViewModel.kt │ │ ├── feature │ │ │ ├── MainActivity.kt │ │ │ ├── appdetail │ │ │ │ ├── AppDetailScreen.kt │ │ │ │ ├── AppDetailScreenComponent.kt │ │ │ │ ├── AppDetailViewModel.kt │ │ │ │ ├── Libraries.kt │ │ │ │ └── MoreInfo.kt │ │ │ ├── applist │ │ │ │ ├── AppListScreen.kt │ │ │ │ ├── AppListScreenComponent.kt │ │ │ │ └── AppListViewModel.kt │ │ │ ├── devicelist │ │ │ │ ├── DeviceListScreen.kt │ │ │ │ ├── DeviceListScreenComponent.kt │ │ │ │ └── DeviceListViewModel.kt │ │ │ ├── libdetail │ │ │ │ ├── LibraryDetailScreen.kt │ │ │ │ ├── LibraryDetailScreenComponent.kt │ │ │ │ └── LibraryDetailViewModel.kt │ │ │ ├── liblist │ │ │ │ ├── LibraryListScreen.kt │ │ │ │ ├── LibraryListScreenComponent.kt │ │ │ │ └── LibraryListViewModel.kt │ │ │ ├── login │ │ │ │ ├── LogInScreen.kt │ │ │ │ ├── LogInScreenComponent.kt │ │ │ │ └── LogInScreenViewModel.kt │ │ │ ├── pathway │ │ │ │ ├── PathwayScreen.kt │ │ │ │ ├── PathwayScreenComponent.kt │ │ │ │ └── PathwayViewModel.kt │ │ │ ├── splash │ │ │ │ ├── SplashScreen.kt │ │ │ │ ├── SplashScreenComponent.kt │ │ │ │ └── SplashViewModel.kt │ │ │ └── update │ │ │ │ ├── UpdateScreen.kt │ │ │ │ ├── UpdateScreenComponent.kt │ │ │ │ └── UpdateScreenViewModel.kt │ │ ├── navigation │ │ │ ├── Component.kt │ │ │ └── NavHostComponent.kt │ │ ├── theme │ │ │ ├── Colors.kt │ │ │ ├── Theme.kt │ │ │ └── Typography.kt │ │ └── util │ │ │ ├── ColorGenerator.kt │ │ │ └── IntExt.kt │ │ └── util │ │ ├── ApkSource.kt │ │ ├── ColorUtil.kt │ │ ├── PureRandom.kt │ │ ├── R.kt │ │ └── flow │ │ └── EventFlow.kt └── resources │ ├── apktool_2.9.3.jar │ ├── drawables │ ├── books.svg │ ├── guy.png │ ├── ic_error_code.png │ ├── launcher_icons │ │ ├── linux.png │ │ ├── macos.icns │ │ └── windows.ico │ ├── loading.png │ ├── logo.svg │ ├── no_device.png │ ├── playstore.svg │ ├── storefront.svg │ ├── usb.svg │ └── woman_desk.png │ ├── fonts │ ├── FiraCode-Regular.ttf │ ├── GoogleSans-Bold.ttf │ ├── GoogleSans-Medium.ttf │ └── GoogleSans-Regular.ttf │ └── jadx-1.3.1.zip └── test ├── kotlin └── com │ └── theapache64 │ └── stackzy │ ├── data │ ├── repo │ │ ├── AdbRepoTest.kt │ │ ├── ApkAnalyzerRepoTest.kt │ │ ├── AuthRepoTest.kt │ │ ├── ConfigRepoTest.kt │ │ ├── JadxRepoTest.kt │ │ ├── LibrariesRepoTest.kt │ │ ├── PlayStoreRepoTest.kt │ │ ├── ResultsRepoTest.kt │ │ └── UntrackedLibsRepoTest.kt │ └── util │ │ └── StringUtilsTest.kt │ ├── test │ ├── MyDaggerMockRule.kt │ ├── TestComponent.kt │ └── Utils.kt │ ├── ui │ └── feature │ │ └── applist │ │ └── AppListViewModelTest.kt │ └── util │ ├── AndroidVersionIdentifierTest.kt │ └── LibrariesRepoExt.kt └── resources ├── a.i.apk ├── com.Dani.Balls_1.18_unity.apk ├── com.mobmaxime.xamarin_xamarin.apk ├── com.netflix.mediaclient_AndroidManifest.xml ├── com.reactnativeanimationexamples_react_native.apk ├── com.sts.flutter_flutter.apk ├── com.swot.emicalculator_cordova.apk ├── com.theah64.whatsappstatusbrowser_java_android.apk ├── com.theapache64.topcorn_kotlin_android.apk └── com.twitter.android_9.26.0.apk /.fleet/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "gradle", 5 | "name": "Run App", 6 | "tasks": [ 7 | "run" 8 | ] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report about: Create a report to help us improve title: '' 3 | labels: '' 4 | assignees: '' 5 | 6 | --- 7 | 8 | **Describe the bug** 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | Steps to reproduce the behavior: 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Actual behavior** 23 | A clear and concise description of what actually happened. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | 30 | - OS: [e.g. Windows 7 Ultimate] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request about: Suggest an idea for this project title: '' 3 | labels: '' 4 | assignees: '' 5 | 6 | --- 7 | 8 | **Is your feature request related to a problem? Please describe.** 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | **Describe the solution you'd like** 12 | A clear and concise description of what you want to happen. 13 | 14 | **Describe alternatives you've considered** 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Pull request checklist 4 | 5 | Please check if your PR fulfills the following requirements: 6 | 7 | - [ ] Tests for the changes have been added (for bug fixes / features) 8 | - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) 9 | - [ ] Build (`./gradlew build`) was run locally and any changes were pushed 10 | - [ ] Lint (`./gradlew test`) has passed locally and any fixes were made for failures 11 | 12 | ## Pull request type 13 | 14 | 15 | 16 | 17 | 18 | Please check the type of change your PR introduces: 19 | 20 | - [ ] Bugfix 21 | - [ ] Feature 22 | - [ ] Code style update (formatting, renaming) 23 | - [ ] Refactoring (no functional changes, no api changes) 24 | - [ ] Build related changes 25 | - [ ] Documentation content changes 26 | - [ ] Other (please describe): 27 | 28 | ## What is the current behavior? 29 | 30 | 31 | 32 | Issue Number: N/A 33 | 34 | ## What is the new behavior? 35 | 36 | 37 | 38 | - 39 | - 40 | - 41 | 42 | ## Does this introduce a breaking change? 43 | 44 | - [ ] Yes 45 | - [ ] No 46 | 47 | 48 | 49 | ## Other information 50 | 51 | -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | org.gradle.parallel=true 3 | org.gradle.workers.max=2 4 | org.gradle.jvmargs=-Xmx6G 5 | org.gradle.caching=true 6 | org.gradle.configureondemand=true 7 | # parallel kapt 8 | kapt.use.worker.api=true -------------------------------------------------------------------------------- /.github/config/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🌟 Features", 5 | "labels": [ 6 | "🚀", 7 | "🌟" 8 | ] 9 | }, 10 | { 11 | "title": "## 🐛 Fixes", 12 | "labels": [ 13 | "🐛", 14 | "🚑" 15 | ] 16 | }, 17 | { 18 | "title": "## 💬 Other", 19 | "labels": [] 20 | } 21 | ], 22 | "pr_template": "${{TITLE}}", 23 | "label_extractor": [ 24 | { 25 | "pattern": "(.) (.+)", 26 | "target": "$1" 27 | } 28 | ], 29 | "transformers": [ 30 | { 31 | "pattern": "(\\[ImgBot\\]) (.+)", 32 | "target": "- [ImgBot] $2" 33 | }, 34 | { 35 | "pattern": "(.) (.+)", 36 | "target": "- $2" 37 | } 38 | ], 39 | "exclude_merge_branches": [ 40 | "merge pull request", 41 | "Merge pull request" 42 | ] 43 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | if: github.event_name == 'pull_request' 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - uses: actions/setup-java@v1 23 | with: 24 | java-version: '17' 25 | java-package: jdk 26 | 27 | - name: Validate gradle wrapper 28 | uses: gradle/wrapper-validation-action@v1 29 | 30 | - name: Copy CI gradle.properties 31 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 32 | 33 | - name: Checkout Gradle Build Cache 34 | uses: actions/cache@v2 35 | with: 36 | path: | 37 | ~/.gradle/caches 38 | ~/.gradle/wrapper 39 | !~/.gradle/wrapper/dists/**/gradle*.zip 40 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 41 | restore-keys: | 42 | gradle-${{ runner.os }}- 43 | 44 | - name: Build Debug 45 | run: ./gradlew build -x test 46 | 47 | changelog: 48 | name: Changelog 49 | runs-on: ubuntu-latest 50 | if: startsWith(github.ref, 'refs/tags/') 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v2 54 | 55 | - name: Build Changelog 56 | id: github_release 57 | uses: mikepenz/release-changelog-builder-action@v1 58 | with: 59 | configuration: ".github/config/configuration.json" 60 | commitMode: true 61 | ignorePreReleases: ${{ !contains(github.ref, '-') }} 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | - name: Release 66 | uses: softprops/action-gh-release@91409e712cf565ce9eff10c87a8d1b11b81757ae 67 | with: 68 | body: ${{steps.github_release.outputs.changelog}} 69 | prerelease: ${{ contains(github.event.inputs.version, '-rc') || contains(github.event.inputs.version, '-b') || contains(github.event.inputs.version, '-a') }} 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | 73 | release: 74 | name: Release 75 | strategy: 76 | fail-fast: false 77 | matrix: 78 | os: [macos-14, ubuntu-latest, windows-latest, macos-13] 79 | runs-on: ${{ matrix.os }} 80 | if: startsWith(github.ref, 'refs/tags/') 81 | steps: 82 | - name: Checkout 83 | uses: actions/checkout@v2 84 | 85 | - uses: actions/setup-java@v1 86 | with: 87 | java-version: '17' 88 | java-package: jdk 89 | 90 | - name: Validate gradle wrapper 91 | uses: gradle/wrapper-validation-action@v1 92 | 93 | - name: Copy CI gradle.properties 94 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 95 | 96 | - name: Checkout Gradle Build Cache 97 | uses: actions/cache@v2 98 | with: 99 | path: | 100 | ~/.gradle/caches 101 | ~/.gradle/wrapper 102 | !~/.gradle/wrapper/dists/**/gradle*.zip 103 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 104 | restore-keys: | 105 | gradle-${{ runner.os }}- 106 | 107 | - name: Build Debug 108 | run: ./gradlew build -x test 109 | 110 | - name: Build Release App 111 | run: | 112 | ./gradlew packageUberJarForCurrentOS 113 | ./gradlew packageDistributionForCurrentOS 114 | 115 | - name: Archive Artifacts 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: distributable-${{ matrix.os }} 119 | if-no-files-found: ignore 120 | path: | 121 | build/**/*.deb 122 | build/**/*.msi 123 | build/compose/jars/*.jar 124 | 125 | - name: Release 126 | uses: softprops/action-gh-release@91409e712cf565ce9eff10c87a8d1b11b81757ae 127 | with: 128 | prerelease: ${{ contains(github.event.inputs.version, '-rc') || contains(github.event.inputs.version, '-b') || contains(github.event.inputs.version, '-a') }} 129 | files: | 130 | build/**/*.deb 131 | build/**/*.msi 132 | build/compose/jars/*.jar 133 | env: 134 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,intellij,gradle 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij Patch ### 79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 80 | 81 | # *.iml 82 | # modules.xml 83 | # .idea/misc.xml 84 | # *.ipr 85 | 86 | # Sonarlint plugin 87 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 88 | .idea/**/sonarlint/ 89 | 90 | # SonarQube Plugin 91 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 92 | .idea/**/sonarIssues.xml 93 | 94 | # Markdown Navigator plugin 95 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 96 | .idea/**/markdown-navigator.xml 97 | .idea/**/markdown-navigator-enh.xml 98 | .idea/**/markdown-navigator/ 99 | 100 | # Cache file creation bug 101 | # See https://youtrack.jetbrains.com/issue/JBR-2257 102 | .idea/$CACHE_FILE$ 103 | 104 | # CodeStream plugin 105 | # https://plugins.jetbrains.com/plugin/12206-codestream 106 | .idea/codestream.xml 107 | 108 | ### Kotlin ### 109 | # Compiled class file 110 | *.class 111 | 112 | # Log file 113 | *.log 114 | 115 | # BlueJ files 116 | *.ctxt 117 | 118 | # Mobile Tools for Java (J2ME) 119 | .mtj.tmp/ 120 | 121 | # Package Files # 122 | *.jar 123 | *.war 124 | *.nar 125 | *.ear 126 | *.zip 127 | *.tar.gz 128 | *.rar 129 | 130 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 131 | hs_err_pid* 132 | 133 | ### Gradle ### 134 | .gradle 135 | build/ 136 | 137 | # Ignore Gradle GUI config 138 | gradle-app.setting 139 | 140 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 141 | !gradle-wrapper.jar 142 | 143 | # Cache of project 144 | .gradletasknamecache 145 | 146 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 147 | # gradle/wrapper/gradle-wrapper.properties 148 | 149 | ### Gradle Patch ### 150 | **/build/ 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle 153 | build 154 | adb 155 | 156 | !src/main/resources/apktool_*.jar 157 | !src/main/resources/adb 158 | 159 | AdbWinApi.dll 160 | AdbWinUsbApi.dll 161 | adb.exe 162 | .DS_Store 163 | 164 | build/topcorn_decompiled 165 | gpm.json 166 | 167 | jadx-* 168 | !jadx-*.zip -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.theapache64.stackzy.AppKt 3 | 4 | -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | kotlin("kapt") 4 | } 5 | 6 | group = "com.theapache64.stackzy" 7 | version = "1.2.5" 8 | 9 | repositories { 10 | mavenCentral() 11 | maven { url = uri("https://jitpack.io") } 12 | } 13 | 14 | dependencies { 15 | implementation(kotlin("stdlib-jdk8")) 16 | 17 | // Adam : The ADB client 18 | api("com.malinskiy.adam:adam:0.4.3") 19 | 20 | // Moshi : A modern JSON API for Android and Java 21 | val moshiVersion = "1.15.0" 22 | api("com.squareup.moshi:moshi:$moshiVersion") 23 | implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") 24 | kapt("com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion") 25 | implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.31") 26 | 27 | // Retrosheet : Turn Google Spreadsheet to JSON endpoint (for Android and JVM) 28 | api("com.github.theapache64:retrosheet:2.0.0") 29 | 30 | // Retrofit : A type-safe HTTP client for Android and Java. 31 | val retrofitVersion = "2.9.0" 32 | api("com.squareup.retrofit2:retrofit:$retrofitVersion") 33 | implementation("com.squareup.retrofit2:converter-moshi:$retrofitVersion") 34 | 35 | 36 | // Kotlinx Coroutines Core : Coroutines support libraries for Kotlin 37 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") 38 | 39 | // Arbor : Like Timber, just different. 40 | api("com.ToxicBakery.logging:arbor-jvm:1.35.72") 41 | 42 | val daggerVersion: String by rootProject.extra 43 | api("com.google.dagger:dagger:$daggerVersion") 44 | kapt("com.google.dagger:dagger-compiler:$daggerVersion") 45 | 46 | 47 | // GooglePlay API 48 | implementation("com.google.protobuf:protobuf-java:3.19.3") 49 | api("com.github.theapache64:google-play-api:0.0.9") 50 | 51 | // SnakeYAML : YAML 1.1 parser and emitter for Java 52 | implementation("org.yaml:snakeyaml:1.29") 53 | } 54 | 55 | tasks.withType { 56 | kotlinOptions.jvmTarget = "17" 57 | // kotlinOptions.freeCompilerArgs += "-Xuse-experimental=androidx.compose.foundation.ExperimentalFoundationApi" 58 | // kotlinOptions.freeCompilerArgs += "-Xuse-experimental=androidx.compose.ui.ExperimentalComposeUiApi" 59 | // kotlinOptions.freeCompilerArgs += "-Xuse-experimental=kotlin.io.path.ExperimentalPathApi" 60 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/brut/androlib/meta/MetaInfo.kt: -------------------------------------------------------------------------------- 1 | package brut.androlib.meta 2 | 3 | /** 4 | * To parse YAML generated by apk-tool 5 | */ 6 | data class MetaInfo( 7 | var sdkInfo: SdkInfo? = null, 8 | var versionInfo: VersionInfo? = null 9 | ) { 10 | data class SdkInfo( 11 | var minSdkVersion: Int? = null, 12 | var targetSdkVersion: Int? = null 13 | ) 14 | 15 | data class VersionInfo( 16 | var versionCode: Int? = null, 17 | var versionName: String? = null 18 | ) 19 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/data/src/main/kotlin/com/theapache64/stackzy/data/local/.gitkeep -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/AlphabetCircle.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | abstract class BaseAlphabetCircle { 4 | 5 | abstract fun getTitle(): String 6 | abstract fun getSubtitle(): String 7 | open fun getSubtitle2(): String? = null 8 | abstract fun imageUrl(): String? 9 | open fun getAlphabet() = getTitle().first() 10 | open fun isNew(): Boolean = false 11 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/AnalysisReport.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | import com.theapache64.stackzy.data.remote.Config 4 | import com.theapache64.stackzy.data.remote.Library 5 | import com.theapache64.stackzy.data.remote.Result 6 | import com.theapache64.stackzy.data.repo.ResultsRepo 7 | import java.io.File 8 | 9 | 10 | interface AnalysisReportDefinition { 11 | val appName: String? 12 | val packageName: String 13 | val platform: Platform 14 | val libraries: Set 15 | val untrackedLibraries: Set 16 | val apkSizeInMb: Float 17 | val assetsDir: File? 18 | val permissions: Set 19 | val gradleInfo: GradleInfo 20 | } 21 | 22 | class AnalysisReport( 23 | override val appName: String?, 24 | override val packageName: String, 25 | override val platform: Platform, 26 | override val libraries: Set, 27 | override val untrackedLibraries: Set, 28 | override val apkSizeInMb: Float, 29 | override val assetsDir: File?, 30 | override val permissions: Set, 31 | override val gradleInfo: GradleInfo 32 | ) : AnalysisReportDefinition 33 | 34 | 35 | fun AnalysisReport.toResult(resultsRepo: ResultsRepo, config: Config? = null, logoImageUrl: String): Result { 36 | 37 | return Result( 38 | appName = this.appName ?: this.packageName, 39 | packageName = this.packageName, 40 | libPackages = this.libraries.joinToString(",") { it.packageName }, 41 | versionName = this.gradleInfo.versionName ?: "Unknown", 42 | versionCode = this.gradleInfo.versionCode ?: -1, 43 | platform = this.platform::class.simpleName!!, 44 | apkSizeInMb = this.apkSizeInMb, 45 | permissions = this.permissions.joinToString(","), 46 | gradleInfoJson = resultsRepo.jsonify(this.gradleInfo), 47 | stackzyLibVersion = config?.latestStackzyLibVersion ?: 0, 48 | logoImageUrl = logoImageUrl 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/AndroidApp.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | import com.malinskiy.adam.request.pkg.Package 4 | 5 | interface AndroidAppDefinition { 6 | val appPackage: Package 7 | val isSystemApp: Boolean 8 | val versionCode: Int? 9 | val versionName: String? 10 | val appTitle: String? 11 | val imageUrl: String? 12 | val appSize: String? 13 | } 14 | 15 | class AndroidApp( 16 | override val appPackage: Package, 17 | override val isSystemApp: Boolean, 18 | override val versionCode: Int? = null, 19 | override val versionName: String? = null, 20 | override val appTitle: String? = null, 21 | override val imageUrl: String? = null, 22 | override val appSize: String? = null, 23 | ) : AndroidAppDefinition -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/AndroidDevice.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | import com.malinskiy.adam.request.device.Device 4 | 5 | interface AndroidDeviceDefinition { 6 | val name: String 7 | val model: String 8 | val device: Device 9 | } 10 | 11 | class AndroidDevice( 12 | override val name: String, 13 | override val model: String, 14 | override val device: Device 15 | ) : AndroidDeviceDefinition -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/AppArgs.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | data class AppArgs( 4 | val appName: String, 5 | val version: String, 6 | val versionCode: Int 7 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/GradleInfo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class GradleInfo( 7 | val versionCode: Int?, 8 | val versionName: String?, 9 | val minSdk: Sdk?, 10 | val targetSdk: Sdk?, 11 | ) { 12 | data class Sdk( 13 | val sdkInt: Int, 14 | val versionName: String? 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/LibResult.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | import com.theapache64.stackzy.data.remote.Library 4 | 5 | data class LibResult( 6 | var appLibs: Set, 7 | val untrackedLibs: Set, // untracked package names 8 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/local/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.local 2 | 3 | sealed class Platform(val name: String) { 4 | companion object { 5 | fun fromClassName(platformClassName: String): Platform { 6 | return when (platformClassName) { 7 | NativeKotlin::class.simpleName -> NativeKotlin() 8 | NativeJava::class.simpleName -> NativeJava() 9 | ReactNative::class.simpleName -> ReactNative() 10 | Flutter::class.simpleName -> Flutter() 11 | Cordova::class.simpleName -> Cordova() 12 | PhoneGap::class.simpleName -> PhoneGap() 13 | Xamarin::class.simpleName -> Xamarin() 14 | Unity::class.simpleName -> Unity() 15 | else -> throw IllegalArgumentException("Undefined platform '$platformClassName'") 16 | } 17 | } 18 | } 19 | 20 | class NativeKotlin : Platform("Native Android with Kotlin") 21 | class NativeJava : Platform("Native Android with Java") 22 | class ReactNative : Platform("React Native") 23 | class Flutter : Platform("Flutter") 24 | class Cordova : Platform("Apache Cordova") 25 | class PhoneGap : Platform("Adobe PhoneGap") 26 | class Xamarin : Platform("Xamarin") 27 | class Unity : Platform("Unity") 28 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/ApiInterface.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.github.theapache64.retrosheet.annotations.KeyValue 4 | import com.github.theapache64.retrosheet.annotations.Read 5 | import com.github.theapache64.retrosheet.annotations.Write 6 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 7 | import com.theapache64.stackzy.di.module.NetworkModule 8 | import kotlinx.coroutines.flow.Flow 9 | import retrofit2.http.Body 10 | import retrofit2.http.GET 11 | import retrofit2.http.POST 12 | import retrofit2.http.Query 13 | 14 | interface ApiInterface { 15 | 16 | @Read("SELECT * ORDER BY name") 17 | @GET(NetworkModule.TABLE_LIBRARIES) 18 | fun getLibraries(): Flow>> 19 | 20 | @Write 21 | @POST(NetworkModule.TABLE_UNTRACKED_LIBS) 22 | fun addUntrackedLibrary( 23 | @Body untrackedLibrary: UntrackedLibrary 24 | ): Flow> 25 | 26 | @Read("SELECT *") 27 | @GET(NetworkModule.TABLE_UNTRACKED_LIBS) 28 | fun getUntrackedLibraries(): Flow>> 29 | 30 | @KeyValue 31 | @GET(NetworkModule.TABLE_CONFIG) 32 | fun getConfig(): Flow> 33 | 34 | @Write 35 | @POST(NetworkModule.TABLE_RESULTS) 36 | fun addResult(@Body result: Result): Flow> 37 | 38 | @Read("SELECT * WHERE package_name = :package_name AND version_code = :version_code AND stackzy_lib_version = :stackzy_lib_version LIMIT 1") 39 | @GET(NetworkModule.TABLE_RESULTS) 40 | fun getResult( 41 | @Query("package_name") packageName: String, 42 | @Query("version_code") versionCode: Int, 43 | @Query("stackzy_lib_version") stackzyLibVersion: Int, 44 | ): Flow> 45 | 46 | @Read("SELECT * WHERE lib_packages contains :lib_package AND package_name != 'com.theapache64.test.app' ORDER BY app_name") 47 | @GET(NetworkModule.TABLE_RESULTS) 48 | fun getResults( 49 | @Query("lib_package") libPackageName: String, 50 | ): Flow>> 51 | 52 | @Read("SELECT lib_packages WHERE lib_packages != '' && package_name != 'com.theapache64.test.app' ORDER BY name") 53 | @GET(NetworkModule.TABLE_RESULTS) 54 | fun getAllLibPackages(): Flow>> 55 | 56 | @Read("SELECT * WHERE package_name = :package_name AND version_code != :except_v_code ORDER BY created_at DESC LIMIT 1") 57 | @GET(NetworkModule.TABLE_RESULTS) 58 | fun getPrevResult( 59 | @Query("package_name") packageName: String, 60 | @Query("except_v_code") exceptVersionCode: Int 61 | ): Flow> 62 | 63 | @Read("SELECT *") 64 | @GET(NetworkModule.TABLE_FUN_FACTS) 65 | fun getFunFacts(): Flow>> 66 | 67 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/Config.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class Config( 8 | @Json(name = "should_consider_result_cache") 9 | val shouldConsiderResultCache: Boolean, 10 | @Json(name = "latest_stackzy_lib_version") 11 | val latestStackzyLibVersion: Int, 12 | @Json(name = "mandatory_version_code") 13 | val mandatoryVersionCode: Int, 14 | @Json(name = "is_browse_by_lib_enabled") 15 | val isBrowseByLibEnabled: Boolean, 16 | @Json(name = "is_play_store_enabled") 17 | val isPlayStoreEnabled: Boolean, 18 | @Json(name = "is_libs_tracking_enabled") 19 | val isLibsTrackingEnabled: Boolean, 20 | @Json(name = "is_down") 21 | val isDown: Boolean, 22 | @Json(name = "down_reason") 23 | val downReason: String, 24 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/FunFact.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class FunFact( 8 | @Json(name = "id") 9 | val id: Int, // 1 10 | @Json(name = "fun_fact") 11 | val funFact: String // lorem ipsum 12 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/Library.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | interface LibraryDefinition { 7 | val category: String 8 | val id: Int 9 | val name: String 10 | val packageName: String 11 | val replacementPackage: String? 12 | val website: String 13 | } 14 | 15 | @JsonClass(generateAdapter = true) 16 | data class Library( 17 | @Json(name = "category") 18 | override val category: String, 19 | @Json(name = "id") 20 | override val id: Int, 21 | @Json(name = "name") 22 | override val name: String, 23 | @Json(name = "package_name") 24 | override val packageName: String, 25 | @Json(name = "replacement_package") 26 | override val replacementPackage: String?, 27 | @Json(name = "website") 28 | override val website: String, 29 | ) : LibraryDefinition { 30 | 31 | companion object { 32 | const val CATEGORY_OTHER = "Other" 33 | } 34 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/OptionalResult.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class OptionalResult( 8 | @Json(name = "lib_packages") 9 | val libPackages: String?, 10 | ) 11 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/Result.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class Result( 8 | @Json(name = "app_name") 9 | val appName: String, 10 | @Json(name = "lib_packages") 11 | val libPackages: String?, 12 | @Json(name = "package_name") 13 | val packageName: String, // comma-sep 14 | @Json(name = "version_code") 15 | val versionCode: Int, 16 | @Json(name = "version_name") 17 | val versionName: String, 18 | @Json(name = "platform") 19 | val platform: String, 20 | @Json(name = "apk_size_in_mb") 21 | val apkSizeInMb: Float, 22 | @Json(name = "permissions") 23 | val permissions: String?, // comma-sep 24 | @Json(name = "gradle_info_json") 25 | val gradleInfoJson: String, 26 | @Json(name = "stackzy_lib_version") 27 | val stackzyLibVersion: Int, 28 | @Json(name = "logo_image_url") 29 | val logoImageUrl: String 30 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/remote/UntrackedLibrary.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.remote 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | 7 | @JsonClass(generateAdapter = true) 8 | data class UntrackedLibrary( 9 | @Json(name = "package_names") 10 | val packageNames: String 11 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/ApkToolRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.theapache64.stackzy.data.util.CommandExecutor 4 | import com.theapache64.stackzy.di.ApkToolJarFile 5 | import com.toxicbakery.logging.Arbor 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import java.io.File 9 | import javax.inject.Inject 10 | import kotlin.io.path.createTempDirectory 11 | 12 | class ApkToolRepo @Inject constructor( 13 | @ApkToolJarFile 14 | private val apkToolJarFile: File 15 | ) { 16 | 17 | suspend fun decompile( 18 | destinationFile: File, 19 | targetDir: File = createTempDirectory().toFile(), 20 | onDecompileMessage: ((String) -> Unit)? = null 21 | ): File = withContext(Dispatchers.IO) { 22 | 23 | val command = 24 | "java -jar \"${apkToolJarFile.absolutePath}\" d \"${destinationFile.absolutePath}\" -o \"${targetDir.absolutePath}\" -f" 25 | Arbor.d("Decompiling : \n$command && code-insiders '${targetDir.absolutePath}'") 26 | CommandExecutor.executeCommand( 27 | command = command, 28 | isSkipException = false, 29 | isLivePrint = true, 30 | onPrintLine = onDecompileMessage 31 | ) 32 | targetDir 33 | } 34 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/AuthRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.gpa.api.Play 4 | import com.github.theapache64.gpa.model.Account 5 | import com.squareup.moshi.Moshi 6 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 7 | import com.theapache64.stackzy.util.Crypto 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.withContext 11 | import java.util.prefs.Preferences 12 | import javax.inject.Inject 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | class AuthRepo @Inject constructor( 17 | private val moshi: Moshi, 18 | private val pref: Preferences, 19 | private val crypto: Crypto 20 | ) { 21 | 22 | companion object { 23 | private const val KEY_ENC_ACCOUNT = "enc_account" 24 | private const val KEY_IS_REMEMBER = "is_remember" 25 | private const val KEY_IS_LOGGED_IN = "is_logged_in" 26 | } 27 | 28 | private val accountAdapter by lazy { 29 | moshi.adapter(Account::class.java) 30 | } 31 | 32 | /** 33 | * To get active account 34 | */ 35 | fun getAccount(): Account? { 36 | val encAccountJson = pref.get(KEY_ENC_ACCOUNT, null) 37 | return if (encAccountJson != null) { 38 | // Parse 39 | val accountJson = crypto.decrypt(encAccountJson) 40 | accountAdapter.fromJson(accountJson) 41 | } else { 42 | null 43 | } 44 | } 45 | 46 | /** 47 | * To login with given google username and password 48 | */ 49 | suspend fun logIn(username: String, password: String) = withContext(Dispatchers.IO) { 50 | flow> { 51 | // Loading 52 | emit(Resource.Loading()) 53 | 54 | try { 55 | val account = Play.login(username, password) 56 | emit(Resource.Success(account)) 57 | } catch (e: Exception) { 58 | e.printStackTrace() 59 | val userFriendlyMsg = when (e.message) { 60 | "NeedsBrowser" -> "Looks like you've 2FA enabled. Please disable it and try again" 61 | else -> e.message 62 | } 63 | emit(Resource.Error(userFriendlyMsg ?: "Something went wrong")) 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * To persist account 70 | */ 71 | fun storeAccount(account: Account, isRemember: Boolean) { 72 | val encAccountJson = crypto.encrypt(accountAdapter.toJson(account)) 73 | pref.put(KEY_ENC_ACCOUNT, encAccountJson) 74 | pref.putBoolean(KEY_IS_REMEMBER, isRemember) 75 | } 76 | 77 | 78 | /** 79 | * To logout and remove account details from preference 80 | */ 81 | fun clearAccount() { 82 | pref.remove(KEY_ENC_ACCOUNT) 83 | } 84 | 85 | /** 86 | * To get account or throw exception 87 | */ 88 | fun getAccountOrThrow() = getAccount() 89 | ?: throw IllegalArgumentException("Couldn't get account. Are you sure you've logged in via the app?") 90 | 91 | fun isRemember() = pref.getBoolean(KEY_IS_REMEMBER, false) 92 | 93 | fun setLoggedIn(isLoggedIn: Boolean) { 94 | pref.putBoolean(KEY_IS_LOGGED_IN, isLoggedIn) 95 | } 96 | 97 | fun isLoggedIn() = pref.getBoolean(KEY_IS_LOGGED_IN, false) 98 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/ConfigRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.theapache64.stackzy.data.remote.ApiInterface 5 | import com.theapache64.stackzy.data.remote.Config 6 | import com.theapache64.stackzy.data.remote.ConfigJsonAdapter 7 | import java.util.prefs.Preferences 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * To manage global config flags 13 | */ 14 | @Singleton 15 | open class ConfigRepo @Inject constructor( 16 | private val apiInterface: ApiInterface, 17 | private val moshi: Moshi, 18 | private val pref: Preferences 19 | ) { 20 | companion object { 21 | private const val KEY_CONFIG = "config" 22 | } 23 | 24 | private val configJsonAdapter by lazy { 25 | ConfigJsonAdapter(moshi) 26 | } 27 | 28 | fun getRemoteConfig() = 29 | apiInterface.getConfig() 30 | 31 | fun getLocalConfig(): Config? { 32 | return pref.get(KEY_CONFIG, null)?.run { 33 | configJsonAdapter.fromJson(this) 34 | } 35 | } 36 | 37 | fun saveConfigToLocal(data: Config) { 38 | pref.put(KEY_CONFIG, configJsonAdapter.toJson(data)) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/FunFactsRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.Types 5 | import com.theapache64.stackzy.data.remote.ApiInterface 6 | import com.theapache64.stackzy.data.remote.FunFact 7 | import java.util.prefs.Preferences 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | @Singleton 12 | open class FunFactsRepo @Inject constructor( 13 | private val apiInterface: ApiInterface, 14 | private val moshi: Moshi, 15 | private val pref: Preferences 16 | ) { 17 | companion object { 18 | private const val KEY_FUN_FACTS = "FunFacts" 19 | } 20 | 21 | private val funFactsJsonAdapter by lazy { 22 | val listMyData = Types.newParameterizedType(List::class.java, FunFact::class.java) 23 | moshi.adapter>(listMyData) 24 | } 25 | 26 | fun getRemoteFunFacts() = 27 | apiInterface.getFunFacts() 28 | 29 | fun getLocalFunFacts(): Set? { 30 | val funFactsJson = pref.get(KEY_FUN_FACTS, null) 31 | return funFactsJsonAdapter.fromJson(funFactsJson)?.toSet() 32 | } 33 | 34 | fun saveFunFactsToLocal(data: List) { 35 | pref.put(KEY_FUN_FACTS, funFactsJsonAdapter.toJson(data)) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/JadxRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.theapache64.stackzy.data.util.CommandExecutor 4 | import com.theapache64.stackzy.di.JadxDirPath 5 | import com.theapache64.stackzy.util.OSType 6 | import com.theapache64.stackzy.util.OsCheck 7 | import java.io.File 8 | import java.nio.file.Path 9 | import javax.inject.Inject 10 | import kotlin.io.path.absolutePathString 11 | import kotlin.io.path.div 12 | 13 | class JadxRepo @Inject constructor( 14 | @JadxDirPath val jadxDirPath: Path 15 | ) { 16 | fun open( 17 | apkFile: File 18 | ) { 19 | val jadxPath = jadxDirPath / "bin" / "jadx-gui" 20 | val jadX = if (OsCheck.operatingSystemType == OSType.Windows) { 21 | "${jadxPath.absolutePathString()}.bat" // execute bat 22 | } else { 23 | "sh '${jadxPath.absolutePathString()}'" // execute shell script 24 | } 25 | val command = "$jadX '${apkFile.absolutePath}'" 26 | CommandExecutor.executeCommand( 27 | command, 28 | isLivePrint = false, 29 | isSkipException = true 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/LibrariesRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.theapache64.stackzy.data.remote.ApiInterface 4 | import com.theapache64.stackzy.data.remote.Library 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | class LibrariesRepo @Inject constructor( 10 | private val apiInterface: ApiInterface 11 | ) { 12 | 13 | private var cachedLibraries: List? = null 14 | 15 | fun getRemoteLibraries() = apiInterface.getLibraries() 16 | 17 | fun cacheLibraries(Libraries: List) { 18 | cachedLibraries = Libraries 19 | } 20 | 21 | fun getCachedLibraries(): List? { 22 | return cachedLibraries 23 | } 24 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/PlayStoreRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.akdeniz.googleplaycrawler.GooglePlay 4 | import com.akdeniz.googleplaycrawler.GooglePlayAPI 5 | import com.akdeniz.googleplaycrawler.GooglePlayException 6 | import com.github.theapache64.gpa.api.Play 7 | import com.github.theapache64.gpa.model.Account 8 | import com.malinskiy.adam.request.pkg.Package 9 | import com.theapache64.stackzy.data.local.AndroidApp 10 | import com.theapache64.stackzy.data.util.bytesToMb 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.flow 13 | import kotlinx.coroutines.flow.flowOn 14 | import kotlinx.coroutines.withContext 15 | import java.io.File 16 | import java.io.FileOutputStream 17 | import java.util.* 18 | import javax.inject.Inject 19 | 20 | class PlayStoreRepo @Inject constructor() { 21 | 22 | suspend fun find( 23 | packageName: String, 24 | api: GooglePlayAPI 25 | ): AndroidApp? = withContext(Dispatchers.IO) { 26 | try { 27 | api.details( 28 | packageName 29 | ).let { 30 | parseAndroidApp(it.docV2) 31 | } 32 | } catch (e: GooglePlayException) { 33 | null 34 | } 35 | } 36 | 37 | /** 38 | * To search with the given keyword in play store 39 | */ 40 | suspend fun search( 41 | keyword: String, 42 | api: GooglePlayAPI, 43 | maxSearchResult: Int = 30 44 | ): List = withContext(Dispatchers.IO) { 45 | // First search 46 | var serp = Play.search(query = keyword, api = api) 47 | 48 | // loading more pages until "either no more page" or "maxSearchResult or more items" 49 | while ( 50 | serp.nextPageUrl?.isNotBlank() == true && 51 | serp.content.distinctBy { it.docid }.size <= maxSearchResult 52 | ) { 53 | serp = Play.search(query = keyword, api = api, serp) 54 | } 55 | 56 | // We've loaded all results from network. Now let's sanitize and convert it to AndroidApp class 57 | serp.content 58 | .distinctBy { it.docid } // To remove duplicates 59 | .map { item -> 60 | parseAndroidApp(item) 61 | } 62 | } 63 | 64 | private fun parseAndroidApp(item: GooglePlay.DocV2): AndroidApp { 65 | // Convert bytes to MB (to readable format) 66 | val appDetails = item.details.appDetails 67 | val sizeInMb = appDetails.installDetails.totalApkSize.bytesToMb.let { sizeInMb -> 68 | "%.2f".format(Locale.US, sizeInMb).toFloat() 69 | } 70 | 71 | // Adding app to final list 72 | return AndroidApp( 73 | appPackage = Package(item.docid), // Package name 74 | appTitle = item.title, // App title 75 | versionCode = appDetails.versionCode, 76 | versionName = appDetails.versionString, 77 | imageUrl = item.imageList[1].imageUrl, // Logo URL 78 | appSize = "$sizeInMb MB", // APK Size 79 | isSystemApp = false, 80 | ) 81 | } 82 | 83 | /** 84 | * To download APK for the given packageName and write to given apkFile using given account 85 | */ 86 | @Suppress("BlockingMethodInNonBlockingContext") 87 | fun downloadApk( 88 | apkFile: File, 89 | account: Account, 90 | packageName: String 91 | ) = flow { 92 | val api = Play.getApi(account) 93 | val appDetails = api.details(packageName) 94 | val downloadData = api.purchaseAndDeliver( 95 | packageName, 96 | appDetails.docV2.details.appDetails.versionCode, 97 | 1 98 | ) 99 | 100 | val totalSize = downloadData.appSize 101 | 102 | // Starting download 103 | downloadData.openApp().use { input -> 104 | FileOutputStream(apkFile).use { output -> 105 | val buffer = ByteArray(1024) 106 | var read: Int 107 | var counter = 0f 108 | while (input.read(buffer).also { read = it } != -1) { 109 | // Write 110 | output.write(buffer, 0, read) 111 | 112 | // Update progress 113 | counter += read 114 | val percentage = (counter / totalSize) * 100 115 | emit(percentage.toInt()) 116 | } 117 | 118 | // Finish progress 119 | emit(100) 120 | } 121 | } 122 | }.flowOn(Dispatchers.IO) 123 | 124 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/ResultsRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.theapache64.stackzy.data.local.GradleInfo 5 | import com.theapache64.stackzy.data.local.GradleInfoJsonAdapter 6 | import com.theapache64.stackzy.data.remote.ApiInterface 7 | import com.theapache64.stackzy.data.remote.Result 8 | import javax.inject.Inject 9 | 10 | /** 11 | * To store/retrieve to/from global Results sheet 12 | */ 13 | class ResultsRepo @Inject constructor( 14 | private val apiInterface: ApiInterface, 15 | private val moshi: Moshi 16 | ) { 17 | fun add(result: Result) = apiInterface.addResult(result) 18 | fun findResult( 19 | packageName: String, 20 | versionCode: Int, 21 | libVersionCode: Int 22 | ) = apiInterface.getResult(packageName, versionCode, libVersionCode) 23 | 24 | private val gradleInfoAdapter by lazy { 25 | GradleInfoJsonAdapter(moshi) 26 | } 27 | 28 | fun parseGradleInfo(gradleInfoJson: String): GradleInfo? { 29 | return gradleInfoAdapter.fromJson(gradleInfoJson) 30 | } 31 | 32 | fun jsonify(gradleInfo: GradleInfo): String { 33 | return gradleInfoAdapter.toJson(gradleInfo) 34 | } 35 | 36 | fun getAllLibPackages() = apiInterface.getAllLibPackages() 37 | fun getResults(libPackageName: String) = apiInterface.getResults(libPackageName) 38 | fun getPrevResult( 39 | packageName: String, 40 | exceptVersionCode: Int 41 | ) = apiInterface.getPrevResult(packageName, exceptVersionCode) 42 | } 43 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/repo/UntrackedLibsRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.theapache64.stackzy.data.remote.ApiInterface 4 | import com.theapache64.stackzy.data.remote.UntrackedLibrary 5 | import javax.inject.Inject 6 | 7 | /** 8 | * A repository to fetch and insert data from/to the untracked_libs table 9 | */ 10 | class UntrackedLibsRepo @Inject constructor( 11 | private val apiInterface: ApiInterface 12 | ) { 13 | fun add(untrackedLibrary: UntrackedLibrary) = apiInterface.addUntrackedLibrary(untrackedLibrary) 14 | fun getUntrackedLibs() = apiInterface.getUntrackedLibraries() 15 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/AndroidVersionIdentifier.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | object AndroidVersionIdentifier { 4 | private val androidVersionMap by lazy { 5 | mutableMapOf( 6 | 3 to "Cupcake", 7 | 4 to "Donut", 8 | 5..7 to "Eclair", 9 | 8 to "Froyo", 10 | 9..10 to "Gingerbread", 11 | 11..13 to "Honeycomb", 12 | 14..15 to "Ice Cream Sandwich", 13 | 16..18 to "Jelly Bean", 14 | 19..20 to "KitKat", 15 | 21..22 to "Lollipop", 16 | 23 to "Marshmallow", 17 | 24..25 to "Nougat", 18 | 26..27 to "Oreo", 19 | 28 to "Pie", 20 | 29 to "Android 10", 21 | 30 to "Android 11", 22 | 31 to "Android 12", 23 | ) 24 | } 25 | 26 | fun getVersion(sdkInt: Int): String? { 27 | var versionName: String? = null 28 | for ((key, value) in androidVersionMap) { 29 | 30 | if (key is Int && key == sdkInt) { 31 | // Int 32 | versionName = value 33 | } else if (key is IntRange && key.contains(sdkInt)) { 34 | // Int range 35 | versionName = value 36 | } 37 | 38 | if (versionName != null) { 39 | break 40 | } 41 | } 42 | return versionName 43 | } 44 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/CommandExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | import com.theapache64.stackzy.util.OSType 4 | import com.theapache64.stackzy.util.OsCheck 5 | import com.toxicbakery.logging.Arbor 6 | import java.io.BufferedReader 7 | import java.io.IOException 8 | import java.io.InputStreamReader 9 | 10 | /** 11 | * To execute commands programmatically 12 | */ 13 | object CommandExecutor { 14 | 15 | @Throws 16 | fun executeCommand( 17 | command: String, 18 | isLivePrint: Boolean, 19 | isSkipException: Boolean, 20 | onPrintLine: ((String) -> Unit)? = null 21 | ): String = 22 | executeCommands(arrayOf(command), isLivePrint, isSkipException, onPrintLine).joinToString(separator = "\n") 23 | 24 | 25 | @Throws(IOException::class) 26 | fun executeCommands( 27 | commands: Array, 28 | isLivePrint: Boolean, 29 | isSkipException: Boolean, 30 | onPrintLine: ((String) -> Unit)? = null 31 | ): List { 32 | 33 | val rt = Runtime.getRuntime() 34 | 35 | val proc = if (OsCheck.operatingSystemType == OSType.Windows) { 36 | // direct execution 37 | rt.exec(commands.joinToString(separator = " ")) 38 | } else { 39 | // execute via shell 40 | rt.exec( 41 | arrayOf( 42 | "/bin/sh", "-c", *commands 43 | ) 44 | ) 45 | } 46 | 47 | val stdInput = BufferedReader(InputStreamReader(proc.inputStream)) 48 | val stdError = BufferedReader(InputStreamReader(proc.errorStream)) 49 | 50 | // Read the output from the command 51 | val result = mutableListOf() 52 | stdInput.forEachLine { line -> 53 | if (isLivePrint) { 54 | Arbor.d(line) 55 | } 56 | onPrintLine?.invoke(line) 57 | result.add(line) 58 | } 59 | 60 | // Read any errors from the attempted command 61 | val error = StringBuilder() 62 | stdError.forEachLine { line -> 63 | if (isLivePrint) { 64 | Arbor.d(line) 65 | } 66 | onPrintLine?.invoke(line) 67 | error.append(line).append("\n") 68 | } 69 | 70 | if (!isSkipException) { 71 | if (error.isNotBlank() && result.isEmpty()) { // throw error only if result is empty 72 | // has error 73 | throw IOException(error.toString()) 74 | } 75 | } 76 | 77 | return result 78 | } 79 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/Crypto.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | 4 | import java.util.* 5 | import javax.crypto.Cipher 6 | import javax.crypto.spec.SecretKeySpec 7 | 8 | 9 | /** 10 | * To do basic text encryption and decryption with predefined salt. 11 | */ 12 | class Crypto( 13 | private val salt: ByteArray 14 | ) { 15 | 16 | companion object { 17 | private const val ALGORITHM = "AES" 18 | } 19 | 20 | fun encrypt(plainText: String): String { 21 | val cipher = Cipher.getInstance(ALGORITHM) 22 | cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(salt, ALGORITHM)) 23 | val encodedValue = cipher.doFinal(plainText.toByteArray()) 24 | return Base64.getEncoder().encodeToString(encodedValue) 25 | } 26 | 27 | fun decrypt(encodedText: String): String { 28 | val cipher = Cipher.getInstance(ALGORITHM) 29 | cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(salt, ALGORITHM)) 30 | val decodedValue = Base64.getDecoder().decode(encodedText) 31 | val decValue = cipher.doFinal(decodedValue) 32 | return String(decValue) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/FileExt.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | import java.io.File 4 | 5 | val File.size get() = if (!exists()) 0.0 else length().toDouble() 6 | val File.sizeInKb get() = size / 1024 7 | val File.sizeInMb get() = sizeInKb / 1024 8 | 9 | /** 10 | * To convert bytes to MB 11 | */ 12 | val Long.bytesToMb get() = (this / 1024) / 1024f -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/OsCheck.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Types of Operating Systems 7 | */ 8 | enum class OSType { 9 | Windows, MacOS, Linux, Other 10 | } 11 | 12 | object OsCheck { 13 | 14 | /** 15 | * detect the operating system from the os.name System property a 16 | * 17 | * @returns - the operating system detected 18 | */ 19 | val operatingSystemType: OSType by lazy { 20 | 21 | val os = System 22 | .getProperty("os.name", "generic") 23 | .lowercase(Locale.ENGLISH) 24 | 25 | if (os.indexOf("mac") >= 0 || os.indexOf("darwin") >= 0) { 26 | OSType.MacOS 27 | } else if (os.indexOf("win") >= 0) { 28 | OSType.Windows 29 | } else if (os.indexOf("nux") >= 0) { 30 | OSType.Linux 31 | } else { 32 | OSType.Other 33 | } 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/ResDir.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | object ResDir { 4 | val dir by lazy { 5 | System.getProperty("compose.application.resources.dir") 6 | ?: System.getProperty("user.dir") 7 | ?: System.getProperty("user.home") 8 | } 9 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | object StringUtils { 4 | private val apostropheRegEx by lazy { 5 | "^\".*[']+.*\"\$".toRegex() 6 | } 7 | 8 | fun removeApostrophe(input: String): String { 9 | return if (apostropheRegEx.matches(input)) { 10 | input.substring(1, input.length - 1) 11 | } else { 12 | input 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/UnZip.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | 4 | import java.nio.file.Path 5 | import java.util.zip.ZipFile 6 | import kotlin.io.path.* 7 | 8 | fun Path.unzip( 9 | outputDir: Path = getDefaultOutputDir(this) 10 | ): Path { 11 | // Delete existing first 12 | outputDir.toFile().deleteRecursively() 13 | 14 | ZipFile(this.toFile()).use { zip -> 15 | zip.entries().asSequence().forEach { entry -> 16 | if (!entry.isDirectory) { 17 | zip.getInputStream(entry).use { input -> 18 | val outputFile = outputDir / entry.name 19 | 20 | with(outputFile) { 21 | if (!outputFile.parent.exists()) { 22 | parent.createDirectories() 23 | } 24 | } 25 | 26 | outputFile.createFile() 27 | outputFile.outputStream().use { output -> 28 | input.copyTo(output) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | return outputDir 36 | } 37 | 38 | private fun getDefaultOutputDir(inputZipPath: Path): Path { 39 | return inputZipPath.parent / inputZipPath.nameWithoutExtension 40 | } 41 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/calladapter/flow/FlowResourceCallAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util.calladapter.flow 2 | 3 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource.Error 4 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource.Success 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.catch 7 | import kotlinx.coroutines.flow.flow 8 | import retrofit2.Call 9 | import retrofit2.CallAdapter 10 | import retrofit2.awaitResponse 11 | import java.lang.reflect.Type 12 | 13 | /** 14 | * To convert retrofit response to Flow>. 15 | * Inspired from FlowCallAdapterFactory 16 | */ 17 | class FlowResourceCallAdapter( 18 | private val responseType: Type, 19 | private val isSelfExceptionHandling: Boolean 20 | ) : CallAdapter>> { 21 | 22 | override fun responseType() = responseType 23 | 24 | override fun adapt(call: Call) = flow> { 25 | 26 | // Firing loading resource 27 | emit(Resource.Loading()) 28 | 29 | val resp = call.awaitResponse() 30 | 31 | if (resp.isSuccessful) { 32 | resp.body()?.let { data -> 33 | // Success 34 | emit(Success(data)) 35 | } ?: kotlin.run { 36 | // Error 37 | emit(Error("Response can't be null", resp.code())) 38 | } 39 | } else { 40 | // Error 41 | val errorBody = resp.message() 42 | emit(Error(errorBody, resp.code())) 43 | } 44 | 45 | }.catch { error: Throwable -> 46 | if (isSelfExceptionHandling) { 47 | emit(Error(error.message ?: "Something went wrong", -1)) 48 | } else { 49 | throw error 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/calladapter/flow/FlowResourceCallAdapterFactory.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util.calladapter.flow 2 | 3 | 4 | import kotlinx.coroutines.flow.Flow 5 | import retrofit2.CallAdapter 6 | import retrofit2.CallAdapter.Factory 7 | import retrofit2.Retrofit 8 | import java.lang.reflect.ParameterizedType 9 | import java.lang.reflect.Type 10 | 11 | class FlowResourceCallAdapterFactory( 12 | private val isSelfExceptionHandling: Boolean = true 13 | ) : Factory() { 14 | override fun get( 15 | returnType: Type, 16 | annotations: Array, 17 | retrofit: Retrofit 18 | ): CallAdapter<*, *>? { 19 | if (getRawType(returnType) != Flow::class.java) { 20 | return null 21 | } 22 | val observableType = getParameterUpperBound(0, returnType as ParameterizedType) 23 | val rawObservableType = getRawType(observableType) 24 | require(rawObservableType == Resource::class.java) { "type must be a resource" } 25 | require(observableType is ParameterizedType) { "resource must be parameterized" } 26 | val bodyType = getParameterUpperBound(0, observableType) 27 | return FlowResourceCallAdapter( 28 | bodyType, 29 | isSelfExceptionHandling 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/data/util/calladapter/flow/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util.calladapter.flow 2 | 3 | /** 4 | * Created by theapache64 : Jul 26 Sun,2020 @ 13:22 5 | */ 6 | sealed class Resource { 7 | 8 | class Loading( 9 | val message: String? = null 10 | ) : Resource() 11 | 12 | data class Success( 13 | val data: T, 14 | val message: String? = null 15 | ) : Resource() 16 | 17 | data class Error( 18 | val errorData: String, 19 | val errorCode: Int? = null 20 | ) : Resource() 21 | } 22 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/di/Qualifiers.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @MustBeDocumented 7 | @Retention(AnnotationRetention.RUNTIME) 8 | annotation class ApkToolJarFile 9 | 10 | @Qualifier 11 | @MustBeDocumented 12 | @Retention(AnnotationRetention.RUNTIME) 13 | annotation class JadxDirPath 14 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/di/module/ApkToolModule.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di.module 2 | 3 | import com.theapache64.stackzy.data.util.ResDir 4 | import com.theapache64.stackzy.di.ApkToolJarFile 5 | import dagger.Module 6 | import dagger.Provides 7 | import java.io.File 8 | import java.io.IOException 9 | 10 | @Module 11 | class ApkToolModule { 12 | 13 | companion object { 14 | private val apkToolJar = File("${ResDir.dir}${File.separator}apk-tool.jar") 15 | private const val APKTOOL_JAR_NAME = "apktool_2.9.3.jar" 16 | } 17 | 18 | @Provides 19 | @ApkToolJarFile 20 | fun provideApkToolJarFile(): File { 21 | if (apkToolJar.exists().not()) { 22 | val apkToolStream = this::class.java.classLoader.getResourceAsStream(APKTOOL_JAR_NAME) 23 | if (apkToolStream != null) { 24 | apkToolJar.parentFile.let { parentDir -> 25 | if (parentDir.exists().not()) { 26 | parentDir.mkdirs() 27 | } 28 | } 29 | if (apkToolJar.createNewFile()) { 30 | apkToolJar.writeBytes(apkToolStream.readAllBytes()) 31 | } else { 32 | throw IOException("Failed to create ${apkToolJar.absolutePath}") 33 | } 34 | } else { 35 | throw IOException("Failed to parse apk-tool from resources") 36 | } 37 | } 38 | 39 | return apkToolJar 40 | } 41 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/di/module/CryptoModule.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di.module 2 | 3 | import com.theapache64.stackzy.util.Crypto 4 | import dagger.Module 5 | import dagger.Provides 6 | import javax.inject.Singleton 7 | 8 | @Module 9 | class CryptoModule { 10 | companion object { 11 | private val SALT = "tHeApAChe64Stack".toByteArray() 12 | } 13 | 14 | @Singleton 15 | @Provides 16 | fun provideCrypto(): Crypto { 17 | return Crypto(SALT) 18 | } 19 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/di/module/JadxModule.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di.module 2 | 3 | import com.theapache64.stackzy.data.util.ResDir 4 | import com.theapache64.stackzy.data.util.unzip 5 | import com.theapache64.stackzy.di.JadxDirPath 6 | import dagger.Module 7 | import dagger.Provides 8 | import java.io.IOException 9 | import java.nio.file.Path 10 | import kotlin.io.path.* 11 | 12 | @Module 13 | class JadxModule { 14 | 15 | companion object { 16 | private val jadxZipFile = Path(ResDir.dir) / "jadx-1.3.1.zip" 17 | private val jadxDirFile = Path(ResDir.dir) / "build" / jadxZipFile.nameWithoutExtension 18 | } 19 | 20 | @Provides 21 | @JadxDirPath 22 | fun provideJadXDirFile(): Path { 23 | if (jadxDirFile.exists().not()) { 24 | val jadxStream = this::class.java.classLoader.getResourceAsStream(jadxZipFile.name) 25 | if (jadxStream != null) { 26 | // Copying jadx zip to local folder 27 | jadxZipFile.parent.toFile().let { parentDir -> 28 | if (parentDir.exists().not()) { 29 | parentDir.mkdirs() 30 | } 31 | } 32 | jadxDirFile.parent.toFile().let { parentDir -> 33 | if (parentDir.exists().not()) { 34 | parentDir.mkdirs() 35 | } 36 | } 37 | jadxZipFile.writeBytes(jadxStream.readAllBytes()) 38 | jadxZipFile.unzip(jadxDirFile) 39 | jadxZipFile.deleteIfExists() 40 | } else { 41 | throw IOException("Failed to parse apk-tool from resources") 42 | } 43 | } 44 | 45 | 46 | return jadxDirFile 47 | } 48 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/di/module/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di.module 2 | 3 | import com.github.theapache64.retrosheet.RetrosheetInterceptor 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 6 | import com.theapache64.stackzy.data.remote.ApiInterface 7 | import com.theapache64.stackzy.data.util.calladapter.flow.FlowResourceCallAdapterFactory 8 | import com.toxicbakery.logging.Arbor 9 | import dagger.Module 10 | import dagger.Provides 11 | import okhttp3.OkHttpClient 12 | import retrofit2.Retrofit 13 | import retrofit2.converter.moshi.MoshiConverterFactory 14 | import javax.inject.Singleton 15 | 16 | 17 | @Module 18 | class NetworkModule { 19 | 20 | companion object { 21 | const val TABLE_CATEGORIES = "categories" 22 | const val TABLE_LIBRARIES = "libraries" 23 | const val TABLE_UNTRACKED_LIBS = "untracked_libs" 24 | const val TABLE_RESULTS = "results" 25 | const val TABLE_CONFIG = "config" 26 | const val TABLE_FUN_FACTS = "fun_facts" 27 | } 28 | 29 | @Singleton 30 | @Provides 31 | fun provideRetrosheetInterceptor(): RetrosheetInterceptor { 32 | return RetrosheetInterceptor.Builder() 33 | // .setLogging(true) 34 | .addSheet( 35 | sheetName = TABLE_CATEGORIES, 36 | "id", "name" 37 | ) 38 | .addSheet( 39 | sheetName = TABLE_FUN_FACTS, 40 | "id", "fun_fact" 41 | ) 42 | .addSheet( 43 | sheetName = TABLE_LIBRARIES, 44 | "id", "name", "package_name", "category", "website" 45 | ) 46 | .addSheet( 47 | sheetName = TABLE_UNTRACKED_LIBS, 48 | "created_at", "package_names" 49 | ) 50 | .addSheet( 51 | sheetName = TABLE_CONFIG, 52 | "should_consider_result_cache", 53 | "current_lib_version_code" 54 | ) 55 | .addForm( 56 | TABLE_UNTRACKED_LIBS, 57 | "https://docs.google.com/forms/d/e/1FAIpQLSdWuRkjXqBkL-w5NfktA_ju_sI2bJTDVb4LoYco4mxEpskU9g/viewform?usp=sf_link" 58 | ) 59 | .addSheet( 60 | sheetName = TABLE_RESULTS, 61 | "created_at", 62 | "app_name", 63 | "package_name", 64 | "version_name", 65 | "version_code", 66 | "stackzy_lib_version", 67 | "lib_packages", 68 | "platform", 69 | "apk_size_in_mb", 70 | "permissions", 71 | "gradle_info_json", 72 | "logo_image_url" 73 | ) 74 | .addForm( 75 | endPoint = TABLE_RESULTS, 76 | formLink = "https://docs.google.com/forms/d/e/1FAIpQLSdiTZz47N2FHUXLSvsdzAxVRKqzWq30xjkpCOQugKbHLJuRGg/viewform?usp=sf_link" 77 | ) 78 | .build() 79 | } 80 | 81 | @Singleton 82 | @Provides 83 | fun provideOkHttpClient(retrosheetInterceptor: RetrosheetInterceptor): OkHttpClient { 84 | return OkHttpClient.Builder() 85 | .addInterceptor(retrosheetInterceptor) 86 | .build() 87 | } 88 | 89 | @Singleton 90 | @Provides 91 | fun provideMoshi(): Moshi { 92 | return Moshi.Builder() 93 | .add(KotlinJsonAdapterFactory()) 94 | .build() 95 | } 96 | 97 | @Singleton 98 | @Provides 99 | fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit { 100 | 101 | return Retrofit.Builder() 102 | .client(okHttpClient) 103 | .baseUrl("https://docs.google.com/spreadsheets/d/1KBxVO5tXySbezBr-9rb2Y3qWo5PCMrvkD1aWQxZRepI/") 104 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 105 | .addCallAdapterFactory(FlowResourceCallAdapterFactory()) 106 | .build() 107 | } 108 | 109 | @Singleton 110 | @Provides 111 | fun provideApiInterface(retrofit: Retrofit): ApiInterface { 112 | Arbor.d("Creating new API interface") 113 | return retrofit.create(ApiInterface::class.java) 114 | } 115 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/theapache64/stackzy/di/module/PreferenceModule.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di.module 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import java.util.prefs.Preferences 6 | import javax.inject.Singleton 7 | 8 | @Module 9 | class PreferenceModule { 10 | 11 | @Singleton 12 | @Provides 13 | fun providePreference(): Preferences { 14 | return Preferences.userRoot().node("Stackzy") 15 | } 16 | } -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, 8 | socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | * Using welcoming and inclusive language 15 | * Being respectful of differing viewpoints and experiences 16 | * Gracefully accepting constructive criticism 17 | * Focusing on what is best for the community 18 | * Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | * Trolling, insulting/derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | * Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | theapache64@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed 48 | necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to 49 | the reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available 57 | at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 58 | 59 | [homepage]: https://www.contributor-covenant.org 60 | 61 | For answers to common questions about this code of conduct, see 62 | https://www.contributor-covenant.org/faq 63 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 🤝 Contributing 2 | 3 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any 4 | contributions you make are **greatly appreciated**. 5 | 6 | 1. Open an issue first to discuss what you would like to change. 7 | 1. Fork the Project 8 | 1. Create your feature branch (`git checkout -b feature/1/amazing-feature`) 9 | 1. Commit your changes (`git commit -m 'Add some amazing feature'`) 10 | 1. Push to the branch (`git push origin feature/1/amazing-feature`) 11 | 1. Open a pull request 12 | 13 | (Here, `1` = issue number) 14 | 15 | Please make sure to update tests as appropriate. -------------------------------------------------------------------------------- /docs/USAGE.md: -------------------------------------------------------------------------------- 1 | # 📽️ Usage 2 | 3 | **1. Select pathway** 4 | 5 | ![](../extras/pathway.png) 6 | 7 | **2. Select App** 8 | 9 | ![](../extras/select_app.png) 10 | 11 | **3. Enjoy result ;)** 12 | 13 | ![](../extras/libs.png) 14 | 15 | ![](../extras/meta.png) -------------------------------------------------------------------------------- /docs/algorithms/browse_code_algo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/docs/algorithms/browse_code_algo.png -------------------------------------------------------------------------------- /docs/algorithms/result_cache_algo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/docs/algorithms/result_cache_algo.png -------------------------------------------------------------------------------- /extras/cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/extras/cover.jpeg -------------------------------------------------------------------------------- /extras/libs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/extras/libs.png -------------------------------------------------------------------------------- /extras/meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/extras/meta.png -------------------------------------------------------------------------------- /extras/pathway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/extras/pathway.png -------------------------------------------------------------------------------- /extras/select_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/extras/select_app.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | maven { url = uri("https://jitpack.io") } 6 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 7 | } 8 | 9 | } 10 | 11 | rootProject.name = "stackzy" 12 | include("data") 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/App.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy 2 | 3 | import com.bugsnag.Bugsnag 4 | import com.theapache64.cyclone.core.Application 5 | import com.theapache64.stackzy.data.local.AppArgs 6 | import com.theapache64.stackzy.ui.feature.MainActivity 7 | import com.toxicbakery.logging.Arbor 8 | import com.toxicbakery.logging.Seedling 9 | 10 | 11 | val bugsnag = Bugsnag("c98f629ae7f3f708bc9ce9bf5d6b8342") 12 | 13 | class App( 14 | appArgs: AppArgs 15 | ) : Application() { 16 | 17 | companion object { 18 | // GLOBAL CONFIGS 19 | lateinit var appArgs: AppArgs 20 | } 21 | 22 | init { 23 | App.appArgs = appArgs 24 | } 25 | 26 | override fun onCreate() { 27 | super.onCreate() 28 | Arbor.sow(Seedling()) 29 | 30 | val splashIntent = MainActivity.getStartIntent() 31 | startActivity(splashIntent) 32 | } 33 | } 34 | 35 | /** 36 | * The magic begins here 37 | */ 38 | fun main() { 39 | // Parsing application arguments 40 | val appArgs = AppArgs( 41 | appName = "Stackzy", 42 | version = "v1.2.8", // TODO: Change below date also 43 | versionCode = 20250119 44 | ) 45 | 46 | // Passing args 47 | App(appArgs).onCreate() 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.di 2 | 3 | import com.theapache64.stackzy.data.repo.AdbRepo 4 | import com.theapache64.stackzy.di.module.* 5 | import com.theapache64.stackzy.ui.feature.appdetail.AppDetailScreenComponent 6 | import com.theapache64.stackzy.ui.feature.applist.AppListScreenComponent 7 | import com.theapache64.stackzy.ui.feature.devicelist.DeviceListScreenComponent 8 | import com.theapache64.stackzy.ui.feature.libdetail.LibraryDetailScreenComponent 9 | import com.theapache64.stackzy.ui.feature.liblist.LibraryListScreenComponent 10 | import com.theapache64.stackzy.ui.feature.login.LogInScreenComponent 11 | import com.theapache64.stackzy.ui.feature.pathway.PathwayScreenComponent 12 | import com.theapache64.stackzy.ui.feature.splash.SplashScreenComponent 13 | import com.theapache64.stackzy.ui.feature.update.UpdateScreenComponent 14 | import dagger.Component 15 | import javax.inject.Singleton 16 | 17 | @Singleton 18 | @Component( 19 | modules = [ 20 | NetworkModule::class, 21 | ApkToolModule::class, 22 | PreferenceModule::class, 23 | CryptoModule::class, 24 | JadxModule::class, 25 | ] 26 | ) 27 | interface AppComponent { 28 | fun inject(splashScreenComponent: SplashScreenComponent) 29 | fun inject(selectPathwayScreenComponent: PathwayScreenComponent) 30 | fun inject(logInScreenComponent: LogInScreenComponent) 31 | fun inject(appListScreenComponent: AppListScreenComponent) 32 | fun inject(appDetailScreenComponent: AppDetailScreenComponent) 33 | fun inject(deviceListScreenComponent: DeviceListScreenComponent) 34 | fun inject(updateScreenComponent: UpdateScreenComponent) 35 | fun inject(libraryListScreenComponent: LibraryListScreenComponent) 36 | fun inject(libraryDetailScreenComponent: LibraryDetailScreenComponent) 37 | 38 | // bind repo to this component 39 | fun bind(): AdbRepo 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/model/AlphabetCircle.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.model 2 | 3 | import androidx.compose.ui.graphics.Brush 4 | import com.theapache64.stackzy.data.local.BaseAlphabetCircle 5 | import com.theapache64.stackzy.util.ColorUtil 6 | 7 | abstract class AlphabetCircle : BaseAlphabetCircle() { 8 | 9 | private val randomColor = ColorUtil.getRandomColor() 10 | private val brighterColor = ColorUtil.getBrightenedColor(randomColor) 11 | private val bgColor = Brush.horizontalGradient( 12 | colors = listOf(randomColor, brighterColor) 13 | ) 14 | 15 | fun getGradientColor(): Brush = bgColor 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/model/AnalysisReportWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.model 2 | 3 | import com.theapache64.stackzy.data.local.AnalysisReport 4 | import com.theapache64.stackzy.data.local.AnalysisReportDefinition 5 | 6 | class AnalysisReportWrapper( 7 | private val report: AnalysisReport, 8 | val libraryWrappers: List 9 | ) : AnalysisReportDefinition by report -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/model/AndroidAppWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.model 2 | 3 | import com.theapache64.stackzy.data.local.AndroidApp 4 | import com.theapache64.stackzy.data.local.AndroidAppDefinition 5 | import java.util.* 6 | 7 | class AndroidAppWrapper( 8 | val androidApp: AndroidApp, 9 | val shouldUseVersionNameAsSubTitle: Boolean = false 10 | ) : AndroidAppDefinition by androidApp, AlphabetCircle() { 11 | companion object { 12 | /** 13 | * Remove these keywords when to GUESS app name from package name 14 | */ 15 | private val titleNegRegEx = "(\\.app|\\.android|\\.beta|\\.com)".toRegex() 16 | } 17 | 18 | override fun getTitle(): String { 19 | return appTitle ?: appPackage.name 20 | .replace(titleNegRegEx, "") 21 | .split(".").last() 22 | .replaceFirstChar { 23 | if (it.isLowerCase()) { 24 | it.titlecase(Locale.US) 25 | } else { 26 | it.toString() 27 | } 28 | } 29 | } 30 | 31 | override fun getSubtitle(): String { 32 | return appPackage.name 33 | } 34 | 35 | override fun getSubtitle2(): String? { 36 | return if (shouldUseVersionNameAsSubTitle) { 37 | "v${versionName}" 38 | } else { 39 | appSize 40 | } 41 | } 42 | 43 | override fun imageUrl(): String? = imageUrl 44 | 45 | override fun getAlphabet(): Char { 46 | return getTitle().first() 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/model/AndroidDeviceWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.model 2 | 3 | import com.theapache64.stackzy.data.local.AndroidDevice 4 | import com.theapache64.stackzy.data.local.AndroidDeviceDefinition 5 | 6 | class AndroidDeviceWrapper( 7 | val androidDevice: AndroidDevice 8 | ) : AndroidDeviceDefinition by androidDevice, AlphabetCircle() { 9 | 10 | override fun getTitle() = model 11 | override fun getSubtitle() = name 12 | override fun imageUrl(): String? = null 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/model/LibraryWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.model 2 | 3 | import com.theapache64.stackzy.data.remote.Library 4 | import com.theapache64.stackzy.data.remote.LibraryDefinition 5 | import com.theapache64.stackzy.data.remote.Result 6 | 7 | class LibraryWrapper( 8 | private val library: Library, 9 | private val prevResult: Result? 10 | ) : AlphabetCircle(), LibraryDefinition by library { 11 | 12 | private val isNewLib: Boolean by lazy { 13 | if (prevResult != null) { 14 | // If the prev result has this library inside the libPackage, then it is not a new lib. 15 | prevResult.libPackages?.contains(library.packageName) == false 16 | } else { 17 | false 18 | } 19 | } 20 | 21 | override fun getTitle(): String { 22 | return name 23 | } 24 | 25 | override fun getSubtitle(): String { 26 | return category 27 | } 28 | 29 | override fun imageUrl(): String? = null 30 | 31 | override fun isNew(): Boolean { 32 | return isNewLib 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/AlphabetCircle.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.scale 14 | import androidx.compose.ui.graphics.Brush 15 | import androidx.compose.ui.text.font.FontWeight 16 | 17 | private const val DEFAULT_SCALE = 1f 18 | private const val ANIMATED_SCALE = 1.2f 19 | 20 | @Composable 21 | fun AlphabetCircle( 22 | character: Char, 23 | color: Brush, 24 | modifier: Modifier = Modifier, 25 | isNew: Boolean = false 26 | ) { 27 | val currentScale = if (isNew) { 28 | // If the library is new, we'll show a zoom-in/zoom-out animation 29 | var currentScaleState by remember { mutableStateOf(DEFAULT_SCALE) } 30 | 31 | LaunchedEffect(Unit) { 32 | if (isNew) { 33 | currentScaleState = ANIMATED_SCALE 34 | } 35 | } 36 | animateFloatAsState( 37 | currentScaleState, 38 | animationSpec = tween( 39 | durationMillis = 500 40 | ), 41 | finishedListener = { 42 | currentScaleState = DEFAULT_SCALE 43 | } 44 | ).value 45 | } else { 46 | DEFAULT_SCALE 47 | } 48 | 49 | Box( 50 | modifier = modifier 51 | .scale(currentScale) 52 | .background(color, CircleShape), 53 | contentAlignment = Alignment.Center 54 | ) { 55 | Text( 56 | text = character.uppercaseChar().toString(), 57 | style = MaterialTheme.typography.h5.copy(fontWeight = FontWeight.Bold) 58 | ) 59 | } 60 | 61 | } 62 | 63 | /* 64 | fun main(args: Array) = singleWindowApplication { 65 | StackzyTheme { 66 | Box( 67 | modifier = Modifier.fillMaxSize(), 68 | contentAlignment = Alignment.Center 69 | ) { 70 | AlphabetCircle( 71 | character = 'C', 72 | color = Brush.linearGradient(colors = listOf(Color.Red, Color.Green)), 73 | isNew = true, 74 | modifier = Modifier.size(60.dp) 75 | ) 76 | } 77 | } 78 | }*/ 79 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/Badge.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun Badge( 15 | title: String, 16 | padding: Dp = 5.dp, 17 | modifier: Modifier = Modifier 18 | ) { 19 | Text( 20 | text = title, 21 | modifier = modifier 22 | .background(MaterialTheme.colors.secondary, RoundedCornerShape(5.dp)) 23 | .padding(padding), 24 | style = MaterialTheme.typography.caption, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/BottomGradient.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/CenterBox.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | /** 11 | * Only used for debugging/preview purpose 12 | */ 13 | @Composable 14 | fun CenterBox( 15 | content: @Composable BoxScope.() -> Unit 16 | ) { 17 | Box( 18 | modifier = Modifier.fillMaxSize(), 19 | contentAlignment = Alignment.Center 20 | ) { 21 | content() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/CustomScaffold.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.Icon 6 | import androidx.compose.material.IconButton 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Text 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.ChevronLeft 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Brush 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import com.theapache64.stackzy.util.R 18 | 19 | /** 20 | * To show a basic content page 21 | */ 22 | const val CONTENT_PADDING_VERTICAL = 15 23 | const val CONTENT_PADDING_HORIZONTAL = 15 24 | 25 | @Composable 26 | fun CustomScaffold( 27 | title: String, 28 | subTitle: String? = null, 29 | modifier: Modifier = Modifier, 30 | onBackClicked: (() -> Unit)? = null, 31 | topRightSlot: (@Composable () -> Unit)? = null, 32 | bottomGradient: Boolean = false, 33 | content: @Composable BoxScope.() -> Unit 34 | ) { 35 | Column( 36 | modifier = modifier 37 | .fillMaxSize() 38 | .padding( 39 | horizontal = CONTENT_PADDING_HORIZONTAL.dp, 40 | vertical = CONTENT_PADDING_VERTICAL.dp, 41 | ) 42 | ) { 43 | 44 | // Header 45 | Row( 46 | modifier = Modifier 47 | .height(60.dp), 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | 51 | // Back button 52 | if (onBackClicked != null) { 53 | IconButton( 54 | onClick = onBackClicked, 55 | ) { 56 | Icon( 57 | imageVector = Icons.Outlined.ChevronLeft, 58 | contentDescription = R.string.select_app_cd_go_back 59 | ) 60 | } 61 | } 62 | 63 | // Title and Subtitle 64 | Column { 65 | Text( 66 | text = title, 67 | style = MaterialTheme.typography.h5 68 | ) 69 | 70 | if (subTitle != null) { 71 | Text( 72 | text = subTitle, 73 | style = MaterialTheme.typography.body2, 74 | color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) 75 | ) 76 | } 77 | } 78 | 79 | // Right slot (Search, Icons etc) 80 | if (topRightSlot != null) { 81 | Box( 82 | modifier = Modifier.fillMaxWidth(), 83 | contentAlignment = Alignment.CenterEnd 84 | ) { 85 | topRightSlot() 86 | } 87 | } 88 | } 89 | 90 | Spacer( 91 | modifier = Modifier.height(10.dp) 92 | ) 93 | 94 | Box { 95 | // Content slot 96 | content() 97 | 98 | if (bottomGradient) { 99 | BottomGradient() 100 | } 101 | } 102 | 103 | } 104 | 105 | } 106 | 107 | val BOTTOM_GRADIENT_HEIGHT = 50.dp 108 | 109 | @Composable 110 | fun BoxScope.BottomGradient() { 111 | // Bottom gradient 112 | Box( 113 | modifier = Modifier 114 | .fillMaxWidth() 115 | .height(BOTTOM_GRADIENT_HEIGHT) 116 | .align(Alignment.BottomCenter) 117 | .background( 118 | brush = Brush.verticalGradient( 119 | colors = listOf( 120 | Color.Transparent, 121 | com.theapache64.stackzy.ui.theme.R.color.BigStone 122 | ) 123 | ) 124 | ) 125 | ) 126 | } 127 | 128 | @Composable 129 | fun GradientMargin() { 130 | Spacer( 131 | modifier = Modifier 132 | .height(BOTTOM_GRADIENT_HEIGHT) 133 | ) 134 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/ErrorSnackBar.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Snackbar 7 | import androidx.compose.material.Text 8 | import androidx.compose.material.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import com.theapache64.stackzy.util.R 14 | 15 | /** 16 | * To show an error SnackBar with retry button at bottom of the screen. 17 | */ 18 | @Composable 19 | fun BoxScope.ErrorSnackBar( 20 | syncFailedReason: String, 21 | onRetryClicked: (() -> Unit)? = null, 22 | ) { 23 | Snackbar( 24 | modifier = Modifier 25 | .padding(10.dp) 26 | .align(Alignment.BottomCenter), 27 | backgroundColor = MaterialTheme.colors.surface, 28 | contentColor = MaterialTheme.colors.onSurface, 29 | action = { 30 | if (onRetryClicked != null) { 31 | TextButton( 32 | onClick = onRetryClicked 33 | ) { 34 | Text(R.string.all_action_retry) 35 | } 36 | } 37 | } 38 | ) { 39 | Text(text = syncFailedReason) 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/FullScreenError.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.painter.Painter 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.unit.dp 13 | 14 | /** 15 | * To show full screen error with centered title and message 16 | */ 17 | @Composable 18 | fun FullScreenError( 19 | title: String, 20 | message: String, 21 | image: Painter? = null, 22 | action: (@Composable () -> Unit)? = null, 23 | modifier: Modifier = Modifier 24 | ) { 25 | Column( 26 | modifier = modifier 27 | .fillMaxSize(), 28 | verticalArrangement = Arrangement.Center, 29 | horizontalAlignment = Alignment.CenterHorizontally 30 | ) { 31 | if (image != null) { 32 | // Image 33 | Image( 34 | painter = image, 35 | modifier = Modifier.width(300.dp), 36 | contentDescription = "" 37 | ) 38 | } 39 | 40 | /*Space*/ 41 | Spacer( 42 | modifier = Modifier.height(30.dp) 43 | ) 44 | 45 | /*Title*/ 46 | Text( 47 | text = title, 48 | style = MaterialTheme.typography.h4 49 | ) 50 | 51 | /*Space*/ 52 | Spacer( 53 | modifier = Modifier.height(10.dp) 54 | ) 55 | 56 | /*Message*/ 57 | Text( 58 | text = message, 59 | textAlign = TextAlign.Center, 60 | style = MaterialTheme.typography.body2, 61 | color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) 62 | ) 63 | 64 | if (action != null) { 65 | /*Space*/ 66 | Spacer( 67 | modifier = Modifier.height(10.dp) 68 | ) 69 | 70 | action() 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/LoadingText.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.alpha 9 | 10 | @Composable 11 | fun LoadingText( 12 | message: String, 13 | modifier: Modifier = Modifier 14 | ) { 15 | var enabled by remember { mutableStateOf(true) } 16 | 17 | val alpha = if (enabled) { 18 | 1f 19 | } else { 20 | 0.2f 21 | } 22 | 23 | val animatedAlpha by animateFloatAsState( 24 | targetValue = alpha, 25 | animationSpec = tween(200), 26 | finishedListener = { 27 | enabled = !enabled 28 | } 29 | ) 30 | 31 | Text( 32 | text = message, 33 | modifier = modifier.alpha(animatedAlpha) 34 | ) 35 | 36 | LaunchedEffect(Unit) { 37 | enabled = !enabled 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/Logo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.ColorFilter 8 | import androidx.compose.ui.res.painterResource 9 | import com.theapache64.stackzy.util.R 10 | 11 | @Composable 12 | fun Logo( 13 | modifier: Modifier = Modifier 14 | ) { 15 | 16 | Image( 17 | contentDescription = R.string.logo, 18 | painter = painterResource("drawables/logo.svg"), 19 | colorFilter = ColorFilter.tint(MaterialTheme.colors.primary), 20 | modifier = modifier, 21 | ) 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/loading/LoadingAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common.loading 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.rotate 12 | import androidx.compose.ui.graphics.ColorFilter 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.unit.dp 15 | import com.theapache64.stackzy.data.remote.FunFact 16 | import com.theapache64.stackzy.ui.common.LoadingText 17 | import com.theapache64.stackzy.ui.common.loading.funfact.FunFact 18 | 19 | /** 20 | * To show a rotating icon at the center and blinking text at the bottom of the screen 21 | */ 22 | @Composable 23 | fun LoadingAnimation( 24 | message: String, 25 | funFacts: Set? 26 | ) { 27 | 28 | var isRotated by remember { mutableStateOf(false) } 29 | 30 | val targetRotation = if (isRotated) { 31 | 0f 32 | } else { 33 | 90f 34 | } 35 | 36 | val animatedRotation by animateFloatAsState( 37 | targetValue = targetRotation, 38 | animationSpec = tween(200), 39 | finishedListener = { 40 | isRotated = !isRotated 41 | } 42 | ) 43 | 44 | 45 | Box( 46 | modifier = Modifier.fillMaxSize() 47 | ) { 48 | Column( 49 | modifier = Modifier.align(Alignment.Center), 50 | ) { 51 | Image( 52 | modifier = Modifier 53 | .rotate(animatedRotation) 54 | .align(Alignment.CenterHorizontally) 55 | .size(50.dp), 56 | colorFilter = ColorFilter.tint(MaterialTheme.colors.primary), 57 | painter = painterResource("drawables/loading.png"), 58 | contentDescription = "" 59 | ) 60 | 61 | if (funFacts != null) { 62 | Spacer(modifier = Modifier.height(15.dp)) 63 | FunFact(funFacts) 64 | } 65 | } 66 | 67 | LoadingText( 68 | modifier = Modifier.align(Alignment.BottomCenter), 69 | message = message 70 | ) 71 | } 72 | 73 | LaunchedEffect(Unit) { 74 | // Ignite the animation 75 | isRotated = !isRotated 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/loading/funfact/FunFact.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common.loading.funfact 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.font.FontStyle 11 | import androidx.compose.ui.unit.sp 12 | import androidx.compose.ui.window.singleWindowApplication 13 | import com.theapache64.stackzy.data.remote.FunFact 14 | import com.theapache64.stackzy.ui.common.CenterBox 15 | import com.theapache64.stackzy.util.PureRandom 16 | 17 | /** 18 | * App level config flag 19 | */ 20 | private var isClicked = false 21 | 22 | @Composable 23 | fun FunFact( 24 | funFacts: Set, 25 | modifier: Modifier = Modifier 26 | ) { 27 | val pureRandom = remember { PureRandom(funFacts) } 28 | var currentFunFact by remember { mutableStateOf(pureRandom.get()) } 29 | 30 | Column( 31 | horizontalAlignment = Alignment.CenterHorizontally, 32 | modifier = modifier.clickable { 33 | isClicked = true 34 | currentFunFact = pureRandom.get() 35 | } 36 | ) { 37 | 38 | Text( 39 | text = "\"${currentFunFact.funFact}\"", 40 | fontStyle = FontStyle.Italic, 41 | color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f), 42 | fontSize = 15.sp 43 | ) 44 | 45 | if (!isClicked) { 46 | Text( 47 | text = "Click to get more facts", 48 | color = MaterialTheme.colors.onSurface.copy(alpha = 0.3f), 49 | fontSize = 13.sp, 50 | ) 51 | } 52 | 53 | } 54 | } 55 | 56 | 57 | fun main(args: Array) = singleWindowApplication { 58 | CenterBox { 59 | FunFact( 60 | setOf( 61 | FunFact(id = 1, "A"), 62 | FunFact(id = 2, "B"), 63 | FunFact(id = 3, "C"), 64 | FunFact(id = 4, "D"), 65 | FunFact(id = 5, "E"), 66 | ) 67 | ) 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/common/loading/funfact/FunFactViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.common.loading.funfact 2 | 3 | class FunFactViewModel { 4 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature 2 | 3 | import androidx.compose.ui.res.painterResource 4 | import androidx.compose.ui.unit.dp 5 | import androidx.compose.ui.window.application 6 | import androidx.compose.ui.window.rememberWindowState 7 | import com.arkivanov.decompose.DefaultComponentContext 8 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 9 | import com.theapache64.cyclone.core.Activity 10 | import com.theapache64.cyclone.core.Intent 11 | import com.theapache64.stackzy.App 12 | import com.theapache64.stackzy.ui.navigation.NavHostComponent 13 | import com.theapache64.stackzy.ui.theme.R 14 | import com.theapache64.stackzy.ui.theme.StackzyTheme 15 | import java.awt.Taskbar 16 | import java.awt.image.BufferedImage 17 | import javax.imageio.ImageIO 18 | import androidx.compose.ui.window.Window as setContent 19 | 20 | 21 | class MainActivity : Activity() { 22 | companion object { 23 | fun getStartIntent(): Intent { 24 | return Intent(MainActivity::class).apply { 25 | // data goes here 26 | } 27 | } 28 | } 29 | 30 | override fun onCreate() { 31 | super.onCreate() 32 | try { 33 | /* 34 | *TODO : Temp fix for https://github.com/theapache64/stackzy/issues/72 35 | * Should be updated once resolved : 36 | */ 37 | Taskbar.getTaskbar().iconImage = getAppIcon() 38 | } catch (e: UnsupportedOperationException) { 39 | e.printStackTrace() 40 | } 41 | 42 | val lifecycle = LifecycleRegistry() 43 | val root = NavHostComponent(DefaultComponentContext(lifecycle)) 44 | 45 | application { 46 | setContent( 47 | onCloseRequest = ::exitApplication, 48 | title = "${App.appArgs.appName} (${App.appArgs.version})", 49 | icon = painterResource(R.drawables.appIcon), 50 | state = rememberWindowState( 51 | width = 1224.dp, 52 | height = 800.dp 53 | ), 54 | ) { 55 | StackzyTheme { 56 | // Igniting navigation 57 | root.render() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | 65 | /** 66 | * To get app icon for toolbar and system tray 67 | */ 68 | private fun getAppIcon(): BufferedImage { 69 | 70 | // Retrieving image 71 | val resourceFile = MainActivity::class.java.classLoader.getResourceAsStream(R.drawables.appIcon) 72 | val imageInput = ImageIO.read(resourceFile) 73 | 74 | val newImage = BufferedImage( 75 | imageInput.width, 76 | imageInput.height, 77 | BufferedImage.TYPE_INT_ARGB 78 | ) 79 | 80 | // Drawing 81 | val canvas = newImage.createGraphics() 82 | canvas.drawImage(imageInput, 0, 0, null) 83 | canvas.dispose() 84 | 85 | return newImage 86 | } 87 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/appdetail/AppDetailScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.appdetail 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.theapache64.stackzy.di.AppComponent 8 | import com.theapache64.stackzy.model.AndroidAppWrapper 9 | import com.theapache64.stackzy.model.LibraryWrapper 10 | import com.theapache64.stackzy.ui.navigation.Component 11 | import com.theapache64.stackzy.util.ApkSource 12 | import javax.inject.Inject 13 | 14 | class AppDetailScreenComponent( 15 | appComponent: AppComponent, 16 | componentContext: ComponentContext, 17 | private val selectedApp: AndroidAppWrapper, 18 | private val apkSource: ApkSource, 19 | val onLibrarySelected: (LibraryWrapper) -> Unit, 20 | private val onBackClicked: () -> Unit 21 | ) : Component, ComponentContext by componentContext { 22 | 23 | @Inject 24 | lateinit var appDetailViewModel: AppDetailViewModel 25 | 26 | init { 27 | appComponent.inject(this) 28 | } 29 | 30 | @Composable 31 | override fun render() { 32 | 33 | val scope = rememberCoroutineScope() 34 | LaunchedEffect(appDetailViewModel) { 35 | appDetailViewModel.init( 36 | scope = scope, 37 | apkSource = apkSource, 38 | androidApp = selectedApp, 39 | ) 40 | appDetailViewModel.startDecompile() 41 | } 42 | 43 | AppDetailScreen( 44 | viewModel = appDetailViewModel, 45 | onLibrarySelected = onLibrarySelected, 46 | onBackClicked = onBackClicked 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/appdetail/Libraries.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.appdetail 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.lazy.grid.GridCells 7 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 8 | import androidx.compose.foundation.lazy.grid.items 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.unit.dp 13 | import com.theapache64.stackzy.data.local.Platform 14 | import com.theapache64.stackzy.model.AnalysisReportWrapper 15 | import com.theapache64.stackzy.model.LibraryWrapper 16 | import com.theapache64.stackzy.ui.common.FullScreenError 17 | import com.theapache64.stackzy.ui.common.GradientMargin 18 | import com.theapache64.stackzy.ui.common.Selectable 19 | 20 | 21 | @Composable 22 | fun Libraries( 23 | report: AnalysisReportWrapper, 24 | onLibrarySelected: (LibraryWrapper) -> Unit 25 | ) { 26 | 27 | if (report.libraries.isEmpty()) { 28 | // No libraries found 29 | val platform = report.platform 30 | if (platform is Platform.NativeKotlin || platform is Platform.NativeJava) { 31 | // native platform with libs 32 | FullScreenError( 33 | title = "We couldn't find any libraries", 34 | message = "But don't worry, we're improving our dictionary strength. Please try later", 35 | image = painterResource("drawables/guy.png") 36 | ) 37 | } else { 38 | // non native platform with no libs 39 | FullScreenError( 40 | title = "// TODO : ", 41 | message = "${report.platform.name} dependency analysis is not yet supported", 42 | image = painterResource("drawables/ic_error_code.png") 43 | ) 44 | } 45 | } else { 46 | 47 | LazyVerticalGrid( 48 | columns = GridCells.Fixed(4) 49 | ) { 50 | items( 51 | items = report.libraryWrappers 52 | ) { app -> 53 | Column { 54 | // GridItem 55 | Selectable( 56 | data = app, 57 | onSelected = onLibrarySelected 58 | ) 59 | 60 | Spacer( 61 | modifier = Modifier.height(10.dp) 62 | ) 63 | } 64 | } 65 | 66 | item { 67 | // Gradient margin 68 | GradientMargin() 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/applist/AppListScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.applist 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.theapache64.stackzy.di.AppComponent 8 | import com.theapache64.stackzy.model.AndroidAppWrapper 9 | import com.theapache64.stackzy.ui.navigation.Component 10 | import com.theapache64.stackzy.util.ApkSource 11 | import javax.inject.Inject 12 | 13 | class AppListScreenComponent( 14 | componentContext: ComponentContext, 15 | appComponent: AppComponent, 16 | private val apkSource: ApkSource, 17 | val onAppSelected: (ApkSource, AndroidAppWrapper) -> Unit, 18 | val onBackClicked: () -> Unit, 19 | val onLogInNeeded: (shouldGoToPlayStore: Boolean) -> Unit 20 | ) : Component, ComponentContext by componentContext { 21 | 22 | @Inject 23 | lateinit var appListViewModel: AppListViewModel 24 | 25 | init { 26 | 27 | appComponent.inject(this) 28 | } 29 | 30 | @Composable 31 | override fun render() { 32 | val scope = rememberCoroutineScope() 33 | LaunchedEffect(appListViewModel) { 34 | 35 | appListViewModel.init(scope, apkSource, onLogInNeeded) 36 | if (appListViewModel.apps.value == null) { 37 | appListViewModel.loadApps() 38 | } 39 | } 40 | 41 | SelectAppScreen( 42 | appListViewModel = appListViewModel, 43 | onBackClicked = onBackClicked, 44 | onAppSelected = { app -> 45 | onAppSelected(apkSource, app) 46 | } 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/devicelist/DeviceListScreen.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.devicelist 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.items 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.unit.dp 12 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 13 | import com.theapache64.stackzy.model.AndroidDeviceWrapper 14 | import com.theapache64.stackzy.ui.common.CustomScaffold 15 | import com.theapache64.stackzy.ui.common.ErrorSnackBar 16 | import com.theapache64.stackzy.ui.common.FullScreenError 17 | import com.theapache64.stackzy.ui.common.Selectable 18 | import com.theapache64.stackzy.ui.common.loading.LoadingAnimation 19 | import com.theapache64.stackzy.util.R 20 | 21 | /** 22 | * To select a device from connected devices 23 | */ 24 | @Composable 25 | fun SelectDeviceScreen( 26 | deviceListViewModel: DeviceListViewModel, 27 | onBackClicked: () -> Unit, 28 | onDeviceSelected: (AndroidDeviceWrapper) -> Unit 29 | ) { 30 | val devices by deviceListViewModel.connectedDevices.collectAsState() 31 | 32 | Content( 33 | devicesResp = devices, 34 | onDeviceSelected = onDeviceSelected, 35 | onBackClicked = onBackClicked, 36 | onRetry = { deviceListViewModel.watchConnectedDevices() } 37 | ) 38 | } 39 | 40 | @Composable 41 | fun Content( 42 | devicesResp: Resource>?, 43 | onDeviceSelected: (AndroidDeviceWrapper) -> Unit, 44 | onBackClicked: () -> Unit, 45 | onRetry: () -> Unit 46 | ) { 47 | if (devicesResp == null) { 48 | // Just background 49 | Box( 50 | modifier = Modifier.fillMaxSize() 51 | ) 52 | return 53 | } 54 | 55 | // Content 56 | CustomScaffold( 57 | title = R.string.device_select_the_device, 58 | onBackClicked = onBackClicked 59 | ) { 60 | 61 | when (devicesResp) { 62 | is Resource.Loading -> { 63 | LoadingAnimation(message = devicesResp.message ?: "", null) 64 | } 65 | 66 | is Resource.Error -> { 67 | ErrorSnackBar(syncFailedReason = devicesResp.errorData, onRetryClicked = onRetry) 68 | } 69 | 70 | is Resource.Success -> { 71 | val devices = devicesResp.data 72 | if (devices.isEmpty()) { 73 | FullScreenError( 74 | title = R.string.device_no_device_title, 75 | message = R.string.device_no_device_message, 76 | image = painterResource("drawables/no_device.png") 77 | ) 78 | } else { 79 | 80 | Spacer( 81 | modifier = Modifier.height(10.dp) 82 | ) 83 | 84 | LazyColumn { 85 | items(devices) { device -> 86 | Selectable( 87 | data = device, 88 | modifier = Modifier 89 | .width(400.dp), 90 | onSelected = onDeviceSelected 91 | ) 92 | 93 | Spacer( 94 | modifier = Modifier.height(10.dp) 95 | ) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/devicelist/DeviceListScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.devicelist 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.theapache64.stackzy.di.AppComponent 8 | import com.theapache64.stackzy.model.AndroidDeviceWrapper 9 | import com.theapache64.stackzy.ui.navigation.Component 10 | import com.toxicbakery.logging.Arbor 11 | import javax.inject.Inject 12 | 13 | class DeviceListScreenComponent( 14 | appComponent: AppComponent, 15 | private val componentContext: ComponentContext, 16 | private val onDeviceSelected: (AndroidDeviceWrapper) -> Unit, 17 | private val onBackClicked: () -> Unit 18 | ) : Component, ComponentContext by componentContext { 19 | 20 | @Inject 21 | lateinit var deviceListViewModel: DeviceListViewModel 22 | 23 | init { 24 | appComponent.inject(this) 25 | } 26 | 27 | @Composable 28 | override fun render() { 29 | 30 | val scope = rememberCoroutineScope() 31 | DisposableEffect(deviceListViewModel) { 32 | Arbor.d("Init scope") 33 | deviceListViewModel.init(scope) 34 | deviceListViewModel.watchConnectedDevices() 35 | onDispose { 36 | Arbor.d("Dispose scope") 37 | deviceListViewModel.stopWatchConnectedDevices() 38 | } 39 | } 40 | 41 | SelectDeviceScreen( 42 | deviceListViewModel, 43 | onBackClicked = onBackClicked, 44 | onDeviceSelected = { 45 | onDeviceSelected(it) 46 | } 47 | ) 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/devicelist/DeviceListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.devicelist 2 | 3 | import com.theapache64.stackzy.data.repo.AdbRepo 4 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 5 | import com.theapache64.stackzy.model.AndroidDeviceWrapper 6 | import com.toxicbakery.logging.Arbor 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | import kotlinx.coroutines.flow.catch 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | class DeviceListViewModel @Inject constructor( 15 | private val adbRepo: AdbRepo 16 | ) { 17 | 18 | private lateinit var viewModelScope: CoroutineScope 19 | private val _connectedDevices = MutableStateFlow>?>(null) 20 | val connectedDevices: StateFlow>?> = _connectedDevices 21 | 22 | 23 | fun init(scope: CoroutineScope) { 24 | this.viewModelScope = scope 25 | } 26 | 27 | 28 | /** 29 | * To start watching connected devices 30 | */ 31 | fun watchConnectedDevices() { 32 | viewModelScope.launch { 33 | _connectedDevices.value = Resource.Loading("🔍 Scanning for devices...") 34 | adbRepo.watchConnectedDevice() 35 | .catch { 36 | Arbor.d("Error: ${it.message}") 37 | _connectedDevices.value = Resource.Error(it.message ?: "Something went wrong") 38 | } 39 | .collect { 40 | Arbor.d("Devices : $it") 41 | _connectedDevices.value = Resource.Success(it.map { device -> AndroidDeviceWrapper(device) }) 42 | } 43 | } 44 | } 45 | 46 | 47 | /** 48 | * To stop watching connected devices 49 | */ 50 | fun stopWatchConnectedDevices() { 51 | Arbor.d("Removing watcher") 52 | adbRepo.cancelWatchConnectedDevice() 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/libdetail/LibraryDetailScreen.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.libdetail 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.grid.GridCells 5 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 6 | import androidx.compose.foundation.lazy.grid.items 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.collectAsState 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.unit.dp 13 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 14 | import com.theapache64.stackzy.model.AndroidAppWrapper 15 | import com.theapache64.stackzy.ui.common.CustomScaffold 16 | import com.theapache64.stackzy.ui.common.ErrorSnackBar 17 | import com.theapache64.stackzy.ui.common.FullScreenError 18 | import com.theapache64.stackzy.ui.common.Selectable 19 | import com.theapache64.stackzy.ui.common.loading.LoadingAnimation 20 | import com.theapache64.stackzy.util.R 21 | 22 | @Composable 23 | fun LibraryDetailScreen( 24 | viewModel: LibraryDetailViewModel, 25 | onBackClicked: () -> Unit, 26 | ) { 27 | val pageTitle by viewModel.pageTitle.collectAsState() 28 | val appsResp by viewModel.apps.collectAsState() 29 | val searchKeyword by viewModel.searchKeyword.collectAsState() 30 | 31 | 32 | CustomScaffold( 33 | title = pageTitle, 34 | subTitle = R.string.lib_detail_sub_title, 35 | onBackClicked = onBackClicked 36 | ) { 37 | when (appsResp) { 38 | is Resource.Loading -> { 39 | val message = (appsResp as Resource.Loading>).message ?: "" 40 | LoadingAnimation(message, funFacts = null) 41 | } 42 | 43 | is Resource.Error -> { 44 | Box { 45 | ErrorSnackBar( 46 | (appsResp as Resource.Error>).errorData 47 | ) 48 | } 49 | } 50 | 51 | is Resource.Success -> { 52 | val libraries = (appsResp as Resource.Success>).data 53 | 54 | 55 | Column { 56 | 57 | if (libraries.isNotEmpty()) { 58 | LazyVerticalGrid( 59 | columns = GridCells.Fixed(4) 60 | ) { 61 | items(libraries) { library -> 62 | Column { 63 | // GridItem 64 | Selectable( 65 | modifier = Modifier.fillMaxWidth(), 66 | data = library, 67 | onSelected = viewModel::onAppClicked 68 | ) 69 | 70 | Spacer( 71 | modifier = Modifier.height(10.dp) 72 | ) 73 | } 74 | } 75 | } 76 | 77 | } else { 78 | // No app found 79 | FullScreenError( 80 | title = "Library not found", 81 | message = "Couldn't find any library with $searchKeyword", 82 | image = painterResource("drawables/woman_desk.png"), 83 | ) 84 | } 85 | } 86 | 87 | } 88 | 89 | null -> { 90 | LoadingAnimation("Preparing apps...", null) 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/libdetail/LibraryDetailScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.libdetail 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.theapache64.stackzy.di.AppComponent 8 | import com.theapache64.stackzy.model.AndroidAppWrapper 9 | import com.theapache64.stackzy.model.LibraryWrapper 10 | import com.theapache64.stackzy.ui.navigation.Component 11 | import com.theapache64.stackzy.util.ApkSource 12 | import javax.inject.Inject 13 | 14 | class LibraryDetailScreenComponent( 15 | componentContext: ComponentContext, 16 | appComponent: AppComponent, 17 | val libraryWrapper: LibraryWrapper, 18 | val onAppClicked: (ApkSource, AndroidAppWrapper) -> Unit, 19 | val onBackClicked: () -> Unit, 20 | val onLogInNeeded: (Boolean) -> Unit 21 | ) : Component, ComponentContext by componentContext { 22 | 23 | @Inject 24 | lateinit var libDetailViewModel: LibraryDetailViewModel 25 | 26 | init { 27 | 28 | appComponent.inject(this) 29 | } 30 | 31 | @Composable 32 | override fun render() { 33 | val scope = rememberCoroutineScope() 34 | LaunchedEffect(libDetailViewModel) { 35 | 36 | libDetailViewModel.init(scope, libraryWrapper, onAppClicked, onLogInNeeded) 37 | 38 | if (libDetailViewModel.apps.value == null) { 39 | libDetailViewModel.loadApps() 40 | } 41 | } 42 | 43 | LibraryDetailScreen( 44 | viewModel = libDetailViewModel, 45 | onBackClicked = onBackClicked 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/libdetail/LibraryDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.libdetail 2 | 3 | import com.malinskiy.adam.request.pkg.Package 4 | import com.theapache64.stackzy.data.local.AndroidApp 5 | import com.theapache64.stackzy.data.repo.AuthRepo 6 | import com.theapache64.stackzy.data.repo.ResultsRepo 7 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 8 | import com.theapache64.stackzy.model.AndroidAppWrapper 9 | import com.theapache64.stackzy.model.LibraryWrapper 10 | import com.theapache64.stackzy.util.ApkSource 11 | import com.theapache64.stackzy.util.R 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | class LibraryDetailViewModel @Inject constructor( 19 | private val resultsRepo: ResultsRepo, 20 | private val authRepo: AuthRepo 21 | ) { 22 | private lateinit var libWrapper: LibraryWrapper 23 | private lateinit var viewModelScope: CoroutineScope 24 | 25 | private val _apps = MutableStateFlow>?>(null) 26 | val apps = _apps.asStateFlow() 27 | 28 | private val _pageTitle = MutableStateFlow("") 29 | val pageTitle = _pageTitle.asStateFlow() 30 | 31 | private val _searchKeyword = MutableStateFlow("") 32 | val searchKeyword = _searchKeyword.asStateFlow() 33 | 34 | private lateinit var onAppSelected: (ApkSource, AndroidAppWrapper) -> Unit 35 | private lateinit var onLogInNeeded: (shouldGoToPlayStore: Boolean) -> Unit 36 | 37 | fun init( 38 | viewModelScope: CoroutineScope, 39 | libWrapper: LibraryWrapper, 40 | onAppSelected: (ApkSource, AndroidAppWrapper) -> Unit, 41 | onLogInNeeded: (shouldGoToPlayStore: Boolean) -> Unit 42 | ) { 43 | this.viewModelScope = viewModelScope 44 | this.libWrapper = libWrapper 45 | this.onAppSelected = onAppSelected 46 | this.onLogInNeeded = onLogInNeeded 47 | 48 | this._pageTitle.value = libWrapper.name 49 | } 50 | 51 | fun loadApps() { 52 | viewModelScope.launch { 53 | resultsRepo.getResults(libWrapper.packageName).collect { 54 | when (it) { 55 | is Resource.Loading -> { 56 | _apps.value = Resource.Loading(R.string.lib_detail_loading) 57 | } 58 | 59 | is Resource.Success -> { 60 | val apps = it.data 61 | .distinctBy { result -> result.packageName } 62 | .map { result -> 63 | AndroidAppWrapper( 64 | AndroidApp( 65 | appPackage = Package(name = result.packageName), 66 | isSystemApp = false, 67 | versionCode = result.versionCode, 68 | versionName = result.versionName, 69 | appTitle = result.appName, 70 | imageUrl = result.logoImageUrl 71 | ), 72 | shouldUseVersionNameAsSubTitle = true 73 | ) 74 | } 75 | _apps.value = Resource.Success(apps) 76 | } 77 | 78 | is Resource.Error -> { 79 | 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | fun onAppClicked(appWrapper: AndroidAppWrapper) { 87 | viewModelScope.launch { 88 | authRepo.getAccount()?.let { account -> 89 | onAppSelected(ApkSource.PlayStore, appWrapper) 90 | } ?: onLogInNeeded(false) 91 | } 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/liblist/LibraryListScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.liblist 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.theapache64.stackzy.di.AppComponent 8 | import com.theapache64.stackzy.model.LibraryWrapper 9 | import com.theapache64.stackzy.ui.navigation.Component 10 | import com.toxicbakery.logging.Arbor 11 | import javax.inject.Inject 12 | 13 | class LibraryListScreenComponent( 14 | componentContext: ComponentContext, 15 | appComponent: AppComponent, 16 | val onLibraryClicked: (LibraryWrapper) -> Unit, 17 | val onBackClicked: () -> Unit 18 | ) : Component, ComponentContext by componentContext { 19 | 20 | @Inject 21 | lateinit var libraryListViewModel: LibraryListViewModel 22 | 23 | init { 24 | appComponent.inject(this) 25 | } 26 | 27 | @Composable 28 | override fun render() { 29 | val scope = rememberCoroutineScope() 30 | LaunchedEffect(libraryListViewModel) { 31 | libraryListViewModel.init(scope) 32 | 33 | if (libraryListViewModel.libsResp.value == null) { 34 | libraryListViewModel.loadLibraries() 35 | Arbor.d("render: Loading libraries.. ") 36 | } 37 | } 38 | 39 | LibraryListScreen( 40 | viewModel = libraryListViewModel, 41 | onLibraryClicked = onLibraryClicked, 42 | onBackClicked = onBackClicked 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/liblist/LibraryListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.liblist 2 | 3 | import com.theapache64.stackzy.data.remote.Library 4 | import com.theapache64.stackzy.data.remote.OptionalResult 5 | import com.theapache64.stackzy.data.repo.LibrariesRepo 6 | import com.theapache64.stackzy.data.repo.ResultsRepo 7 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 8 | import com.theapache64.stackzy.model.LibraryWrapper 9 | import com.theapache64.stackzy.ui.util.getSingularOrPlural 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | import kotlinx.coroutines.launch 14 | import java.util.* 15 | import javax.inject.Inject 16 | 17 | class LibraryListViewModel @Inject constructor( 18 | private val librariesRepo: LibrariesRepo, 19 | private val resultsRepo: ResultsRepo 20 | ) { 21 | private lateinit var viewModelScope: CoroutineScope 22 | 23 | private var fullLibs: List? = null 24 | private val _searchKeyword = MutableStateFlow("") 25 | val searchKeyword: StateFlow = _searchKeyword 26 | 27 | private val _subTitle = MutableStateFlow("") 28 | val subTitle: StateFlow = _subTitle 29 | 30 | private val _libsResp = MutableStateFlow>?>(null) 31 | val libsResp: StateFlow>?> = _libsResp 32 | 33 | fun init( 34 | scope: CoroutineScope, 35 | ) { 36 | this.viewModelScope = scope 37 | } 38 | 39 | fun loadLibraries() { 40 | viewModelScope.launch { 41 | val cachedLibs = librariesRepo.getCachedLibraries()!! 42 | resultsRepo.getAllLibPackages().collect { 43 | when (it) { 44 | is Resource.Loading -> { 45 | _libsResp.value = Resource.Loading() 46 | } 47 | 48 | is Resource.Success -> { 49 | // Here, valid means results that have at least one library 50 | val validLibPackages = filterValidLibPackages(cachedLibs, allLibPackages = it.data) 51 | fullLibs = validLibPackages 52 | _libsResp.value = Resource.Success(validLibPackages) 53 | _subTitle.value = getSubtitleFor(validLibPackages) 54 | } 55 | 56 | is Resource.Error -> { 57 | _libsResp.value = Resource.Error(it.errorData) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | private fun filterValidLibPackages( 65 | cachedLibs: List, 66 | allLibPackages: List 67 | ): List { 68 | return cachedLibs.filter { library -> 69 | allLibPackages.find { 70 | it.libPackages?.contains(library.packageName) ?: false 71 | } != null 72 | }.map { LibraryWrapper(it, null) } 73 | } 74 | 75 | fun onSearchKeywordChanged(newKeyword: String) { 76 | _searchKeyword.value = newKeyword.replace("\n", "") 77 | 78 | // Filtering libraries 79 | val apps = if (newKeyword.isNotBlank()) { 80 | // Filter 81 | fullLibs 82 | ?.filter { 83 | // search with keyword 84 | it.name.lowercase(Locale.getDefault()).contains(newKeyword, ignoreCase = true) || 85 | it.packageName.lowercase(Locale.getDefault()).contains(newKeyword, ignoreCase = true) 86 | } 87 | ?: listOf() 88 | } else { 89 | fullLibs ?: listOf() 90 | } 91 | 92 | _subTitle.value = getSubtitleFor(apps) 93 | _libsResp.value = Resource.Success(apps) 94 | } 95 | 96 | private fun getSubtitleFor(apps: List): String { 97 | return "${apps.size} ${apps.size.getSingularOrPlural("library", "libraries")}" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/login/LogInScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.login 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.github.theapache64.gpa.model.Account 8 | import com.theapache64.stackzy.di.AppComponent 9 | import com.theapache64.stackzy.ui.navigation.Component 10 | import javax.inject.Inject 11 | 12 | class LogInScreenComponent( 13 | appComponent: AppComponent, 14 | private val componentContext: ComponentContext, 15 | private val onLoggedIn: (shouldGoToPlayStore: Boolean, Account) -> Unit, 16 | private val onBackClicked: () -> Unit, 17 | private val shouldGoToPlayStore: Boolean 18 | ) : Component, ComponentContext by componentContext { 19 | 20 | @Inject 21 | lateinit var viewModel: LogInScreenViewModel 22 | 23 | init { 24 | appComponent.inject(this) 25 | } 26 | 27 | @Composable 28 | override fun render() { 29 | 30 | val scope = rememberCoroutineScope() 31 | LaunchedEffect(viewModel) { 32 | viewModel.init(scope, onLoggedIn, shouldGoToPlayStore) 33 | } 34 | 35 | LogInScreen( 36 | viewModel = viewModel, 37 | onBackClicked = onBackClicked 38 | ) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/login/LogInScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.login 2 | 3 | import com.github.theapache64.gpa.model.Account 4 | import com.theapache64.stackzy.data.repo.AuthRepo 5 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.flow.onEach 10 | import kotlinx.coroutines.launch 11 | import java.awt.Desktop 12 | import java.net.URI 13 | import javax.inject.Inject 14 | 15 | class LogInScreenViewModel @Inject constructor( 16 | private val authRepo: AuthRepo 17 | ) { 18 | 19 | companion object { 20 | const val URL_GOOGLE_CREATE_ACCOUNT = "https://accounts.google.com/signup" 21 | } 22 | 23 | private var shouldGoToPlayStore: Boolean = false 24 | private lateinit var viewModelScope: CoroutineScope 25 | 26 | // Using env vars for debug purpose only. 27 | private val _username = MutableStateFlow("") 28 | val username: StateFlow = _username 29 | 30 | private val _password = MutableStateFlow("") 31 | val password: StateFlow = _password 32 | 33 | private val _isRemember = MutableStateFlow(false) 34 | val isRemember: StateFlow = _isRemember 35 | 36 | private val _isUsernameError = MutableStateFlow(false) 37 | val isUsernameError: StateFlow = _isUsernameError 38 | 39 | private val _isPasswordError = MutableStateFlow(false) 40 | val isPasswordError: StateFlow = _isPasswordError 41 | 42 | private val _logInResponse = MutableStateFlow?>(null) 43 | val logInResponse: StateFlow?> = _logInResponse 44 | 45 | private lateinit var onLoggedIn: (shouldGoToPlayStore: Boolean, Account) -> Unit 46 | 47 | private var isSubmitted = false 48 | 49 | fun init( 50 | scope: CoroutineScope, 51 | onLoggedIn: (shouldGoToPlayStore: Boolean, Account) -> Unit, 52 | shouldGoToPlayStore: Boolean 53 | ) { 54 | this.viewModelScope = scope 55 | this.onLoggedIn = onLoggedIn 56 | this.shouldGoToPlayStore = shouldGoToPlayStore 57 | 58 | viewModelScope.launch { 59 | val isRemember = authRepo.isRemember() 60 | if (isRemember) { 61 | // load account and set username and password field 62 | authRepo.getAccount()?.let { account -> 63 | _username.value = account.username 64 | _password.value = account.password 65 | } 66 | } 67 | _isRemember.value = isRemember 68 | } 69 | } 70 | 71 | fun onUsernameChanged(newUsername: String) { 72 | _username.value = newUsername 73 | 74 | // Show error only if the form is submitted 75 | _isUsernameError.value = isSubmitted && isValidUsername(newUsername) 76 | } 77 | 78 | 79 | fun onPasswordChanged(newPassword: String) { 80 | _password.value = newPassword 81 | 82 | // Show error only if the form is submitted 83 | _isPasswordError.value = isSubmitted && isValidPassword(newPassword) 84 | } 85 | 86 | fun onLogInClicked() { 87 | isSubmitted = true 88 | 89 | val username = username.value.trim() 90 | val password = password.value.trim() 91 | 92 | _isUsernameError.value = isValidUsername(username) 93 | _isPasswordError.value = isValidPassword(password) 94 | 95 | viewModelScope.launch { 96 | authRepo.logIn(username, password) 97 | .onEach { 98 | if (it is Resource.Success) { 99 | authRepo.storeAccount(it.data, isRemember.value) 100 | authRepo.setLoggedIn(true) 101 | onLoggedIn(it.data) 102 | } 103 | }.collect { logInResponse -> 104 | _logInResponse.value = logInResponse 105 | } 106 | } 107 | } 108 | 109 | private fun isValidUsername(newUsername: String) = newUsername.isEmpty() 110 | private fun isValidPassword(newPassword: String) = newPassword.isEmpty() 111 | 112 | fun onCreateAccountClicked() { 113 | Desktop.getDesktop().browse(URI(URL_GOOGLE_CREATE_ACCOUNT)) 114 | } 115 | 116 | fun onRememberChanged(isRemember: Boolean) { 117 | _isRemember.value = isRemember 118 | } 119 | 120 | fun onRememberClicked() { 121 | _isRemember.value = !isRemember.value 122 | } 123 | 124 | fun onLoggedIn(account: Account) { 125 | this.onLoggedIn.invoke(shouldGoToPlayStore, account) 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/pathway/PathwayScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.pathway 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import com.arkivanov.decompose.ComponentContext 6 | import com.theapache64.stackzy.di.AppComponent 7 | import com.theapache64.stackzy.ui.navigation.Component 8 | import javax.inject.Inject 9 | 10 | class PathwayScreenComponent( 11 | appComponent: AppComponent, 12 | private val componentContext: ComponentContext, 13 | private val onAdbSelected: () -> Unit, 14 | private val onLibrariesSelected: () -> Unit, 15 | onPlayStoreSelected: () -> Unit, 16 | onLogInNeeded: (shouldGoToPlayStore: Boolean) -> Unit, 17 | ) : Component, ComponentContext by componentContext { 18 | 19 | @Inject 20 | lateinit var viewModel: PathwayViewModel 21 | 22 | init { 23 | appComponent.inject(this) 24 | 25 | viewModel.init( 26 | onPlayStoreSelected = onPlayStoreSelected, 27 | onLogInNeeded = { 28 | onLogInNeeded(true) 29 | } 30 | ) 31 | } 32 | 33 | @Composable 34 | override fun render() { 35 | LaunchedEffect(viewModel) { 36 | viewModel.refreshAccount() 37 | } 38 | 39 | PathwayScreen( 40 | viewModel = viewModel, 41 | onAdbSelected = onAdbSelected, 42 | onLibrariesSelected = onLibrariesSelected 43 | ) 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/pathway/PathwayViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.pathway 2 | 3 | import com.github.theapache64.gpa.model.Account 4 | import com.theapache64.stackzy.data.repo.AuthRepo 5 | import com.theapache64.stackzy.data.repo.ConfigRepo 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import javax.inject.Inject 10 | 11 | 12 | class PathwayViewModel @Inject constructor( 13 | private val authRepo: AuthRepo, 14 | configRepo: ConfigRepo 15 | ) { 16 | 17 | private val config = configRepo.getLocalConfig() 18 | 19 | companion object { 20 | private const val INFO_MADE_WITH_LOVE = "Made with ❤️" 21 | } 22 | 23 | private val _loggedInAccount = MutableStateFlow(null) 24 | val loggedInAccount: StateFlow = _loggedInAccount 25 | 26 | private val _focusedCardInfo = MutableStateFlow(INFO_MADE_WITH_LOVE) 27 | val focusedCardInfo = _focusedCardInfo.asStateFlow() 28 | 29 | private val _isBrowseByLibEnabled = MutableStateFlow(config?.isBrowseByLibEnabled ?: false) 30 | val isBrowseByLibEnabled = _isBrowseByLibEnabled.asStateFlow() 31 | 32 | private val _isPlayStoreEnabled = MutableStateFlow(config?.isPlayStoreEnabled ?: false) 33 | val isPlayStoreEnabled = _isPlayStoreEnabled.asStateFlow() 34 | 35 | private lateinit var onPlayStoreSelected: () -> Unit 36 | private lateinit var onLogInNeeded: () -> Unit 37 | 38 | fun init( 39 | onPlayStoreSelected: () -> Unit, 40 | onLogInNeeded: () -> Unit, 41 | ) { 42 | this.onPlayStoreSelected = onPlayStoreSelected 43 | this.onLogInNeeded = onLogInNeeded 44 | } 45 | 46 | fun refreshAccount() { 47 | val isLoggedIn = authRepo.isLoggedIn() 48 | _loggedInAccount.value = if (isLoggedIn) { 49 | authRepo.getAccount() 50 | } else { 51 | null 52 | } 53 | } 54 | 55 | fun onPlayStoreClicked() { 56 | // Check if user is logged in 57 | if (loggedInAccount.value == null) { 58 | // not logged in 59 | onLogInNeeded.invoke() 60 | } else { 61 | // logged in 62 | onPlayStoreSelected.invoke() 63 | } 64 | } 65 | 66 | fun onLogoutClicked() { 67 | authRepo.setLoggedIn(false) 68 | // If 'RememberMe` is disabled, clear account info as well 69 | if (!authRepo.isRemember()) { 70 | authRepo.clearAccount() 71 | } 72 | _loggedInAccount.value = null 73 | } 74 | 75 | fun onPlayStoreCardFocused() { 76 | _focusedCardInfo.value = "Browse though PlayStore apps" 77 | } 78 | 79 | fun onAdbCardFocused() { 80 | _focusedCardInfo.value = "Browse through connected android device" 81 | } 82 | 83 | fun onLibrariesCardFocused() { 84 | _focusedCardInfo.value = "Find apps that are using a specific library" 85 | } 86 | 87 | fun onCardFocusLost() { 88 | _focusedCardInfo.value = INFO_MADE_WITH_LOVE 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/splash/SplashScreen.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.splash 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.collectAsState 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import com.theapache64.stackzy.ui.common.ErrorSnackBar 15 | import com.theapache64.stackzy.ui.common.LoadingText 16 | import com.theapache64.stackzy.ui.common.Logo 17 | 18 | 19 | /** 20 | * Renders SplashScreen 21 | */ 22 | @Composable 23 | fun SplashScreen( 24 | splashViewModel: SplashViewModel, 25 | onSyncFinished: () -> Unit, 26 | onUpdateNeeded: () -> Unit 27 | ) { 28 | 29 | val isSyncFinished by splashViewModel.isSyncFinished.collectAsState() 30 | val syncFailedReason by splashViewModel.syncFailedMsg.collectAsState() 31 | val syncMessage by splashViewModel.syncMsg.collectAsState() 32 | val shouldUpdate by splashViewModel.shouldUpdate.collectAsState(initial = false) 33 | 34 | LaunchedEffect(shouldUpdate) { 35 | if (shouldUpdate) { 36 | onUpdateNeeded() 37 | } 38 | } 39 | 40 | if (isSyncFinished) { 41 | onSyncFinished() 42 | return 43 | } 44 | 45 | // Content 46 | Box( 47 | modifier = Modifier.fillMaxSize() 48 | ) { 49 | 50 | // Logo 51 | Logo( 52 | modifier = Modifier 53 | .size(100.dp) 54 | .align(Alignment.Center) 55 | ) 56 | 57 | if (!isSyncFinished) { 58 | 59 | // Loading text 60 | LoadingText( 61 | message = syncMessage, 62 | modifier = Modifier 63 | .padding(bottom = 30.dp) 64 | .align(Alignment.BottomCenter) 65 | 66 | ) 67 | } 68 | 69 | syncFailedReason?.let { 70 | ErrorSnackBar( 71 | syncFailedReason = it, 72 | onRetryClicked = { 73 | splashViewModel.onRetryClicked() 74 | } 75 | ) 76 | } 77 | } 78 | } 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/splash/SplashScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.splash 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.arkivanov.decompose.ComponentContext 7 | import com.theapache64.stackzy.di.AppComponent 8 | import com.theapache64.stackzy.ui.navigation.Component 9 | import com.toxicbakery.logging.Arbor 10 | import javax.inject.Inject 11 | 12 | /** 13 | * Splash Screen Component 14 | */ 15 | class SplashScreenComponent( 16 | appComponent: AppComponent, 17 | private val componentContext: ComponentContext, 18 | private val onSyncFinished: () -> Unit, 19 | private val onUpdateNeeded: () -> Unit, 20 | ) : Component, ComponentContext by componentContext { 21 | 22 | @Inject 23 | lateinit var splashViewModel: SplashViewModel 24 | 25 | init { 26 | appComponent.inject(this) 27 | } 28 | 29 | @Composable 30 | override fun render() { 31 | 32 | val scope = rememberCoroutineScope() 33 | LaunchedEffect(splashViewModel) { 34 | Arbor.d("Syncing data...") 35 | splashViewModel.init(scope) 36 | splashViewModel.syncData() 37 | } 38 | 39 | 40 | SplashScreen( 41 | splashViewModel = splashViewModel, 42 | onSyncFinished = onSyncFinished, 43 | onUpdateNeeded = onUpdateNeeded 44 | ) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/update/UpdateScreen.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.update 2 | 3 | import androidx.compose.material.Button 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | import com.theapache64.stackzy.App 7 | import com.theapache64.stackzy.ui.common.FullScreenError 8 | 9 | @Composable 10 | fun UpdateScreen( 11 | viewModel: UpdateScreenViewModel 12 | ) { 13 | FullScreenError( 14 | title = "Update", 15 | message = "Looks like you're using an older version of ${App.appArgs.appName}." + 16 | "Please download the latest version and update. Thank you :)", 17 | action = { 18 | Button( 19 | onClick = { 20 | viewModel.onUpdateClicked() 21 | } 22 | ) { 23 | Text(text = "UPDATE") 24 | } 25 | } 26 | ) 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/update/UpdateScreenComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.update 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.arkivanov.decompose.ComponentContext 5 | import com.theapache64.stackzy.di.AppComponent 6 | import com.theapache64.stackzy.ui.navigation.Component 7 | import javax.inject.Inject 8 | 9 | class UpdateScreenComponent( 10 | appComponent: AppComponent, 11 | private val componentContext: ComponentContext 12 | ) : Component, ComponentContext by componentContext { 13 | 14 | @Inject 15 | lateinit var viewModel: UpdateScreenViewModel 16 | 17 | init { 18 | appComponent.inject(this) 19 | } 20 | 21 | @Composable 22 | override fun render() { 23 | UpdateScreen( 24 | viewModel 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/feature/update/UpdateScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.update 2 | 3 | import java.awt.Desktop 4 | import java.net.URI 5 | import javax.inject.Inject 6 | 7 | class UpdateScreenViewModel @Inject constructor( 8 | 9 | ) { 10 | companion object { 11 | private const val LATEST_VERSION_URL = "https://github.com/theapache64/stackzy/releases/latest" 12 | } 13 | 14 | fun onUpdateClicked() { 15 | Desktop.getDesktop().browse(URI(LATEST_VERSION_URL)) 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/navigation/Component.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | interface Component { 6 | @Composable 7 | fun render() 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/theme/Colors.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object R { 6 | object drawables { 7 | const val appIcon = "drawables/launcher_icons/linux.png" 8 | } 9 | 10 | @Suppress("ClassName") 11 | object color { 12 | val TelegramBlue = Color(0xff30A3E6) 13 | val BigStone = Color(0xff0D1D32) 14 | val Elephant = Color(0xff0D2841) 15 | val WildWatermelon = Color(0xffFF5370) 16 | val Goldenrod = Color(0xffFFCB6B) 17 | val YellowGreen = Color(0xffC3E88D) 18 | val JordyBlue = Color(0xff82B1FF) 19 | val BlueBayoux = Color(0xff546E7A) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.theme 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Surface 8 | import androidx.compose.material.darkColors 9 | import androidx.compose.material.lightColors 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | 14 | // Color set 15 | val LightTheme = lightColors() // TODO : 16 | val DarkTheme = darkColors( 17 | primary = R.color.TelegramBlue, 18 | onPrimary = Color.White, 19 | secondary = R.color.Elephant, 20 | onSecondary = Color.White, 21 | surface = R.color.BigStone, 22 | error = R.color.WildWatermelon 23 | ) 24 | 25 | @Composable 26 | fun StackzyTheme( 27 | isDark: Boolean = true, 28 | content: @Composable (ColumnScope) -> Unit 29 | ) { 30 | MaterialTheme( 31 | colors = if (isDark) DarkTheme else LightTheme, 32 | typography = StackzyTypography 33 | ) { 34 | Surface( 35 | modifier = Modifier.fillMaxSize() 36 | ) { 37 | Column { 38 | content(this) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/theme/Typography.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.text.platform.Font 8 | import androidx.compose.ui.unit.sp 9 | 10 | val GoogleSans = FontFamily( 11 | Font("fonts/GoogleSans-Regular.ttf", FontWeight.Normal), 12 | Font("fonts/GoogleSans-Medium.ttf", FontWeight.Medium), 13 | Font("fonts/GoogleSans-Bold.ttf", FontWeight.Bold), 14 | ) 15 | 16 | val StackzyTypography = Typography( 17 | 18 | defaultFontFamily = GoogleSans, 19 | 20 | h1 = TextStyle( 21 | fontSize = 95.sp, 22 | fontWeight = FontWeight.Normal, 23 | ), 24 | h2 = TextStyle( 25 | fontSize = 59.sp, 26 | fontWeight = FontWeight.Normal, 27 | ), 28 | h3 = TextStyle( 29 | fontSize = 48.sp, 30 | fontWeight = FontWeight.Medium 31 | ), 32 | h4 = TextStyle( 33 | fontSize = 34.sp, 34 | fontWeight = FontWeight.Medium, 35 | ), 36 | h5 = TextStyle( 37 | fontSize = 24.sp, 38 | fontWeight = FontWeight.Medium 39 | ), 40 | h6 = TextStyle( 41 | fontSize = 20.sp, 42 | fontWeight = FontWeight.Bold, 43 | ), 44 | subtitle1 = TextStyle( 45 | fontSize = 16.sp, 46 | fontWeight = FontWeight.Medium, 47 | ), 48 | subtitle2 = TextStyle( 49 | fontSize = 14.sp, 50 | fontWeight = FontWeight.Bold, 51 | ), 52 | body1 = TextStyle( 53 | fontSize = 18.sp, 54 | fontWeight = FontWeight.Normal, 55 | ), 56 | body2 = TextStyle( 57 | fontSize = 14.sp, 58 | fontWeight = FontWeight.Normal, 59 | ), 60 | button = TextStyle( 61 | fontSize = 14.sp, 62 | fontWeight = FontWeight.Bold 63 | ), 64 | caption = TextStyle( 65 | fontSize = 12.sp, 66 | fontWeight = FontWeight.Medium, 67 | ), 68 | overline = TextStyle( 69 | fontSize = 10.sp, 70 | fontWeight = FontWeight.Medium, 71 | ) 72 | ) 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/util/ColorGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.util 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Brush 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.window.Window 16 | import androidx.compose.ui.window.application 17 | import com.github.theapache64.namethatcolor.manager.ColorNameFinder 18 | import com.github.theapache64.namethatcolor.model.HexColor 19 | import com.theapache64.stackzy.ui.common.AlphabetCircle 20 | import com.theapache64.stackzy.util.ColorUtil 21 | import com.toxicbakery.logging.Arbor 22 | import java.util.* 23 | import kotlin.system.exitProcess 24 | 25 | private val addedNameColorNames = mutableSetOf() 26 | 27 | /** 28 | * A color generator to list out colors 29 | */ 30 | fun main() { 31 | var color by mutableStateOf(getRandomColor()) 32 | 33 | val bgColor by derivedStateOf { 34 | 35 | val brighterColor = ColorUtil.getBrightenedColor(color.second) 36 | 37 | Brush.horizontalGradient( 38 | colors = listOf(color.second, brighterColor) 39 | ) 40 | } 41 | 42 | application { 43 | Window( 44 | onCloseRequest = { 45 | exitProcess(0) 46 | } 47 | ) { 48 | Column( 49 | modifier = Modifier.fillMaxSize(), 50 | verticalArrangement = Arrangement.Center, 51 | horizontalAlignment = Alignment.CenterHorizontally 52 | ) { 53 | AlphabetCircle( 54 | character = 'A', 55 | color = bgColor, 56 | modifier = Modifier 57 | .padding(10.dp) 58 | .size(60.dp) 59 | ) 60 | 61 | Row { 62 | Button( 63 | onClick = { 64 | 65 | val colorName = ColorNameFinder.findColor(HexColor(color.first)).second.name 66 | if (!addedNameColorNames.contains(colorName)) { 67 | addedNameColorNames.add(colorName) 68 | Arbor.d( 69 | "Color(0xff${color.first.replace("#", "")}), // $colorName" 70 | ) 71 | } 72 | 73 | color = getRandomColor() 74 | } 75 | ) { 76 | Text(text = "LIKE") 77 | } 78 | 79 | Spacer( 80 | modifier = Modifier.width(10.dp) 81 | ) 82 | 83 | Button( 84 | onClick = { 85 | color = getRandomColor() 86 | } 87 | ) { 88 | Text(text = "DISLIKE") 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | private val random by lazy { Random() } 97 | private fun getRandomColor(): Pair { 98 | val randNum = random.nextInt(0xffffff + 1) 99 | val colorHex = String.format("#%06x", randNum) 100 | val javaColor = java.awt.Color.decode(colorHex) 101 | return Pair( 102 | colorHex, 103 | Color( 104 | javaColor.rgb 105 | ) 106 | ) 107 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/ui/util/IntExt.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.util 2 | 3 | 4 | fun Int.getSingularOrPlural(singular: String, plural: String): String { 5 | return when { 6 | this == 1 -> { 7 | singular 8 | } 9 | 10 | else -> { 11 | plural 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/util/ApkSource.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | import com.theapache64.stackzy.model.AndroidDeviceWrapper 4 | 5 | sealed interface ApkSource { 6 | class Adb(val value: AndroidDeviceWrapper) : ApkSource 7 | data object PlayStore : ApkSource 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/util/ColorUtil.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import kotlin.math.min 5 | 6 | object ColorUtil { 7 | private const val BRIGHT_FRACTION = 0.20f 8 | 9 | /** 10 | * To get brightened version of the given color 11 | */ 12 | fun getBrightenedColor(randomColor: Color): Color { 13 | val newRed = min(1f, (randomColor.red + (randomColor.red + 1 * BRIGHT_FRACTION))) 14 | val newGreen = min(1f, (randomColor.green + (randomColor.green + 1 * BRIGHT_FRACTION))) 15 | val newBlue = min(1f, (randomColor.blue + (randomColor.blue + 1 * BRIGHT_FRACTION))) 16 | 17 | /*Arbor.d("Red : ${randomColor.red} -> $newRed") 18 | Arbor.d("Blue : ${randomColor.blue} -> $newBlue") 19 | Arbor.d("Green : ${randomColor.green} -> $newGreen") 20 | Arbor.d("----------------------------------")*/ 21 | return Color( 22 | red = newRed, 23 | green = newGreen, 24 | blue = newBlue, 25 | ) 26 | } 27 | 28 | private val colorSet = setOf( 29 | Color(0xff127ad7), // Lochmara 30 | Color(0xff0cc845), // Malachite 31 | Color(0xff006d52), // Watercourse 32 | Color(0xff206b02), // Japanese Laurel 33 | Color(0xffa70371), // Flirt 34 | Color(0xff14805b), // Salem 35 | Color(0xff2443e5), // Persian Blue 36 | Color(0xff2f42cc), // Cerulean Blue 37 | Color(0xff445a92), // Chambray 38 | Color(0xff2c99aa), // Eastern Blue 39 | Color(0xffbd335b), // Night Shadz 40 | Color(0xff4bb516), // Christi 41 | Color(0xff778b01), // Olive 42 | Color(0xff1b48a9), // Tory Blue 43 | Color(0xffd5201f), // Thunderbird 44 | Color(0xff932615), // Tabasco 45 | Color(0xffee9c1b), // Carrot Orange 46 | Color(0xff503678), // Minsk 47 | Color(0xffe8961a), // Dixie 48 | Color(0xff204628), // Everglade 49 | Color(0xffe87c00), // Mango Tango 50 | Color(0xff009e5f), // Green Haze 51 | Color(0xffef4aca), // Razzle Dazzle Rose 52 | Color(0xff19365e), // Biscay 53 | Color(0xff001aea), // Blue 54 | Color(0xff2951df), // Royal Blue 55 | Color(0xff9f2b98), // Violet Eggplant 56 | Color(0xffe213f6), // Magenta Fuchsia 57 | Color(0xff1bc3a6), // Java 58 | Color(0xffc1033c), // Shiraz 59 | Color(0xff16a4d9), // Curious Blue 60 | Color(0xfffe389e), // Wild Strawberry 61 | Color(0xff340773), // Blue Diamond 62 | Color(0xff4408a6), // Blue Gem 63 | Color(0xff0b67c2), // Denim 64 | Color(0xff91110d), // Tamarillo 65 | Color(0xff481245), // Loulou 66 | Color(0xff1061fd), // Blue Ribbon 67 | Color(0xff78970b), // Limeade 68 | Color(0xff9810ef), // Electric Violet 69 | Color(0xff31a31a), // La Palma 70 | Color(0xff610b05), // Red Oxide 71 | Color(0xff5935ee), // Purple Heart 72 | Color(0xffb52958), // Hibiscus 73 | Color(0xffe60b8f), // Hollywood Cerise 74 | Color(0xff13012e), // Black Rock 75 | Color(0xff618f9d), // Gothic 76 | Color(0xff902745), // Camelot 77 | Color(0xff1a60dc), // Mariner 78 | Color(0xff3f42b8), // Governor Bay 79 | Color(0xfff80c81), // Rose 80 | Color(0xff2c6369), // Casal 81 | Color(0xff5ecc03), // Lima 82 | Color(0xff141e6c), // Lucky Point 83 | Color(0xfff32905), // Scarlet 84 | Color(0xff434d77), // East Bay 85 | Color(0xff4b1285), // Windsor 86 | Color(0xff3d8fc5), // Boston Blue 87 | Color(0xffca4afa), // Heliotrope 88 | Color(0xff95aa10), // Citron 89 | Color(0xfff51837), // Torch Red 90 | Color(0xff18a955), // Eucalyptus 91 | Color(0xffd80dc9), // Shocking Pink 92 | Color(0xffe77b11), // Christine 93 | Color(0xff55d804), // Bright Green 94 | Color(0xffe4531b), // Flamingo 95 | Color(0xffb81f40), // Maroon Flush 96 | Color(0xff46672e), // Chalet Green 97 | Color(0xffee0069), // Razzmatazz 98 | Color(0xff330f7b), // Persian Indigo 99 | Color(0xffdc0712), // Monza 100 | Color(0xff362288), // Meteorite 101 | Color(0xff299340), // Sea Green 102 | Color(0xff9c852c), // Luxor Gold 103 | Color(0xff21bbe3), // Scooter 104 | Color(0xffd6327d), // Cerise 105 | Color(0xffc729ac), // Medium Red Violet 106 | Color(0xff0d02d8), // Dark Blue 107 | Color(0xff91005f), // Fresh Eggplant 108 | Color(0xff60197d), // Honey Flower 109 | Color(0xff5e1c16), // Cherrywood 110 | Color(0xfff66a07), // Blaze Orange 111 | Color(0xff0a76a3), // Allports 112 | Color(0xff131954), // Bunting 113 | ) 114 | 115 | private val pureRandom = PureRandom(colorSet) 116 | 117 | fun getRandomColor(): Color { 118 | return pureRandom.get() 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/util/PureRandom.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | /** 4 | * To get non-repetitive random items 5 | */ 6 | class PureRandom( 7 | private val items: Set 8 | ) { 9 | private val takenItems = mutableSetOf() 10 | 11 | fun get(): T { 12 | 13 | if (takenItems.size >= items.size) { 14 | // all items taken so clear the list 15 | takenItems.clear() 16 | } 17 | 18 | val takenItem = items.random() 19 | if (takenItems.contains(takenItem)) { 20 | // already taken 21 | return get() 22 | } 23 | takenItems.add(takenItem) 24 | return takenItem 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/util/R.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | object R { 4 | @Suppress("ClassName") 5 | object string { 6 | const val lib_detail_loading = "Loading apps..." 7 | const val libraries_list_title = "Libraries" 8 | const val app_detail_action_open_market = "SEARCH IN MARKET" 9 | const val any_error_title_damn_it = "Damn It!" 10 | const val app_detail_loading_fetching_apk = "Fetching APK..." 11 | const val app_detail_loading_decompiling = "Decompiling..." 12 | const val app_detail_loading_analysing = "Analysing..." 13 | const val app_detail_error_apk_remote_path = "Couldn't find APK remote path" 14 | const val app_detail_title = "Built with" 15 | const val select_app_label_search = "Search" 16 | const val select_app_cd_go_back = "Go back" 17 | const val select_app_title = "Select Application" 18 | const val device_select_the_device = "Select the device" 19 | const val device_no_device_message = "Looks like you're not connected your phone" 20 | const val device_no_device_title = "No device connected" 21 | const val all_action_retry = "RETRY" 22 | const val logo = "Logo" 23 | const val lib_detail_sub_title = "is utilized by these apps" 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/stackzy/util/flow/EventFlow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Boil (https://github.com/theapache64/boil) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.theapache64.stackzy.util.flow 17 | 18 | import kotlinx.coroutines.flow.MutableSharedFlow 19 | 20 | /** 21 | * To fire events. 22 | * This flow won't fire the last value for each collect call. 23 | * This observer will only be invoked on `tryEmit` calls. 24 | * (replacement for SingleLiveEvent :D) 25 | * Created by theapache64 : Jan 08 Fri,2021 @ 01:40 26 | */ 27 | fun mutableEventFlow(): MutableSharedFlow { 28 | return MutableSharedFlow( 29 | replay = 0, 30 | extraBufferCapacity = 1 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/apktool_2.9.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/apktool_2.9.3.jar -------------------------------------------------------------------------------- /src/main/resources/drawables/guy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/guy.png -------------------------------------------------------------------------------- /src/main/resources/drawables/ic_error_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/ic_error_code.png -------------------------------------------------------------------------------- /src/main/resources/drawables/launcher_icons/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/launcher_icons/linux.png -------------------------------------------------------------------------------- /src/main/resources/drawables/launcher_icons/macos.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/launcher_icons/macos.icns -------------------------------------------------------------------------------- /src/main/resources/drawables/launcher_icons/windows.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/launcher_icons/windows.ico -------------------------------------------------------------------------------- /src/main/resources/drawables/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/loading.png -------------------------------------------------------------------------------- /src/main/resources/drawables/logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/drawables/no_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/no_device.png -------------------------------------------------------------------------------- /src/main/resources/drawables/playstore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/drawables/usb.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/drawables/woman_desk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/drawables/woman_desk.png -------------------------------------------------------------------------------- /src/main/resources/fonts/FiraCode-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/fonts/FiraCode-Regular.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/GoogleSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/fonts/GoogleSans-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/GoogleSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/fonts/GoogleSans-Medium.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/GoogleSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/fonts/GoogleSans-Regular.ttf -------------------------------------------------------------------------------- /src/main/resources/jadx-1.3.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/main/resources/jadx-1.3.1.zip -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/AdbRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.malinskiy.adam.request.pkg.Package 5 | import com.theapache64.stackzy.data.local.AndroidApp 6 | import com.theapache64.stackzy.test.FLUTTER_PACKAGE_NAME 7 | import com.theapache64.stackzy.test.MyDaggerMockRule 8 | import com.theapache64.stackzy.test.NATIVE_KOTLIN_PACKAGE_NAME 9 | import com.theapache64.stackzy.test.runBlockingUnitTest 10 | import it.cosenonjaviste.daggermock.InjectFromComponent 11 | import kotlinx.coroutines.flow.distinctUntilChanged 12 | import kotlinx.coroutines.flow.first 13 | import org.junit.Rule 14 | import org.junit.Test 15 | import org.junit.jupiter.api.BeforeAll 16 | 17 | 18 | class AdbRepoTest { 19 | @get:Rule 20 | val daggerMockRule = MyDaggerMockRule() 21 | 22 | @InjectFromComponent 23 | private lateinit var adbRepo: AdbRepo 24 | 25 | @BeforeAll 26 | @Test 27 | fun beforeAll() = runBlockingUnitTest { 28 | val devices = adbRepo.watchConnectedDevice().first() 29 | assert(devices.isNotEmpty()) { 30 | "No device found. Please connect a device to run this test" 31 | } 32 | } 33 | 34 | @Test 35 | fun `Device list works`() = runBlockingUnitTest { 36 | val connectedDevices = adbRepo.watchConnectedDevice().first() 37 | if (connectedDevices.isEmpty()) { 38 | assert(false) { 39 | "Are you sure you've at least one device connected" 40 | } 41 | } else { 42 | // not empty. one or more devices are connected 43 | assert(true) 44 | } 45 | } 46 | 47 | @Test 48 | fun `App list works`() = runBlockingUnitTest { 49 | val connectedDevices = adbRepo.watchConnectedDevice().first() 50 | val installedApps = adbRepo.getInstalledApps(connectedDevices.first().device) 51 | if (installedApps.isEmpty()) { 52 | assert(false) { 53 | "Are you sure you've at least one device connected" 54 | } 55 | } else { 56 | //verify both system apps and 3rd apps are available 57 | val (systemApps, thirdPartyApps) = installedApps.partition { it.isSystemApp } 58 | systemApps.size.should.above(0) 59 | thirdPartyApps.size.should.above(0) 60 | 61 | // not empty. one or more devices are connected 62 | assert(true) 63 | } 64 | } 65 | 66 | @Test 67 | fun `Fetch path works - native android`() = runBlockingUnitTest { 68 | val device = adbRepo.watchConnectedDevice().first().first() 69 | val apkPath = adbRepo.getApkPath( 70 | device, 71 | AndroidApp( 72 | Package(NATIVE_KOTLIN_PACKAGE_NAME), 73 | isSystemApp = false, 74 | ) 75 | ) 76 | apkPath.should.startWith("/data/app/") 77 | } 78 | 79 | @Test 80 | fun `Fetch path works - flutter`() = runBlockingUnitTest { 81 | val device = adbRepo.watchConnectedDevice().first().first() 82 | val apkPath = adbRepo.getApkPath( 83 | device, 84 | AndroidApp( 85 | Package(FLUTTER_PACKAGE_NAME), 86 | isSystemApp = false, 87 | ) 88 | ) 89 | apkPath.should.startWith("/data/app/") 90 | } 91 | 92 | @Test 93 | fun `Fetch path fails for invalid package`() = runBlockingUnitTest { 94 | val device = adbRepo.watchConnectedDevice().first().first() 95 | val app = "dffgfgdf" 96 | val apkPath = adbRepo.getApkPath( 97 | device, 98 | AndroidApp( 99 | Package(app), 100 | isSystemApp = false, 101 | ) 102 | ) 103 | apkPath.should.`null` 104 | } 105 | 106 | @Test 107 | fun `Download ADB works`() = runBlockingUnitTest { 108 | var lastProgress = 0 109 | adbRepo.downloadAdb() 110 | .distinctUntilChanged() 111 | .collect { 112 | lastProgress = it 113 | } 114 | 115 | lastProgress.should.equal(100) 116 | } 117 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/AuthRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.github.theapache64.gpa.model.Account 5 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 6 | import com.theapache64.stackzy.test.MyDaggerMockRule 7 | import com.theapache64.stackzy.test.runBlockingUnitTest 8 | import com.toxicbakery.logging.Arbor 9 | import it.cosenonjaviste.daggermock.InjectFromComponent 10 | import org.junit.Rule 11 | import org.junit.Test 12 | 13 | 14 | class AuthRepoTest { 15 | @get:Rule 16 | val daggerMockRule = MyDaggerMockRule() 17 | 18 | @InjectFromComponent 19 | private lateinit var authRepo: AuthRepo 20 | 21 | @Test 22 | fun givenValidCreds_whenLogIn_thenSuccess() = runBlockingUnitTest { 23 | val username = System.getenv("PLAY_API_GOOGLE_USERNAME")!! 24 | val password = System.getenv("PLAY_API_GOOGLE_PASSWORD")!! 25 | 26 | authRepo.logIn(username, password).collect { 27 | when (it) { 28 | is Resource.Loading -> { 29 | Arbor.d("logging in...") 30 | } 31 | 32 | is Resource.Success -> { 33 | it.data.username.should.equal(username) 34 | } 35 | 36 | is Resource.Error -> { 37 | assert(false) 38 | } 39 | } 40 | } 41 | } 42 | 43 | @Test 44 | fun givenInvalidCreds_whenLogIn_thenError() = runBlockingUnitTest { 45 | 46 | authRepo.logIn("", "").collect { 47 | when (it) { 48 | is Resource.Loading -> { 49 | Arbor.d("logging in...") 50 | } 51 | 52 | is Resource.Success -> { 53 | assert(false) 54 | } 55 | 56 | is Resource.Error -> { 57 | assert(true) 58 | } 59 | } 60 | } 61 | } 62 | 63 | @Test 64 | fun givenAccount_whenStoredGetAndCleared_thenSuccess() { 65 | val dummyAccount = Account( 66 | username = "john.doe", 67 | password = "pass1234", 68 | token = "someSecureToken", 69 | gsfId = "jhj45k34h5k3h45kjh34k", 70 | locale = "en-IN" 71 | ) 72 | authRepo.storeAccount(dummyAccount, true) 73 | authRepo.getAccount().should.equal(dummyAccount) 74 | 75 | authRepo.clearAccount() // test finished, so delete account 76 | authRepo.getAccount().should.`null` 77 | } 78 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/ConfigRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 5 | import com.theapache64.stackzy.test.MyDaggerMockRule 6 | import com.theapache64.stackzy.test.runBlockingUnitTest 7 | import com.toxicbakery.logging.Arbor 8 | import it.cosenonjaviste.daggermock.InjectFromComponent 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | 13 | class ConfigRepoTest { 14 | @get:Rule 15 | val daggerMockRule = MyDaggerMockRule() 16 | 17 | @InjectFromComponent 18 | private lateinit var configRepo: ConfigRepo 19 | 20 | @Test 21 | fun `Get from remote, store in local and get from local`() = runBlockingUnitTest { 22 | configRepo.getRemoteConfig().collect { 23 | when (it) { 24 | is Resource.Loading -> { 25 | Arbor.d("Loading config") 26 | } 27 | 28 | is Resource.Success -> { 29 | val remoteConfig = it.data 30 | remoteConfig.should.not.`null` 31 | 32 | // Got data, now store it in local 33 | configRepo.saveConfigToLocal(remoteConfig) 34 | 35 | // Now get from local 36 | val localConfig = configRepo.getLocalConfig() 37 | 38 | // Both should same 39 | remoteConfig.should.equal(localConfig) 40 | } 41 | 42 | is Resource.Error -> { 43 | assert(false) { 44 | it.errorData 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/JadxRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.theapache64.stackzy.test.MyDaggerMockRule 5 | import it.cosenonjaviste.daggermock.InjectFromComponent 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import kotlin.io.path.exists 9 | 10 | internal class JadxRepoTest { 11 | @get:Rule 12 | val daggerMockRule = MyDaggerMockRule() 13 | 14 | @InjectFromComponent 15 | private lateinit var jadxRepo: JadxRepo 16 | 17 | @Test 18 | fun jadxDirExist() { 19 | jadxRepo.jadxDirPath.exists().should.`true` 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/LibrariesRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 5 | import com.theapache64.stackzy.test.MyDaggerMockRule 6 | import com.theapache64.stackzy.test.runBlockingUnitTest 7 | import it.cosenonjaviste.daggermock.InjectFromComponent 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | internal class LibrariesRepoTest { 12 | 13 | @get:Rule 14 | val daggerMockRule = MyDaggerMockRule() 15 | 16 | @InjectFromComponent 17 | private lateinit var librariesRepo: LibrariesRepo 18 | 19 | @Test 20 | fun `Libraries have data`() = runBlockingUnitTest { 21 | librariesRepo.getRemoteLibraries() 22 | .collect { 23 | when (it) { 24 | is Resource.Loading -> { 25 | // do nothing 26 | } 27 | 28 | is Resource.Success -> { 29 | it.data.size.should.above(0) 30 | } 31 | 32 | is Resource.Error -> { 33 | assert(false) 34 | } 35 | } 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/PlayStoreRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.akdeniz.googleplaycrawler.GooglePlayAPI 4 | import com.github.theapache64.expekt.should 5 | import com.github.theapache64.gpa.api.Play 6 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 7 | import com.theapache64.stackzy.test.MyDaggerMockRule 8 | import com.theapache64.stackzy.test.runBlockingUnitTest 9 | import com.toxicbakery.logging.Arbor 10 | import it.cosenonjaviste.daggermock.InjectFromComponent 11 | import org.junit.Rule 12 | import org.junit.Test 13 | import org.junit.jupiter.api.BeforeAll 14 | import org.junit.jupiter.api.TestInstance 15 | 16 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 17 | class PlayStoreRepoTest { 18 | 19 | @get:Rule 20 | val daggerMockRule = MyDaggerMockRule() 21 | 22 | @InjectFromComponent 23 | private lateinit var playStoreRepo: PlayStoreRepo 24 | 25 | @InjectFromComponent 26 | private lateinit var authRepo: AuthRepo 27 | 28 | companion object { 29 | private lateinit var api: GooglePlayAPI 30 | } 31 | 32 | @BeforeAll 33 | @Test 34 | fun beforeAll() = runBlockingUnitTest { 35 | val username = System.getenv("PLAY_API_GOOGLE_USERNAME")!! 36 | val password = System.getenv("PLAY_API_GOOGLE_PASSWORD")!! 37 | 38 | authRepo.logIn(username, password).collect { 39 | when (it) { 40 | is Resource.Loading -> { 41 | Arbor.d("logging in...") 42 | } 43 | 44 | is Resource.Success -> { 45 | api = Play.getApi(account = it.data) 46 | } 47 | 48 | is Resource.Error -> { 49 | assert(false) 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Test 56 | fun givenKeyword_whenSearch_thenSuccess() = runBlockingUnitTest { 57 | val maxSearchResult = 10 58 | playStoreRepo.search( 59 | "WhatsApp", 60 | api, 61 | maxSearchResult 62 | ).size.should.above(maxSearchResult - 1) // more than or equal 63 | } 64 | 65 | @Test 66 | fun givenPackageName_whenFind_thenReturnValidDetails() = runBlockingUnitTest { 67 | playStoreRepo.find( 68 | "com.theapache64.papercop", 69 | api 70 | )?.appTitle.should.equal("Paper Cop") 71 | } 72 | 73 | @Test 74 | fun givenInvalidPackageName_whenFind_thenReturnNull() = runBlockingUnitTest { 75 | playStoreRepo.find( 76 | "com.theapache64.some.invalid.package.name", 77 | api 78 | ).should.`null` 79 | } 80 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/repo/UntrackedLibsRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.repo 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.theapache64.stackzy.data.remote.UntrackedLibrary 5 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 6 | import com.theapache64.stackzy.test.MyDaggerMockRule 7 | import com.theapache64.stackzy.test.runBlockingUnitTest 8 | import it.cosenonjaviste.daggermock.InjectFromComponent 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | internal class UntrackedLibsRepoTest { 13 | 14 | @get:Rule 15 | val daggerMockRule = MyDaggerMockRule() 16 | 17 | @InjectFromComponent 18 | private lateinit var untrackedLibsRepo: UntrackedLibsRepo 19 | 20 | @Test 21 | fun `Add new untracked library`() = runBlockingUnitTest { 22 | val inputPackageName = "com.test.package" 23 | untrackedLibsRepo 24 | .add(UntrackedLibrary(inputPackageName)) 25 | .collect { 26 | when (it) { 27 | is Resource.Loading -> { 28 | // do nothing 29 | } 30 | 31 | is Resource.Success -> { 32 | it.data.packageNames.should.equal(inputPackageName) 33 | } 34 | 35 | is Resource.Error -> { 36 | assert(false) 37 | } 38 | } 39 | } 40 | } 41 | 42 | @Test 43 | fun `Get untracked packages`() = runBlockingUnitTest { 44 | untrackedLibsRepo 45 | .getUntrackedLibs() 46 | .collect { 47 | when (it) { 48 | is Resource.Loading -> { 49 | // do nothing 50 | } 51 | 52 | is Resource.Success -> { 53 | it.data.size.should.above(0) 54 | } 55 | 56 | is Resource.Error -> { 57 | assert(false) 58 | } 59 | } 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/data/util/StringUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.data.util 2 | 3 | import com.github.theapache64.expekt.should 4 | import org.junit.Test 5 | 6 | 7 | class StringUtilsTest { 8 | @Test 9 | fun `Remove apostrophe from start and end`() { 10 | StringUtils 11 | .removeApostrophe("\"McDonald's\"") 12 | .should.equal("McDonald's") 13 | } 14 | 15 | @Test 16 | fun `Do not remove start and end`() { 17 | StringUtils 18 | .removeApostrophe("McDonald's") 19 | .should.equal("McDonald's") 20 | } 21 | 22 | @Test 23 | fun `Do nothing`() { 24 | StringUtils 25 | .removeApostrophe("McDonalds") 26 | .should.equal("McDonalds") 27 | } 28 | 29 | @Test 30 | fun `Remove apostrophe from start and end - complex`() { 31 | StringUtils 32 | .removeApostrophe("\"\"M\"c'Do'na\"ld's\"\"") 33 | .should.equal("\"M\"c'Do'na\"ld's\"") 34 | } 35 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/test/MyDaggerMockRule.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.test 2 | 3 | import com.theapache64.stackzy.di.module.ApkToolModule 4 | import com.theapache64.stackzy.di.module.JadxModule 5 | import com.theapache64.stackzy.di.module.NetworkModule 6 | import com.theapache64.stackzy.di.module.PreferenceModule 7 | import it.cosenonjaviste.daggermock.DaggerMockRule 8 | 9 | class MyDaggerMockRule : DaggerMockRule( 10 | TestComponent::class.java, 11 | NetworkModule(), 12 | ApkToolModule(), 13 | PreferenceModule(), 14 | JadxModule() 15 | ) { 16 | init { 17 | customizeBuilder { 18 | it 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/test/TestComponent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.test 2 | 3 | import com.theapache64.stackzy.data.remote.ApiInterface 4 | import com.theapache64.stackzy.data.repo.* 5 | import com.theapache64.stackzy.di.module.* 6 | import dagger.Component 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | @Component( 11 | modules = [ 12 | NetworkModule::class, 13 | ApkToolModule::class, 14 | PreferenceModule::class, 15 | CryptoModule::class, 16 | JadxModule::class, 17 | ] 18 | ) 19 | interface TestComponent { 20 | fun apiInterface(): ApiInterface 21 | fun librariesRepo(): LibrariesRepo 22 | fun adbRepo(): AdbRepo 23 | fun authRepo(): AuthRepo 24 | fun configRepo(): ConfigRepo 25 | fun resultRepo(): ResultsRepo 26 | fun playStoreRepo(): PlayStoreRepo 27 | fun apkToolRepo(): ApkToolRepo 28 | fun apkAnalyzerRepo(): ApkAnalyzerRepo 29 | fun untrackedLibsRepo(): UntrackedLibsRepo 30 | fun jadxRepo(): JadxRepo 31 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/test/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.test 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.runBlocking 5 | import java.io.File 6 | import java.net.URL 7 | 8 | const val FLUTTER_APP_NAME = "Flutter Tutorial" 9 | const val FLUTTER_PACKAGE_NAME = "com.sts.flutter" 10 | const val FLUTTER_APK_FILE_NAME = "com.sts.flutter_flutter.apk" 11 | 12 | const val NATIVE_KOTLIN_APP_NAME = "TopCorn" 13 | const val NATIVE_KOTLIN_PACKAGE_NAME = "com.theapache64.topcorn" 14 | const val NATIVE_KOTLIN_APK_FILE_NAME = "com.theapache64.topcorn_kotlin_android.apk" 15 | 16 | const val CORDOVA_APP_NAME = "FinC Financial Calculators" 17 | const val CORDOVA_PACKAGE_NAME = "com.swot.emicalculator" 18 | const val CORDOVA_APK_FILE_NAME = "com.swot.emicalculator_cordova.apk" 19 | 20 | const val XAMARIN_APP_NAME = "Xamarin Samaples" 21 | const val XAMARIN_PACKAGE_NAME = "com.mobmaxime.xamarin" 22 | const val XAMARIN_APK_FILE_NAME = "com.mobmaxime.xamarin_xamarin.apk" 23 | 24 | const val NATIVE_JAVA_APK_FILE_NAME = "com.theah64.whatsappstatusbrowser_java_android.apk" 25 | 26 | const val REACT_NATIVE_APP_NAME = "React Native Animation Examples" 27 | const val REACT_NATIVE_PACKAGE_NAME = "com.reactnativeanimationexamples" 28 | const val REACT_NATIVE_APK_FILE_NAME = "com.reactnativeanimationexamples_react_native.apk" 29 | 30 | fun runBlockingUnitTest(block: suspend (scope: CoroutineScope) -> Unit) = runBlocking { 31 | block(this) 32 | } 33 | 34 | fun getTestResource(name: String): File { 35 | val url: URL = Thread.currentThread().contextClassLoader.getResource(name)!! 36 | return File(url.path) 37 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/ui/feature/applist/AppListViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.ui.feature.applist 2 | 3 | import com.github.theapache64.expekt.should 4 | import org.junit.Test 5 | 6 | class AppListViewModelTest { 7 | @Test 8 | fun validUrl() { 9 | AppListViewModel.isPlayStoreUrl( 10 | "https://play.google.com/store/apps/details?id=com.theapache64.papercop" 11 | ).should.`true` 12 | } 13 | 14 | @Test 15 | fun validFullUrl() { 16 | AppListViewModel.isPlayStoreUrl( 17 | "https://play.google.com/store/apps/details?id=in.startv.hotstar&hl=en_IN" 18 | ).should.`true` 19 | } 20 | 21 | @Test 22 | fun invalidUrl() { 23 | AppListViewModel.isPlayStoreUrl( 24 | "https://play.google.com/store/apps/details?id=co-m.theapache64.papercop" 25 | ).should.`false` 26 | } 27 | 28 | @Test 29 | fun parseValidPackageName() { 30 | AppListViewModel.parsePackageName( 31 | "https://play.google.com/store/apps/details?id=com.theapache64.papercop" 32 | ).should.equal("com.theapache64.papercop") 33 | } 34 | 35 | @Test 36 | fun parseValidPackageNameFull() { 37 | AppListViewModel.parsePackageName( 38 | "https://play.google.com/store/apps/details?id=com.theapache64.papercop&hl=en_IN" 39 | ).should.equal("com.theapache64.papercop") 40 | } 41 | 42 | @Test 43 | fun parseInvalidPackageName() { 44 | AppListViewModel.parsePackageName( 45 | "https://play.google.com/store/apps/details?id=345-453" 46 | ).should.`null` 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/util/AndroidVersionIdentifierTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | import com.github.theapache64.expekt.should 4 | import com.theapache64.stackzy.data.util.AndroidVersionIdentifier 5 | import org.junit.Test 6 | 7 | class AndroidVersionIdentifierTest { 8 | @Test 9 | fun `Single version`() { 10 | AndroidVersionIdentifier.getVersion(3).should.equal("Cupcake") 11 | AndroidVersionIdentifier.getVersion(50).should.`null` 12 | } 13 | 14 | @Test 15 | fun `Range version`() { 16 | AndroidVersionIdentifier.getVersion(4).should.equal("Donut") 17 | AndroidVersionIdentifier.getVersion(5).should.equal("Eclair") 18 | AndroidVersionIdentifier.getVersion(6).should.equal("Eclair") 19 | AndroidVersionIdentifier.getVersion(7).should.equal("Eclair") 20 | AndroidVersionIdentifier.getVersion(8).should.equal("Froyo") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/stackzy/util/LibrariesRepoExt.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.stackzy.util 2 | 3 | import com.theapache64.stackzy.data.remote.Library 4 | import com.theapache64.stackzy.data.repo.LibrariesRepo 5 | import com.theapache64.stackzy.data.util.calladapter.flow.Resource 6 | import com.toxicbakery.logging.Arbor 7 | 8 | suspend fun LibrariesRepo.loadLibs(onLibsLoaded: suspend (List) -> Unit) { 9 | getRemoteLibraries().collect { 10 | when (it) { 11 | is Resource.Loading -> { 12 | Arbor.d("Loading libs") 13 | } 14 | 15 | is Resource.Success -> { 16 | onLibsLoaded(it.data) 17 | } 18 | 19 | is Resource.Error -> { 20 | throw IllegalArgumentException(it.errorData) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/resources/a.i.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/a.i.apk -------------------------------------------------------------------------------- /src/test/resources/com.Dani.Balls_1.18_unity.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.Dani.Balls_1.18_unity.apk -------------------------------------------------------------------------------- /src/test/resources/com.mobmaxime.xamarin_xamarin.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.mobmaxime.xamarin_xamarin.apk -------------------------------------------------------------------------------- /src/test/resources/com.reactnativeanimationexamples_react_native.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.reactnativeanimationexamples_react_native.apk -------------------------------------------------------------------------------- /src/test/resources/com.sts.flutter_flutter.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.sts.flutter_flutter.apk -------------------------------------------------------------------------------- /src/test/resources/com.swot.emicalculator_cordova.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.swot.emicalculator_cordova.apk -------------------------------------------------------------------------------- /src/test/resources/com.theah64.whatsappstatusbrowser_java_android.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.theah64.whatsappstatusbrowser_java_android.apk -------------------------------------------------------------------------------- /src/test/resources/com.theapache64.topcorn_kotlin_android.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.theapache64.topcorn_kotlin_android.apk -------------------------------------------------------------------------------- /src/test/resources/com.twitter.android_9.26.0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/stackzy/5bfdd8dca9eda2a35b54c42bbf3136f4d89825fc/src/test/resources/com.twitter.android_9.26.0.apk --------------------------------------------------------------------------------