├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
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 | 
6 |
7 | **2. Select App**
8 |
9 | 
10 |
11 | **3. Enjoy result ;)**
12 |
13 | 
14 |
15 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/main/resources/drawables/usb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------