├── sample
├── .gitignore
├── src
│ └── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable-v24
│ │ │ ├── ic_launcher_monochrome.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout
│ │ │ └── activity_main.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── venom
│ │ │ └── sample
│ │ │ ├── App.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
└── build.gradle
├── venom
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── drawable-hdpi
│ │ │ │ └── venom_dead_robot.png
│ │ │ ├── drawable-mdpi
│ │ │ │ └── venom_dead_robot.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── venom_dead_robot.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── venom_dead_robot.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ └── venom_dead_robot.png
│ │ │ ├── values
│ │ │ │ ├── styles.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ └── drawable
│ │ │ │ ├── venom_death_bg.xml
│ │ │ │ └── android_adb.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── github
│ │ │ │ └── venom
│ │ │ │ ├── service
│ │ │ │ ├── ServiceDelegate.kt
│ │ │ │ ├── NotificationConfig.kt
│ │ │ │ ├── VenomService.kt
│ │ │ │ └── VenomNotificationManager.kt
│ │ │ │ ├── VenomPreferenceManager.kt
│ │ │ │ ├── CompositionRoot.kt
│ │ │ │ ├── Venom.kt
│ │ │ │ └── DeathActivity.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── venom
│ │ │ └── test
│ │ │ ├── VenomTestActivity.kt
│ │ │ └── VenomTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── venom
│ │ ├── service
│ │ └── ServiceDelegateTest.kt
│ │ └── VenomTest.kt
└── build.gradle
├── venom-no-op
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── venom
│ │ ├── Venom.kt
│ │ └── service
│ │ └── NotificationConfig.kt
└── build.gradle
├── preview
├── header.png
├── preview.gif
└── preview.mp4
├── detekt-config.yml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .editorconfig
├── jitpack.yml
├── .gitignore
├── .github
└── workflows
│ ├── check.yml
│ └── build.yml
├── LICENSE
├── settings.gradle
├── gradle.properties
├── static-analysis.gradle
├── common-android.gradle
├── gradlew.bat
├── README.md
└── gradlew
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/venom/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/venom-no-op/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/preview/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/preview/header.png
--------------------------------------------------------------------------------
/preview/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/preview/preview.gif
--------------------------------------------------------------------------------
/preview/preview.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/preview/preview.mp4
--------------------------------------------------------------------------------
/detekt-config.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 1
3 |
4 | complexity:
5 | TooManyFunctions:
6 | thresholdInClasses: 15
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | ij_kotlin_allow_trailing_comma_on_call_site = false
3 | ij_kotlin_allow_trailing_comma = false
4 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
3 | before_install:
4 | - sdk install java 17.0.1-open
5 | - sdk use java 17.0.1-open
6 |
--------------------------------------------------------------------------------
/sample/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea**
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/venom/src/main/res/drawable-hdpi/venom_dead_robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/venom/src/main/res/drawable-hdpi/venom_dead_robot.png
--------------------------------------------------------------------------------
/venom/src/main/res/drawable-mdpi/venom_dead_robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/venom/src/main/res/drawable-mdpi/venom_dead_robot.png
--------------------------------------------------------------------------------
/venom/src/main/res/drawable-xhdpi/venom_dead_robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/venom/src/main/res/drawable-xhdpi/venom_dead_robot.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/venom/src/main/res/drawable-xxhdpi/venom_dead_robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/venom/src/main/res/drawable-xxhdpi/venom_dead_robot.png
--------------------------------------------------------------------------------
/venom/src/main/res/drawable-xxxhdpi/venom_dead_robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarikSOffice/venom/HEAD/venom/src/main/res/drawable-xxxhdpi/venom_dead_robot.png
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
--------------------------------------------------------------------------------
/venom/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Aug 12 13:32:49 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/venom/src/main/res/drawable/venom_death_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Venom
3 | Process ID: %d
4 | Screen # %d
5 | Next Activity
6 | Start Venom
7 | Stop Venom
8 | Kill
9 | Cancel
10 |
--------------------------------------------------------------------------------
/venom/src/main/res/drawable/android_adb.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/venom/sample/App.kt:
--------------------------------------------------------------------------------
1 | package com.github.venom.sample
2 |
3 | import android.app.Application
4 | import com.github.venom.Venom
5 | import com.github.venom.service.NotificationConfig
6 |
7 | class App : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | val venom = Venom.createInstance(this)
12 |
13 | val notification = NotificationConfig.Builder(this)
14 | .buttonCancel(R.string.venom_notification_button_cancel_override)
15 | .buttonKill(getString(R.string.venom_notification_button_kill_override))
16 | .build()
17 | venom.initialize(notification)
18 | Venom.setGlobalInstance(venom)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - master
7 |
8 | pull_request:
9 | branches-ignore:
10 | - master
11 |
12 | jobs:
13 | jvm:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v2
19 |
20 | - name: Setup Gradle
21 | uses: gradle/gradle-build-action@v2.2.2
22 | with:
23 | cache-read-only: ${{ github.ref != 'refs/heads/master' }}
24 |
25 | - name: Validate Gradle wrapper
26 | uses: gradle/wrapper-validation-action@v1
27 |
28 | - name: Configure JDK
29 | uses: actions/setup-java@v1
30 | with:
31 | java-version: 17
32 |
33 | - name: Run all checks
34 | run: ./gradlew check --stacktrace
35 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/service/ServiceDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.github.venom.service
2 |
3 | import android.app.ActivityManager
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.core.content.ContextCompat
7 |
8 | internal class ServiceDelegate(private val context: Context) {
9 |
10 | fun startService() {
11 | if (!isServiceRunning()) {
12 | ContextCompat.startForegroundService(context, Intent(context, VenomService::class.java))
13 | }
14 | }
15 |
16 | fun stopService() {
17 | context.stopService(Intent(context, VenomService::class.java))
18 | }
19 |
20 | @Suppress("DEPRECATION")
21 | fun isServiceRunning(): Boolean {
22 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
23 | return manager.getRunningServices(Integer.MAX_VALUE)
24 | .any { it.service.className == VenomService::class.java.name }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2020 Yaroslav Berezanskyi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | include ':venom'
26 | include ':venom-no-op'
27 | include ':sample'
28 | rootProject.name = "Venom"
29 |
--------------------------------------------------------------------------------
/venom-no-op/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
24 |
25 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
23 | android.defaults.buildfeatures.buildconfig=true
24 | android.nonTransitiveRClass=false
25 | android.nonFinalResIds=false
26 |
--------------------------------------------------------------------------------
/venom/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 | #512DA8
27 | #404040
28 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | pull_request:
9 | branches:
10 | - master
11 |
12 | jobs:
13 | jvm:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v2
19 |
20 | - name: Setup Gradle
21 | uses: gradle/gradle-build-action@v2.2.2
22 | with:
23 | cache-read-only: ${{ github.ref != 'refs/heads/master' }}
24 |
25 | - name: Validate Gradle wrapper
26 | uses: gradle/wrapper-validation-action@v1
27 |
28 | - name: Configure JDK
29 | uses: actions/setup-java@v1
30 | with:
31 | java-version: 17
32 |
33 | - name: Assemble and test
34 | run: ./gradlew build --stacktrace
35 |
36 | android:
37 | runs-on: macos-latest
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | - name: Setup Gradle
44 | uses: gradle/gradle-build-action@v2.2.2
45 | with:
46 | cache-read-only: ${{ github.ref != 'refs/heads/master' }}
47 |
48 | - name: Validate Gradle wrapper
49 | uses: gradle/wrapper-validation-action@v1
50 |
51 | - name: Configure JDK
52 | uses: actions/setup-java@v1
53 | with:
54 | java-version: 17
55 |
56 | - name: Run all device checks
57 | uses: reactivecircus/android-emulator-runner@v2
58 | with:
59 | api-level: 29
60 | script: ./gradlew connectedCheck --stacktrace
61 |
--------------------------------------------------------------------------------
/venom/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 | Kill
27 | Restart
28 | Cancel
29 | Venom Service
30 | Running and ready to kill
31 |
32 |
--------------------------------------------------------------------------------
/static-analysis.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | apply plugin: "io.gitlab.arturbosch.detekt"
26 | apply plugin: "org.jlleitschuh.gradle.ktlint"
27 |
28 | android {
29 | lint {
30 | disable 'GradleDependency'
31 | warningsAsErrors true
32 | abortOnError true
33 | }
34 | }
35 |
36 | ktlint {
37 | version = rootProject.ext.ktlintVersion
38 | enableExperimentalRules = true
39 | }
40 |
41 | detekt {
42 | config = files("../detekt-config.yml")
43 | buildUponDefaultConfig = true
44 | }
45 |
--------------------------------------------------------------------------------
/venom-no-op/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | apply plugin: 'com.android.library'
26 | apply plugin: 'kotlin-android'
27 | apply from: '../common-android.gradle'
28 | apply from: '../static-analysis.gradle'
29 |
30 | android.namespace 'com.github.venom'
31 |
32 | dependencies {
33 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
34 | }
35 |
36 | project.afterEvaluate {
37 | publishing {
38 | publications {
39 | release(MavenPublication) {
40 | from components.release
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/common-android.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | apply plugin: "maven-publish"
26 |
27 | android {
28 | compileSdkVersion rootProject.ext.compileSdkVersion
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_17
31 | targetCompatibility JavaVersion.VERSION_17
32 | }
33 | kotlinOptions {
34 | jvmTarget = JavaVersion.VERSION_17
35 | }
36 |
37 | defaultConfig {
38 | minSdkVersion rootProject.ext.minSdkVersion
39 | targetSdkVersion rootProject.ext.targetSdkVersion
40 | versionName rootProject.ext.versionName
41 |
42 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | apply plugin: 'com.android.application'
26 | apply plugin: 'kotlin-android'
27 | apply from: '../static-analysis.gradle'
28 | apply from: '../common-android.gradle'
29 |
30 | android {
31 | defaultConfig.applicationId 'com.github.venom.sample'
32 | namespace 'com.github.venom.sample'
33 | }
34 |
35 | dependencies {
36 | debugImplementation project(":venom")
37 | releaseImplementation project(":venom-no-op")
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
39 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
40 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
41 | }
42 |
--------------------------------------------------------------------------------
/venom/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
24 |
25 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/venom/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
37 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/VenomPreferenceManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom
26 |
27 | import android.content.Context
28 | import android.content.Context.MODE_PRIVATE
29 | import android.content.SharedPreferences
30 |
31 | internal class VenomPreferenceManager(context: Context) {
32 |
33 | private val preference: SharedPreferences =
34 | context.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE)
35 |
36 | fun setActive(active: Boolean) {
37 | preference.edit().putBoolean(ACTIVE_KEY, active).apply()
38 | }
39 |
40 | fun isActive(): Boolean {
41 | return preference.getBoolean(ACTIVE_KEY, false)
42 | }
43 |
44 | companion object {
45 | private const val PREFERENCE_NAME = "venom"
46 | private const val ACTIVE_KEY = "active_key"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/CompositionRoot.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom
26 |
27 | import android.content.Context
28 | import com.github.venom.service.ServiceDelegate
29 | import com.github.venom.service.VenomNotificationManager
30 |
31 | internal class CompositionRoot private constructor(context: Context) {
32 | val notificationManager = VenomNotificationManager(context)
33 | val preferenceManager = VenomPreferenceManager(context)
34 | val serviceDelegate = ServiceDelegate(context)
35 |
36 | companion object {
37 | private lateinit var root: CompositionRoot
38 |
39 | fun getCompositionRoot(context: Context): CompositionRoot {
40 | if (!::root.isInitialized) {
41 | root = CompositionRoot(context)
42 | }
43 | return root
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/venom-no-op/src/main/java/com/github/venom/Venom.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | @file:Suppress("unused")
26 |
27 | package com.github.venom
28 |
29 | import android.content.Context
30 | import com.github.venom.service.NotificationConfig
31 |
32 | @Suppress("UNUSED_PARAMETER")
33 | class Venom private constructor() {
34 |
35 | @JvmOverloads
36 | fun initialize(config: NotificationConfig? = null) {
37 | // No-op
38 | }
39 |
40 | fun start() {
41 | // No-op
42 | }
43 |
44 | fun stop() {
45 | // No-op
46 | }
47 |
48 | @Suppress("FunctionOnlyReturningConstant")
49 | fun isRunning() = false
50 |
51 | companion object {
52 |
53 | @JvmStatic
54 | fun createInstance(ignored: Context) = Venom()
55 |
56 | @JvmStatic
57 | fun setGlobalInstance(ignored: Venom) {
58 | // No-op
59 | }
60 |
61 | @JvmStatic
62 | fun getGlobalInstance() = Venom()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/venom/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.venom.sample
2 |
3 | import android.Manifest.permission.POST_NOTIFICATIONS
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.os.Process
9 | import android.view.View
10 | import android.widget.TextView
11 | import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
12 | import androidx.appcompat.app.AppCompatActivity
13 | import com.github.venom.Venom
14 |
15 | class MainActivity : AppCompatActivity() {
16 |
17 | private val venom = Venom.getGlobalInstance()
18 |
19 | private val permissionLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
20 | if (isGranted) {
21 | venom.start()
22 | }
23 | }
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContentView(R.layout.activity_main)
28 |
29 | val number = intent.getIntExtra(NUMBER_ARG, 1)
30 |
31 | findViewById(R.id.pid).text = getString(R.string.pid, Process.myPid())
32 | findViewById(R.id.label).text = getString(R.string.venom_screen_label, number)
33 | findViewById(R.id.next_activity).setOnClickListener {
34 | launch(this, number + 1)
35 | }
36 | findViewById(R.id.start).setOnClickListener {
37 | verifyNotificationPermissionAndLaunchVenom()
38 | }
39 | findViewById(R.id.stop).setOnClickListener {
40 | venom.stop()
41 | }
42 | }
43 |
44 | private fun verifyNotificationPermissionAndLaunchVenom() {
45 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
46 | venom.start()
47 | } else {
48 | permissionLauncher.launch(POST_NOTIFICATIONS)
49 | }
50 | }
51 |
52 | companion object {
53 | private const val NUMBER_ARG = "number_arg"
54 |
55 | fun launch(context: Context, number: Int) {
56 | val intent = Intent(context, MainActivity::class.java)
57 | .putExtra(NUMBER_ARG, number)
58 | context.startActivity(intent)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/venom-no-op/src/main/java/com/github/venom/service/NotificationConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | @file:Suppress("unused")
26 |
27 | package com.github.venom.service
28 |
29 | import android.content.Context
30 |
31 | @Suppress("DataClassPrivateConstructor", "UNUSED_PARAMETER")
32 | data class NotificationConfig private constructor(
33 | val title: String,
34 | val text: String,
35 | val buttonKill: String,
36 | val buttonCancel: String,
37 | val iconRes: Int,
38 | val colorRes: Int
39 | ) {
40 |
41 | class Builder(c: Context) {
42 |
43 | fun title(title: String) = this
44 | fun title(title: Int) = this
45 | fun text(text: String) = this
46 | fun text(text: Int) = this
47 | fun buttonKill(text: String) = this
48 | fun buttonKill(text: Int) = this
49 | fun buttonCancel(text: String) = this
50 | fun buttonCancel(text: Int) = this
51 | fun icon(icon: Int) = this
52 | fun color(color: Int) = this
53 |
54 | fun build() = NotificationConfig("", "", "", "", 0, 0)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/venom/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | apply plugin: 'com.android.library'
26 | apply plugin: 'kotlin-android'
27 | apply from: '../static-analysis.gradle'
28 | apply from: '../common-android.gradle'
29 |
30 | android {
31 | namespace 'com.github.venom'
32 | testNamespace 'com.github.venom.test'
33 | }
34 |
35 | dependencies {
36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
37 | implementation "androidx.core:core-ktx:$androidXCoreVersion"
38 | testImplementation "junit:junit:$junitVersion"
39 | testImplementation "io.mockk:mockk:$mockkVersion"
40 | androidTestImplementation "androidx.test:rules:$androidXTestRulesVersion"
41 | androidTestImplementation "androidx.test:runner:$androidXTestVersion"
42 | androidTestImplementation "androidx.test.ext:junit:$androidXTestExtVersion"
43 | androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion"
44 | }
45 |
46 | project.afterEvaluate {
47 | publishing {
48 | publications {
49 | release(MavenPublication) {
50 | from components.release
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/service/NotificationConfig.kt:
--------------------------------------------------------------------------------
1 | package com.github.venom.service
2 |
3 | import android.content.Context
4 | import androidx.annotation.ColorRes
5 | import androidx.annotation.DrawableRes
6 | import androidx.annotation.StringRes
7 | import com.github.venom.R
8 |
9 | @Suppress("DataClassPrivateConstructor")
10 | data class NotificationConfig private constructor(
11 | val title: String,
12 | val text: String,
13 | val buttonKill: String,
14 | val buttonRestart: String,
15 | val buttonCancel: String,
16 | val iconRes: Int,
17 | val colorRes: Int
18 | ) {
19 |
20 | class Builder(private val c: Context) {
21 | private var title: String = c.getString(R.string.venom_foreground_service_title)
22 | private var text: String = c.getString(R.string.venom_foreground_service_text)
23 | private var buttonKill: String = c.getString(R.string.venom_notification_button_kill)
24 | private var buttonRestart: String = c.getString(R.string.venom_notification_button_restart)
25 | private var buttonCancel: String = c.getString(R.string.venom_notification_button_cancel)
26 | private var icon: Int = R.drawable.android_adb
27 | private var color: Int = R.color.venom_primary
28 |
29 | fun title(title: String) = apply { this.title = title }
30 | fun title(@StringRes title: Int) = apply { this.title = c.getString(title) }
31 | fun text(text: String) = apply { this.text = text }
32 | fun text(@StringRes text: Int) = apply { this.text = c.getString(text) }
33 | fun buttonKill(text: String) = apply { buttonKill = text }
34 | fun buttonKill(@StringRes text: Int) = apply { buttonKill = c.getString(text) }
35 | fun buttonRestart(text: String) = apply { buttonRestart = text }
36 | fun buttonRestart(@StringRes text: Int) = apply { buttonRestart = c.getString(text) }
37 | fun buttonCancel(text: String) = apply { buttonCancel = text }
38 | fun buttonCancel(@StringRes text: Int) = apply { buttonCancel = c.getString(text) }
39 | fun icon(@DrawableRes icon: Int) = apply { this.icon = icon }
40 | fun color(@ColorRes color: Int) = apply { this.color = color }
41 |
42 | fun build() = NotificationConfig(
43 | title,
44 | text,
45 | buttonKill,
46 | buttonRestart,
47 | buttonCancel,
48 | icon,
49 | color
50 | )
51 | }
52 | }
53 |
54 | internal fun defaultNotification(c: Context) = NotificationConfig.Builder(c).build()
55 |
--------------------------------------------------------------------------------
/venom/src/test/java/com/github/venom/service/ServiceDelegateTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.venom.service
2 |
3 | import android.app.ActivityManager
4 | import android.app.ActivityManager.RunningServiceInfo
5 | import android.content.Context
6 | import android.content.Context.ACTIVITY_SERVICE
7 | import androidx.core.content.ContextCompat
8 | import io.mockk.every
9 | import io.mockk.mockk
10 | import io.mockk.mockkStatic
11 | import io.mockk.verify
12 | import org.junit.Assert.assertFalse
13 | import org.junit.Assert.assertTrue
14 | import org.junit.Before
15 | import org.junit.Test
16 |
17 | class ServiceDelegateTest {
18 |
19 | private val context = mockk(relaxed = true)
20 |
21 | private lateinit var delegate: ServiceDelegate
22 |
23 | @Before
24 | fun setup() {
25 | delegate = ServiceDelegate(context)
26 | }
27 |
28 | @Test
29 | fun startService_startServiceIfNotRunning() {
30 | setRunningState(false)
31 |
32 | mockkStatic(ContextCompat::class) {
33 | delegate.startService()
34 | // it's not possible to verify the Intent unfortunately
35 | verify { ContextCompat.startForegroundService(any(), any()) }
36 | }
37 | }
38 |
39 | @Test
40 | fun startService_dontStartServiceIfAlreadyRunning() {
41 | setRunningState(true)
42 |
43 | mockkStatic(ContextCompat::class) {
44 | delegate.startService()
45 | verify(exactly = 0) { ContextCompat.startForegroundService(any(), any()) }
46 | }
47 | }
48 |
49 | @Test
50 | fun stopService_stopService() {
51 | delegate.stopService()
52 |
53 | verify { context.stopService(any()) }
54 | }
55 |
56 | @Test
57 | fun isServiceRunning_returnTrueIfRunning() {
58 | setRunningState(true)
59 |
60 | assertTrue(delegate.isServiceRunning())
61 | }
62 |
63 | @Test
64 | fun isServiceRunning_returnFalseIfNotRunning() {
65 | setRunningState(false)
66 |
67 | assertFalse(delegate.isServiceRunning())
68 | }
69 |
70 | @Suppress("DEPRECATION")
71 | private fun setRunningState(running: Boolean) {
72 | val serviceInfo = mockk()
73 | serviceInfo.service = mockk {
74 | every { className } returns if (running) VenomService::class.java.name else ""
75 | }
76 | every {
77 | context.getSystemService(ACTIVITY_SERVICE)
78 | } returns mockk {
79 | every { getRunningServices(any()) } returns listOf(serviceInfo)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
27 |
28 |
38 |
39 |
49 |
50 |
59 |
60 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/service/VenomService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom.service
26 |
27 | import android.app.Service
28 | import android.content.Intent
29 | import android.os.IBinder
30 | import com.github.venom.CompositionRoot
31 | import com.github.venom.VenomPreferenceManager
32 |
33 | internal class VenomService : Service() {
34 |
35 | private lateinit var prefs: VenomPreferenceManager
36 |
37 | override fun onBind(intent: Intent?): IBinder? = null
38 |
39 | override fun onCreate() {
40 | super.onCreate()
41 | val compositionRoot = CompositionRoot.getCompositionRoot(this)
42 | prefs = compositionRoot.preferenceManager
43 | val notificationManager = compositionRoot.notificationManager
44 | notificationManager.verifyNotificationChannel()
45 | startForeground(NOTIFICATION_ID, notificationManager.createNotification())
46 | }
47 |
48 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
49 | when (intent?.action) {
50 | ACTION_CANCEL -> cancelAndTerminate()
51 | }
52 | return START_NOT_STICKY
53 | }
54 |
55 | private fun cancelAndTerminate() {
56 | prefs.setActive(false)
57 | stopSelf()
58 | }
59 |
60 | companion object {
61 | private const val NOTIFICATION_ID = 200
62 | const val ACTION_CANCEL = "action_cancel"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/venom/src/test/java/com/github/venom/VenomTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.venom
2 |
3 | import android.content.Context
4 | import com.github.venom.service.ServiceDelegate
5 | import com.github.venom.service.VenomNotificationManager
6 | import com.github.venom.service.defaultNotification
7 | import io.mockk.every
8 | import io.mockk.mockk
9 | import io.mockk.verify
10 | import org.junit.Assert.assertFalse
11 | import org.junit.Assert.assertTrue
12 | import org.junit.Before
13 | import org.junit.Test
14 |
15 | class VenomTest {
16 |
17 | private val preferenceManager = mockk(relaxed = true)
18 | private val notificationManager = mockk(relaxed = true)
19 | private val delegate = mockk(relaxed = true)
20 |
21 | private lateinit var venom: Venom
22 |
23 | @Before
24 | fun setup() {
25 | venom = Venom.createInstance(preferenceManager, notificationManager, delegate)
26 | }
27 |
28 | @Test
29 | fun initialize_setNotificationConfigDefault() {
30 | venom.initialize()
31 |
32 | verify(exactly = 0) { notificationManager.config }
33 | }
34 |
35 | @Test
36 | fun initialize_setNotificationConfig() {
37 | val context = mockk()
38 | every { context.getString(any()) } returns ""
39 | val notification = defaultNotification(context)
40 |
41 | venom.initialize(notification)
42 |
43 | verify { notificationManager.config = notification }
44 | }
45 |
46 | @Test
47 | fun initialize_startIfActive() {
48 | setActive()
49 | venom.initialize()
50 |
51 | verify { delegate.startService() }
52 | }
53 |
54 | @Test
55 | fun initialize_dontStartIfInactive() {
56 | setInactive()
57 | venom.initialize()
58 |
59 | verify(exactly = 0) { delegate.startService() }
60 | }
61 |
62 | @Test
63 | fun start_persistSetting() {
64 | venom.start()
65 |
66 | verify { preferenceManager.setActive(true) }
67 | }
68 |
69 | @Test
70 | fun start_startService() {
71 | venom.start()
72 |
73 | verify { delegate.startService() }
74 | }
75 |
76 | @Test
77 | fun stop_persistSetting() {
78 | venom.stop()
79 |
80 | verify { preferenceManager.setActive(false) }
81 | }
82 |
83 | @Test
84 | fun stopService() {
85 | venom.stop()
86 |
87 | verify { delegate.stopService() }
88 | }
89 |
90 | @Test
91 | fun isRunning_returnTrueWhenRunning() {
92 | every { delegate.isServiceRunning() } returns true
93 |
94 | assertTrue(venom.isRunning())
95 | }
96 |
97 | @Test
98 | fun isRunning_returnFalseWhenNotRunning() {
99 | every { delegate.isServiceRunning() } returns false
100 |
101 | assertFalse(venom.isRunning())
102 | }
103 |
104 | private fun setActive() {
105 | every { preferenceManager.isActive() } returns true
106 | }
107 |
108 | private fun setInactive() {
109 | every { preferenceManager.isActive() } returns false
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/venom/src/androidTest/java/com/github/venom/test/VenomTestActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom.test
26 |
27 | import android.app.Activity
28 | import android.content.Context
29 | import android.content.Intent
30 | import android.os.Bundle
31 | import java.io.Serializable
32 |
33 | class VenomTestActivity : Activity() {
34 |
35 | private lateinit var arg: InputArg
36 | private var processedInput = false
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | processedInput = savedInstanceState?.getBoolean(PROCESSED_INPUT_KEY) ?: false
41 |
42 | @Suppress("DEPRECATION")
43 | arg = intent.getSerializableExtra(INPUT_ARG) as InputArg
44 | title = TITLE_PREFIX + arg.number
45 |
46 | val args = arg.args
47 | if (!processedInput && !args.isNullOrEmpty()) {
48 | processedInput = true
49 | val intents = args.map { launchIntent(this, it) }
50 | startActivities(intents.toTypedArray())
51 | }
52 | }
53 |
54 | override fun onStop() {
55 | if (arg.longStop) Thread.sleep(LONG_TASK_TIME_MILLIS)
56 | super.onStop()
57 | }
58 |
59 | override fun onSaveInstanceState(outState: Bundle) {
60 | if (arg.longSaveSate) Thread.sleep(LONG_TASK_TIME_MILLIS)
61 | outState.putBoolean(PROCESSED_INPUT_KEY, processedInput)
62 | super.onSaveInstanceState(outState)
63 | }
64 |
65 | companion object {
66 | private const val INPUT_ARG = "input"
67 | private const val PROCESSED_INPUT_KEY = "processed input"
68 | private const val LONG_TASK_TIME_MILLIS = 5000L
69 |
70 | const val TITLE_PREFIX = "Activity #"
71 |
72 | data class InputArg(
73 | val number: Int,
74 | val longStop: Boolean,
75 | val longSaveSate: Boolean,
76 | val args: ArrayList? = null
77 | ) : Serializable
78 |
79 | fun launchIntent(context: Context, arg: InputArg): Intent {
80 | return Intent(context, VenomTestActivity::class.java)
81 | .putExtra(INPUT_ARG, arg)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Venom
2 |
3 | [](https://jitpack.io/#YarikSOffice/venom)
4 | [](https://github.com/YarikSOffice/venom/actions?query=workflow%3ABuild)
5 | [](https://ktlint.github.io/)
6 | [](https://androidweekly.net/issues/issue-411/)
7 |
8 |
9 |
10 | **Venom** is a lightweight tool that simplifies testing of the process death scenario for your android application.
11 |
12 |
13 | 🎞️ Click to show the preview…
14 |
15 |
16 |
17 | ## Why Venom?
18 |
19 | The Android Run Time aggressively manages its resources and occasionally terminates background applications while the user is away interacting with other apps. In such a case, all the activities are destroyed along with application scope objects and background tasks.
20 |
21 | When the user relaunches the app, the top stack activity is restored from the saved instance state. Knowing this, your applications should always present a consistent interface and be tested against the process death scenario.
22 |
23 | Venom makes it possible to kill the app process from the notification drawer making the testing easier and more straightforward versus the traditional ways like setting the background processes limit in Developer Options or using the stop process button in Android Studio, especially for a QA team.
24 |
25 | ## Setup
26 |
27 | The setup is pretty simple:
28 |
29 | 1. Initialize the library in Application.onCreate:
30 |
31 | ```kotlin
32 | val venom = Venom.createInstance(this)
33 | venom.initialize()
34 | ```
35 |
36 | 2. Call `start`/`stop` whenever you need:
37 |
38 | ```kotlin
39 | venom.start()
40 | // or
41 | venom.stop()
42 | ```
43 | See the sample app for an example.
44 |
45 | ## Customize the notification
46 |
47 | Use the builder to customize the notification:
48 | ```kotlin
49 | val notification = NotificationConfig.Builder(this)
50 | .buttonCancel(R.string.venom_notification_button_cancel_override)
51 | .buttonKill(getString(R.string.venom_notification_button_kill_override))
52 | .buttonRestart(getString(R.string.venom_notification_button_restart_override))
53 | .build()
54 | venom.initialize(notification)
55 | ```
56 |
57 | ## Notification Permission
58 |
59 | Android 13 has introduced a new runtime permission [`POST_NOTIFICATIONS`](https://developer.android.com/reference/android/Manifest.permission#POST_NOTIFICATIONS) which is required to display any kind of notifications including ones for foreground services. Note that this permission is required for Venom to function properly.
60 |
61 | Venom **doesn't** manage the notification permission on your behalf to keep the library flexible. Please, see an example of handling that in the sample app.
62 |
63 |
64 | ## Download
65 |
66 | ```groovy
67 | repositories {
68 | maven { url 'https://jitpack.io' }
69 | }
70 |
71 | dependencies {
72 | debugImplementation "com.github.YarikSOffice.Venom:venom:0.7.1"
73 | releaseImplementation "com.github.YarikSOffice.Venom:venom-no-op:0.7.1"
74 | }
75 | ```
76 |
77 | ## License
78 |
79 | ```
80 | The MIT License (MIT)
81 |
82 | Copyright 2020 Yaroslav Berezanskyi
83 |
84 | Permission is hereby granted, free of charge, to any person obtaining a copy
85 | of this software and associated documentation files (the "Software"), to deal
86 | in the Software without restriction, including without limitation the rights
87 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
88 | copies of the Software, and to permit persons to whom the Software is
89 | furnished to do so, subject to the following conditions:
90 |
91 | The above copyright notice and this permission notice shall be included in all
92 | copies or substantial portions of the Software.
93 |
94 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
95 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
96 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
97 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
98 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
99 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
100 | SOFTWARE.
101 | ```
102 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/Venom.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom
26 |
27 | import android.content.Context
28 | import com.github.venom.service.NotificationConfig
29 | import com.github.venom.service.ServiceDelegate
30 | import com.github.venom.service.VenomNotificationManager
31 |
32 | /**
33 | * Venom is a lightweight tool that simplifies testing of the process death scenario
34 | * for your android application.
35 | *
36 | * Venom makes it possible to kill the app process from the notification drawer making the testing
37 | * easier and more straightforward in comparison with the traditional ways.
38 | */
39 | class Venom private constructor(
40 | private val prefs: VenomPreferenceManager,
41 | private val notificationManager: VenomNotificationManager,
42 | private val delegate: ServiceDelegate
43 | ) {
44 |
45 | /**
46 | * Initializes the [Venom] and invalidates its state.
47 | */
48 | @JvmOverloads
49 | fun initialize(config: NotificationConfig? = null) {
50 | config?.let { notificationManager.config = it }
51 | if (prefs.isActive()) {
52 | start()
53 | }
54 | }
55 |
56 | /**
57 | * Starts the [Venom] and puts the notification in the drawer.
58 | */
59 | fun start() {
60 | prefs.setActive(true)
61 | delegate.startService()
62 | }
63 |
64 | /**
65 | * Terminates the [Venom] and removes the notification from the drawer.
66 | */
67 | fun stop() {
68 | prefs.setActive(false)
69 | delegate.stopService()
70 | }
71 |
72 | /**
73 | * Indicates whether the [Venom] is currently running.
74 | *
75 | * @return true if the [Venom] is currently running
76 | */
77 | fun isRunning(): Boolean {
78 | return delegate.isServiceRunning()
79 | }
80 |
81 | companion object {
82 |
83 | private var instance: Venom? = null
84 |
85 | /**
86 | * Creates a new instance of [Venom].
87 | *
88 | * @param context the [Context] to be used
89 | */
90 | @JvmStatic
91 | fun createInstance(context: Context): Venom {
92 | val root = CompositionRoot.getCompositionRoot(context)
93 | return Venom(root.preferenceManager, root.notificationManager, root.serviceDelegate)
94 | }
95 |
96 | /**
97 | * Sets the global instance returned from [getGlobalInstance].
98 | *
99 | * @param venom the [Venom] instance
100 | *
101 | * @throws [IllegalStateException] if the global instance is already initialized
102 | */
103 | @JvmStatic
104 | fun setGlobalInstance(venom: Venom) {
105 | check(instance == null) { "The global instance is already initialized" }
106 | instance = venom
107 | }
108 |
109 | /**
110 | * Returns the global [Venom] instance set via [setGlobalInstance].
111 | *
112 | * @throws [IllegalStateException] if the global instance is not initialized
113 | */
114 | @JvmStatic
115 | fun getGlobalInstance(): Venom {
116 | return checkNotNull(instance) { "The global instance is not initialized" }
117 | }
118 |
119 | internal fun createInstance(
120 | prefs: VenomPreferenceManager,
121 | notificationManager: VenomNotificationManager,
122 | delegate: ServiceDelegate
123 | ): Venom = Venom(prefs, notificationManager, delegate)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/service/VenomNotificationManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom.service
26 |
27 | import android.app.Notification
28 | import android.app.NotificationChannel
29 | import android.app.NotificationManager
30 | import android.app.PendingIntent
31 | import android.app.Service.NOTIFICATION_SERVICE
32 | import android.content.Context
33 | import android.content.Intent
34 | import android.os.Build
35 | import androidx.core.app.NotificationCompat
36 | import androidx.core.app.NotificationCompat.Action
37 | import androidx.core.content.ContextCompat
38 | import com.github.venom.DeathActivity
39 | import com.github.venom.DeathActivity.LaunchMode
40 | import com.github.venom.DeathActivity.LaunchMode.KILL
41 | import com.github.venom.DeathActivity.LaunchMode.RESTART
42 | import com.github.venom.service.VenomService.Companion.ACTION_CANCEL
43 |
44 | private const val ACTIVITY_KILL_CODE = 100
45 | private const val ACTIVITY_RESTART_CODE = 200
46 | private const val SERVICE_CANCEL_CODE = 300
47 | private const val VENOM_NOTIFICATION_CHANNEL_ID = "venom_notification_channel"
48 | private const val VENOM_NOTIFICATION_CHANNEL_NAME = "Venom"
49 |
50 | internal class VenomNotificationManager(private val context: Context) {
51 |
52 | var config: NotificationConfig = defaultNotification(context)
53 |
54 | fun verifyNotificationChannel() {
55 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
56 | val channel = NotificationChannel(
57 | VENOM_NOTIFICATION_CHANNEL_ID,
58 | VENOM_NOTIFICATION_CHANNEL_NAME,
59 | NotificationManager.IMPORTANCE_LOW
60 | )
61 | val nm = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
62 | nm.createNotificationChannel(channel)
63 | }
64 |
65 | fun createNotification(): Notification {
66 | return NotificationCompat.Builder(context, VENOM_NOTIFICATION_CHANNEL_ID)
67 | .setContentTitle(config.title)
68 | .setContentText(config.text)
69 | .setSmallIcon(config.iconRes)
70 | .setColor(ContextCompat.getColor(context, config.colorRes))
71 | .addAction(createActivityAction(config.buttonKill, KILL, ACTIVITY_KILL_CODE))
72 | .addAction(createActivityAction(config.buttonRestart, RESTART, ACTIVITY_RESTART_CODE))
73 | .addAction(createCancelAction())
74 | .setPriority(NotificationCompat.PRIORITY_HIGH)
75 | .setOngoing(true)
76 | .build()
77 | }
78 |
79 | private fun createActivityAction(
80 | text: String,
81 | launchMode: LaunchMode,
82 | requestCode: Int
83 | ): Action {
84 | val intent = DeathActivity.createIntent(context, launchMode)
85 | val intentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
86 | PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
87 | } else {
88 | PendingIntent.FLAG_ONE_SHOT
89 | }
90 | val pendingIntent = PendingIntent.getActivity(
91 | context,
92 | requestCode,
93 | intent,
94 | intentFlags
95 | )
96 | return Action(0, text, pendingIntent)
97 | }
98 |
99 | private fun createCancelAction(): Action {
100 | val intent = Intent(context, VenomService::class.java).setAction(ACTION_CANCEL)
101 | val intentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
102 | PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
103 | } else {
104 | PendingIntent.FLAG_ONE_SHOT
105 | }
106 | val pendingIntent = PendingIntent.getService(
107 | context,
108 | SERVICE_CANCEL_CODE,
109 | intent,
110 | intentFlags
111 | )
112 | return Action(0, config.buttonCancel, pendingIntent)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/venom/src/main/java/com/github/venom/DeathActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom
26 |
27 | import android.app.Activity
28 | import android.app.Application.ActivityLifecycleCallbacks
29 | import android.content.Context
30 | import android.content.Intent
31 | import android.os.Bundle
32 | import android.os.Handler
33 | import android.os.Looper
34 | import android.os.Process
35 | import com.github.venom.service.VenomService
36 |
37 | internal class DeathActivity : Activity() {
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 | stopService(Intent(this, VenomService::class.java))
42 | @Suppress("DEPRECATION")
43 | val launchMode = intent.getSerializableExtra(LAUNCH_MODE) as LaunchMode
44 | application.registerActivityLifecycleCallbacks(SuicidalLifecycleCallbacks(this, launchMode))
45 | }
46 |
47 | @Deprecated("Deprecated in Java")
48 | override fun onBackPressed() {
49 | // No-op
50 | }
51 |
52 | /**
53 | * Terminates the process when the app is ready to commit suicide.
54 | *
55 | * If the system kills a process, the associated app can restore its activities that have
56 | * a state in a new process. An activity is considered to have a state in two cases:
57 | * 1. It hasn't resumed yet (an empty state).
58 | * 2. The activity manager service has gotten its last saved state. This happens right
59 | * after [onStop] and [onSaveInstanceState] events.
60 | *
61 | * Our goal is to restore the same back stack once the app is recreated. To achieve this,
62 | * we kill the process as soon as:
63 | * 1. [DeathActivity] is resumed - to remove it from the stack.
64 | * 2. The activity manager service gets the last saved state of the activities that
65 | * haven't stopped yet - to keep them in the stack.
66 | *
67 | * When [DeathActivity] is being resumed we schedule a suicide attempt with a delay.
68 | * This delay is long enough to check if there are any activities that haven't stopped yet.
69 | * In case we have them, we postpone the suicide until the activity manager service gets
70 | * the last saved state of such the activities.
71 | */
72 | private class SuicidalLifecycleCallbacks(
73 | private val activity: Activity,
74 | private val launchMode: LaunchMode
75 | ) : ActivityLifecycleCallbacks {
76 |
77 | private val handler = Handler(Looper.getMainLooper())
78 |
79 | /** Kills the app process. */
80 | private val venomousRunnable = Runnable {
81 | if (launchMode == LaunchMode.KILL) {
82 | activity.moveTaskToBack(true)
83 | activity.finish()
84 | }
85 | Process.killProcess(Process.myPid())
86 | }
87 |
88 | /** Schedules the suicide with a delay when [DeathActivity] resumes. */
89 | override fun onActivityResumed(activity: Activity) {
90 | if (activity !is DeathActivity) return
91 | handler.postDelayed(venomousRunnable, DELAY_TIMEOUT_MILLIS)
92 | }
93 |
94 | /**
95 | * Postpones the suicide until the activity manager service gets the last saved state
96 | * of every stopping activity except [DeathActivity].
97 | *
98 | * The Android framework stops an activity in the following steps:
99 | * 1. Invokes [onStop] and [onSaveInstanceState] methods along with associated
100 | * [ActivityLifecycleCallbacks] (the order depends on the targetSdkVersion).
101 | * 2. Reports the last activity saved state to the activity manager service. This *Report*
102 | * is enqueued via the UI thread [Handler] in the same call with the previous step.
103 | *
104 | * Given that, our goal is to terminate the process as soon as the *Report* is processed.
105 | * To achieve this, we use a nested [Handler.post] call:
106 | * 1. The outer runnable is enqueued inside [onActivityStopped] callback and is processed
107 | * when the *Report* is already in the message queue.
108 | * 2. We post the inner runnable which is guaranteed to be processed after the *Report*.
109 | **/
110 | override fun onActivityStopped(activity: Activity) {
111 | if (activity is DeathActivity) return
112 | // cancel the suicide and let the activity give up
113 | handler.removeCallbacksAndMessages(null)
114 | handler.post { handler.post(venomousRunnable) }
115 | }
116 |
117 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
118 | // No-op
119 | }
120 | override fun onActivityStarted(activity: Activity) {
121 | // No-op
122 | }
123 | override fun onActivityPaused(activity: Activity) {
124 | // No-op
125 | }
126 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
127 | // No-op
128 | }
129 | override fun onActivityDestroyed(activity: Activity) {
130 | // No-op
131 | }
132 | }
133 |
134 | companion object {
135 | private const val DELAY_TIMEOUT_MILLIS = 1000L // 1s
136 | private const val LAUNCH_MODE = "launch-mode"
137 |
138 | fun createIntent(context: Context, launchMode: LaunchMode): Intent {
139 | return Intent(context, DeathActivity::class.java)
140 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
141 | .putExtra(LAUNCH_MODE, launchMode)
142 | }
143 | }
144 |
145 | enum class LaunchMode {
146 | RESTART,
147 | KILL
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/venom/src/androidTest/java/com/github/venom/test/VenomTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright 2020 Yaroslav Berezanskyi
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.github.venom.test
26 |
27 | import android.app.ActivityManager
28 | import android.content.ComponentName
29 | import android.content.Context.ACTIVITY_SERVICE
30 | import android.content.Intent
31 | import android.os.Build
32 | import androidx.annotation.IntRange
33 | import androidx.annotation.RequiresApi
34 | import androidx.test.ext.junit.runners.AndroidJUnit4
35 | import androidx.test.platform.app.InstrumentationRegistry
36 | import androidx.test.uiautomator.By
37 | import androidx.test.uiautomator.UiDevice
38 | import androidx.test.uiautomator.Until
39 | import com.github.venom.Venom
40 | import com.github.venom.test.VenomTestActivity.Companion.InputArg
41 | import org.junit.After
42 | import org.junit.Assert.assertEquals
43 | import org.junit.Assert.assertTrue
44 | import org.junit.Before
45 | import org.junit.Test
46 | import org.junit.runner.RunWith
47 |
48 | private const val POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS"
49 |
50 | @RunWith(AndroidJUnit4::class)
51 | @RequiresApi(18)
52 | class VenomTest {
53 |
54 | private val instrumentation = InstrumentationRegistry.getInstrumentation()
55 | private val device = UiDevice.getInstance(instrumentation)
56 | private val appContext = instrumentation.targetContext
57 | private val venom = Venom.createInstance(appContext)
58 |
59 | @Before
60 | fun setupEach() {
61 | grantNotificationsPermission()
62 | venom.start()
63 | collapseNotifications()
64 | }
65 |
66 | private fun grantNotificationsPermission() {
67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
68 | instrumentation.uiAutomation.executeShellCommand(
69 | "pm grant ${appContext.packageName} $POST_NOTIFICATIONS"
70 | )
71 | }
72 | }
73 |
74 | @After
75 | fun tearDown() {
76 | venom.stop()
77 | }
78 |
79 | @Test
80 | fun suicide_oneActivityInForeground_restart() {
81 | launchActivities(count = 1)
82 | restartProcess()
83 | assertDisplayedActivities(count = 1)
84 | }
85 |
86 | @Test
87 | fun suicide_oneActivityInBackground_restart() {
88 | launchActivities(count = 1)
89 | moveToBackgroundAndRestartProcess()
90 | assertDisplayedActivities(count = 1)
91 | }
92 |
93 | @Test
94 | fun suicide_oneActivityInForegroundWithLongStop_restart() {
95 | launchActivities(count = 1, longStop = true)
96 | restartProcess()
97 | assertDisplayedActivities(count = 1)
98 | }
99 |
100 | @Test
101 | fun suicide_oneActivityInBackgroundWithLongStop_restart() {
102 | launchActivities(count = 1, longStop = true)
103 | moveToBackgroundAndRestartProcess()
104 | assertDisplayedActivities(count = 1)
105 | }
106 |
107 | @Test
108 | fun suicide_oneActivityInForegroundWithLongSaveState_restart() {
109 | launchActivities(count = 1, longSaveState = true)
110 | restartProcess()
111 | assertDisplayedActivities(count = 1)
112 | }
113 |
114 | @Test
115 | fun suicide_oneActivityInBackgroundWithLongSaveState_restart() {
116 | launchActivities(count = 1, longSaveState = true)
117 | moveToBackgroundAndRestartProcess()
118 | assertDisplayedActivities(count = 1)
119 | }
120 |
121 | @Test
122 | fun suicide_multipleActivityInForeground_restart() {
123 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT)
124 | restartProcess()
125 | assertDisplayedActivities(count = MULTIPLE_ACTIVITY_COUNT)
126 | }
127 |
128 | @Test
129 | fun suicide_multipleActivityInBackground_restart() {
130 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT)
131 | moveToBackgroundAndRestartProcess()
132 | assertDisplayedActivities(count = MULTIPLE_ACTIVITY_COUNT)
133 | }
134 |
135 | @Test
136 | fun suicide_multipleActivityInForegroundWithLongStop_restart() {
137 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longStop = true)
138 | restartProcess()
139 | assertDisplayedActivities(count = MULTIPLE_ACTIVITY_COUNT)
140 | }
141 |
142 | @Test
143 | fun suicide_multipleActivityInBackgroundWithLongStop_restart() {
144 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longStop = true)
145 | moveToBackgroundAndRestartProcess()
146 | assertDisplayedActivities(count = MULTIPLE_ACTIVITY_COUNT)
147 | }
148 |
149 | @Test
150 | fun suicide_multipleActivityInForegroundWithLongSaveState_restart() {
151 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longSaveState = true)
152 | restartProcess()
153 | assertDisplayedActivities(count = MULTIPLE_ACTIVITY_COUNT)
154 | }
155 |
156 | @Test
157 | fun suicide_multipleActivityInBackgroundWithLongSaveState_restart() {
158 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longSaveState = true)
159 | moveToBackgroundAndRestartProcess()
160 | assertDisplayedActivities(count = MULTIPLE_ACTIVITY_COUNT)
161 | }
162 |
163 | @Test
164 | fun stop_oneActivityInForeground_cancelNotification() {
165 | launchActivities(count = 1)
166 | stopVenom()
167 |
168 | val cancelBtn = By.desc(appContext.getString(R.string.venom_notification_button_cancel))
169 | val found = device.wait(Until.hasObject(cancelBtn), WAIT_TIMEOUT)
170 | assert(found != true)
171 | }
172 |
173 | @Test
174 | fun suicide_oneActivityInForeground_kill() {
175 | launchActivities(count = 1)
176 | killProcess()
177 | assertProcessDeath()
178 | }
179 |
180 | @Test
181 | fun suicide_oneActivityInBackground_kill() {
182 | launchActivities(count = 1)
183 | moveToBackgroundAndKillProcess()
184 | assertProcessDeath()
185 | }
186 |
187 | @Test
188 | fun suicide_oneActivityInForegroundWithLongStop_kill() {
189 | launchActivities(count = 1, longStop = true)
190 | killProcess()
191 | assertProcessDeath()
192 | }
193 |
194 | @Test
195 | fun suicide_oneActivityInBackgroundWithLongStop_kill() {
196 | launchActivities(count = 1, longStop = true)
197 | moveToBackgroundAndKillProcess()
198 | assertProcessDeath()
199 | }
200 |
201 | @Test
202 | fun suicide_oneActivityInForegroundWithLongSaveState_kill() {
203 | launchActivities(count = 1, longSaveState = true)
204 | killProcess()
205 | assertProcessDeath()
206 | }
207 |
208 | @Test
209 | fun suicide_oneActivityInBackgroundWithLongSaveState_kill() {
210 | launchActivities(count = 1, longSaveState = true)
211 | moveToBackgroundAndKillProcess()
212 | assertProcessDeath()
213 | }
214 |
215 | @Test
216 | fun suicide_multipleActivityInForeground_kill() {
217 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT)
218 | killProcess()
219 | assertProcessDeath()
220 | }
221 |
222 | @Test
223 | fun suicide_multipleActivityInBackground_kill() {
224 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT)
225 | moveToBackgroundAndKillProcess()
226 | assertProcessDeath()
227 | }
228 |
229 | @Test
230 | fun suicide_multipleActivityInForegroundWithLongStop_kill() {
231 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longStop = true)
232 | killProcess()
233 | assertProcessDeath()
234 | }
235 |
236 | @Test
237 | fun suicide_multipleActivityInBackgroundWithLongStop_kill() {
238 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longStop = true)
239 | moveToBackgroundAndKillProcess()
240 | assertProcessDeath()
241 | }
242 |
243 | @Test
244 | fun suicide_multipleActivityInForegroundWithLongSaveState_kill() {
245 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longSaveState = true)
246 | killProcess()
247 | assertProcessDeath()
248 | }
249 |
250 | @Test
251 | fun suicide_multipleActivityInBackgroundWithLongSaveState_kill() {
252 | launchActivities(count = MULTIPLE_ACTIVITY_COUNT, longSaveState = true)
253 | moveToBackgroundAndKillProcess()
254 | assertProcessDeath()
255 | }
256 |
257 | private fun launchActivities(
258 | @IntRange(from = 1) count: Int,
259 | longStop: Boolean = false,
260 | longSaveState: Boolean = false
261 | ) {
262 | val args = arrayListOf()
263 | for (i in 0 until count) {
264 | val top = i == count - 1
265 | val arg = InputArg(
266 | number = i + 1,
267 | longStop = longStop && top,
268 | longSaveSate = longSaveState && top
269 | )
270 | args.add(arg)
271 | }
272 | val rootArg = args.removeAt(0).copy(args = args)
273 | val rootIntent = VenomTestActivity.launchIntent(context = appContext, arg = rootArg)
274 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
275 | appContext.startActivity(rootIntent)
276 | device.wait(Until.hasObject(activitySelector(count)), WAIT_TIMEOUT)
277 | }
278 |
279 | private fun restartProcess() {
280 | val restartBtn = By.desc(appContext.getString(R.string.venom_notification_button_restart))
281 |
282 | device.openNotification()
283 | device.wait(Until.findObject(restartBtn), WAIT_TIMEOUT)
284 | device.findObject(restartBtn).click()
285 | collapseNotifications()
286 |
287 | device.wait(Until.gone(activitySelector(activityCount())), WAIT_TIMEOUT)
288 | device.wait(Until.hasObject(anyActivitySelector), WAIT_TIMEOUT)
289 | }
290 |
291 | private fun killProcess() {
292 | val killBtn = By.desc(appContext.getString(R.string.venom_notification_button_kill))
293 |
294 | device.openNotification()
295 | device.wait(Until.findObject(killBtn), WAIT_TIMEOUT)
296 | device.findObject(killBtn).click()
297 | collapseNotifications()
298 | }
299 |
300 | private fun stopVenom() {
301 | val cancelBtn = By.desc(appContext.getString(R.string.venom_notification_button_cancel))
302 | device.openNotification()
303 | device.wait(Until.findObject(cancelBtn), WAIT_TIMEOUT)
304 | device.findObject(cancelBtn).click()
305 | collapseNotifications()
306 |
307 | device.wait(Until.hasObject(activitySelector(activityCount())), WAIT_TIMEOUT)
308 | device.openNotification()
309 | }
310 |
311 | private fun collapseNotifications() {
312 | @Suppress("DEPRECATION") // still eligible for tests
313 | appContext.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
314 | }
315 |
316 | @Suppress("DEPRECATION")
317 | private fun activityCount(): Int {
318 | val am = appContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager
319 | val testActivitiesTask = am.getRunningTasks(Int.MAX_VALUE)
320 | .firstOrNull { it.baseActivity?.className == VenomTestActivity::class.qualifiedName }
321 | return testActivitiesTask?.numActivities ?: 0
322 | }
323 |
324 | private fun assertDisplayedActivities(count: Int) {
325 | assertEquals("Activity count", count, activityCount())
326 | assertTrue("Activity displayed", device.hasObject(activitySelector(count)))
327 | }
328 |
329 | private fun moveToBackgroundAndRestartProcess() {
330 | device.pressHome()
331 | restartProcess()
332 | }
333 |
334 | private fun moveToBackgroundAndKillProcess() {
335 | device.pressHome()
336 | killProcess()
337 | }
338 |
339 | @Suppress("DEPRECATION")
340 | private fun assertProcessDeath() {
341 | val componentName = ComponentName(appContext, VenomTestActivity::class.java)
342 | val testActivity = appContext.packageManager.getActivityInfo(componentName, 0)
343 | val am = appContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager
344 | val repeat = PROCESS_DEATH_ASSERT_TIMES
345 | var result: Result? = null
346 | repeat(repeat) {
347 | val testProcesses = am.runningAppProcesses
348 | .filter { it.processName == testActivity.processName }
349 | .map { it.processName }
350 | result = runCatching {
351 | val message = "Process list must be empty $testProcesses"
352 | assertTrue(message, testProcesses.isEmpty())
353 | }.onFailure {
354 | Thread.sleep(PROCESS_DEATH_ASSERT_DURATION / repeat)
355 | }.onSuccess {
356 | return
357 | }
358 | }
359 | requireNotNull(result).getOrThrow()
360 | }
361 |
362 | companion object {
363 | private const val PROCESS_DEATH_ASSERT_DURATION = 15000L
364 | private const val PROCESS_DEATH_ASSERT_TIMES = 10
365 | private const val WAIT_TIMEOUT = 10000L
366 | private const val MULTIPLE_ACTIVITY_COUNT = 5
367 |
368 | private val anyActivitySelector = By.textStartsWith(VenomTestActivity.TITLE_PREFIX)
369 | private fun activitySelector(number: Int) = By.text(VenomTestActivity.TITLE_PREFIX + number)
370 | }
371 | }
372 |
--------------------------------------------------------------------------------