├── app
├── .gitignore
├── src
│ └── main
│ │ ├── 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
│ │ │ ├── dimens.xml
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable
│ │ │ ├── ic_stop.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ └── ic_camcorder.xml
│ │ ├── values-night
│ │ │ └── styles.xml
│ │ ├── menu
│ │ │ └── menu_scrolling.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ └── layout
│ │ │ └── activity_main.xml
│ │ ├── java
│ │ └── dev
│ │ │ └── bmcreations
│ │ │ └── scrcast
│ │ │ └── app
│ │ │ └── list
│ │ │ ├── StateCallbackActivity.kt
│ │ │ ├── StateObserverActivity.kt
│ │ │ ├── SimpleNotificationProvider.kt
│ │ │ ├── FABExtensions.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── jvm
│ │ │ └── JavaMainActivity.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── library
├── consumer-rules.pro
├── .gitignore
├── gradle.properties
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ └── strings.xml
│ │ └── drawable
│ │ │ ├── ic_stop.xml
│ │ │ ├── ic_pause.xml
│ │ │ ├── ic_resume.xml
│ │ │ ├── ic_camcorder.xml
│ │ │ └── ic_storage_permission_dialog.xml
│ │ ├── java
│ │ └── dev
│ │ │ └── bmcreations
│ │ │ └── scrcast
│ │ │ ├── extensions
│ │ │ └── FeatureCompatibility.kt
│ │ │ ├── internal
│ │ │ ├── recorder
│ │ │ │ ├── service
│ │ │ │ │ ├── Orientations.kt
│ │ │ │ │ └── RecorderService.kt
│ │ │ │ ├── Action.kt
│ │ │ │ ├── State.kt
│ │ │ │ ├── receiver
│ │ │ │ │ └── RecordingNotificationReceiver.kt
│ │ │ │ └── notification
│ │ │ │ │ └── RecorderNotificationProvider.kt
│ │ │ ├── extensions
│ │ │ │ └── Timer.kt
│ │ │ ├── request
│ │ │ │ └── RecordScreen.kt
│ │ │ └── config
│ │ │ │ └── dsl
│ │ │ │ └── OptionsDSL.kt
│ │ │ ├── recorder
│ │ │ ├── RecordingCallbacks.kt
│ │ │ ├── notification
│ │ │ │ └── NotificationProvider.kt
│ │ │ └── RecordingState.kt
│ │ │ ├── config
│ │ │ └── Options.kt
│ │ │ └── ScrCast.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── lifecycle
├── consumer-rules.pro
├── .gitignore
├── gradle.properties
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── dev
│ │ │ └── bmcreations
│ │ │ └── scrcast
│ │ │ └── lifecycle
│ │ │ └── ScrCastLifecycleObserver.kt
│ ├── test
│ │ └── java
│ │ │ └── dev
│ │ │ └── bmcreations
│ │ │ └── scrcast
│ │ │ └── livedata
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── dev
│ │ └── bmcreations
│ │ └── scrcast
│ │ └── livedata
│ │ └── ExampleInstrumentedTest.kt
├── README.md
├── build.gradle
└── proguard-rules.pro
├── logo.png
├── readme-header.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── docs
└── css
│ └── site.css
├── .github
├── ISSUE_TEMPLATE
│ ├── question.md
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── android.yml
│ ├── dokka.yml
│ └── on-release-publish.yml
├── install_archives.sh
├── upload_archives.sh
├── settings.gradle
├── .gitignore
├── deploy-docs.sh
├── android-library.gradle
├── mkdocs.yml
├── gradle.properties
├── gradlew.bat
├── dependencies.gradle
├── README.md
├── gradlew
├── CHANGELOG.md
└── LICENSE.txt
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/library/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lifecycle/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/lifecycle/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/logo.png
--------------------------------------------------------------------------------
/readme-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/readme-header.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/library/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=scrcast
2 | POM_NAME=scrcast
3 | POM_PACKAGING=aar
4 | VERSION_NAME=0.4.0-SNAPSHOT
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/lifecycle/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=scrcast-lifecycle
2 | POM_NAME=scrcast-lifecycle
3 | POM_PACKAGING=aar
4 | VERSION_NAME=0.1.0
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/docs/css/site.css:
--------------------------------------------------------------------------------
1 | .md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 {
2 | font-weight: bold;
3 | color: #353535;
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmcreations/scrcast/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question
4 | title: ''
5 | labels: question
6 | assignees: bmc08gt
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #B5332A
4 |
5 |
--------------------------------------------------------------------------------
/install_archives.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Clean any previous Dokka docs.
4 | rm -rf docs/api
5 |
6 | # Build the new Dokka docs.
7 | ./gradlew clean installArchives --no-daemon --no-parallel
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 180dp
3 | 16dp
4 | 16dp
5 |
--------------------------------------------------------------------------------
/lifecycle/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pause
4 | Resume
5 | Stop
6 |
7 |
--------------------------------------------------------------------------------
/upload_archives.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Deploy updated docs
4 | sh deploy-docs.sh
5 |
6 | # Upload release to maven
7 | ./gradlew uploadArchives -PSONATYPE_NEXUS_USERNAME=$SONATYPE_NEXUS_USERNAME -PSONATYPE_NEXUS_PASSWORD=$SONATYPE_NEXUS_PASSWORD --no-daemon --no-parallel
8 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | maven {
5 | url "https://dl.bintray.com/kotlin/kotlin-eap"
6 | }
7 | }
8 | }
9 |
10 | include ':library'
11 | include ':app'
12 | include ':lifecycle'
13 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jan 10 12:50:47 EST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BB86FC
4 | #6200EE
5 | #3700B3
6 | #03DAC5
7 | #FF4081
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_stop.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_stop.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_pause.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_resume.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lifecycle/README.md:
--------------------------------------------------------------------------------
1 | # Lifecycle-aware support
2 |
3 | To add Lifecycle support, import the extension library:
4 |
5 | ```kotlin
6 | implementation("dev.bmcreations:scrcast-lifecycle:0.1.0")
7 | ```
8 |
9 | You can now observe RecordingState changes via an Observer:
10 |
11 | ```kotlin
12 | recorder.observeRecordingState(lifecycleOwner, { state -> handleRecorderState(state) })
13 | ```
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea/compiler.xml
11 | /.idea/misc.xml
12 | /.idea
13 | .DS_Store
14 | /build
15 | /captures
16 | .externalNativeBuild
17 | .cxx
18 | local.properties
19 |
20 | # Docs
21 | docs/api
22 | site
23 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ trunk ]
6 |
7 | jobs:
8 | build:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: set up JDK 1.8
15 | uses: actions/setup-java@v1
16 | with:
17 | java-version: 1.8
18 | - name: Build with Gradle
19 | run: ./gradlew :library:build
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/dokka.yml:
--------------------------------------------------------------------------------
1 | name: Dokka deploy
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: set up JDK 1.8
17 | uses: actions/setup-java@v1
18 | with:
19 | java-version: 1.8
20 | - name: Build with Gradle
21 | run: ./deploy-docs.sh
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_camcorder.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/extensions/FeatureCompatibility.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.extensions
2 |
3 | import android.media.MediaRecorder
4 | import android.os.Build
5 |
6 | /**
7 | * Whether the target device supports pause and resume operations via [MediaRecorder]
8 | * (e.g the device API level is [Build.VERSION_CODES.N] or higher)
9 | */
10 | val supportsPauseResume = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
11 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_camcorder.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/bmcreations/scrcast/app/list/StateCallbackActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.app.list
2 |
3 | class StateCallbackActivity : MainActivity() {
4 |
5 | override fun onResume() {
6 | super.onResume()
7 | recorder.onRecordingStateChange { state -> handleRecorderState(state) }
8 | }
9 |
10 | override fun onStop() {
11 | super.onStop()
12 | recorder.onRecordingStateChange { }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_scrolling.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lifecycle/src/test/java/dev/bmcreations/scrcast/livedata/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.livedata
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_storage_permission_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/bmcreations/scrcast/app/list/StateObserverActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.app.list
2 |
3 | import android.os.Bundle
4 | import androidx.lifecycle.Observer
5 | import dev.bmcreations.scrcast.lifecycle.observeRecordingState
6 |
7 | class StateObserverActivity : MainActivity() {
8 |
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | recorder.observeRecordingState(this, Observer { state -> handleRecorderState(state) })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/recorder/service/Orientations.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.recorder.service
2 |
3 | import android.util.SparseIntArray
4 | import android.view.Surface
5 | import androidx.annotation.RestrictTo
6 |
7 | @RestrictTo(RestrictTo.Scope.LIBRARY)
8 | val orientations = SparseIntArray().apply {
9 | append(Surface.ROTATION_0, 90)
10 | append(Surface.ROTATION_90, 0)
11 | append(Surface.ROTATION_180, 270)
12 | append(Surface.ROTATION_270, 180)
13 | }
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: bmc08gt
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is.
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/workflows/on-release-publish.yml:
--------------------------------------------------------------------------------
1 | name: Android Release CI
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 |
8 | jobs:
9 | apk:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v1
15 | with:
16 | ref: ${{ github.event.inputs.name }}
17 |
18 | - name: set up JDK 1.8
19 | uses: actions/setup-java@v1
20 | with:
21 | java-version: 1.8
22 |
23 | - name: Build project
24 | run: bash ./upload_archives.sh
25 |
--------------------------------------------------------------------------------
/deploy-docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Clean any previous Dokka docs.
4 | rm -rf docs/api
5 |
6 | # Build the Dokka docs.
7 | ./gradlew clean :library:dokka :lifecycle:dokka
8 |
9 | # Copy outside files into the docs folder.
10 | sed -e '/full configuration details and documentation here/ { N; d; }' < README.md > docs/index.md
11 | cp readme-header.png docs/
12 | cp lifecycle/README.md docs/lifecycle.md
13 |
14 | # Deploy to Github pages.
15 | python3 -m mkdocs gh-deploy --force --verbose
16 |
17 | # Clean up.
18 | rm -rf docs/index.md docs/lifecycle.md docs/readme-header.png
19 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/extensions/Timer.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.extensions
2 |
3 | import android.os.CountDownTimer
4 |
5 |
6 | fun Long.countdown(repeatMillis: Long = 0, onTick: (millis: Long) -> Unit, after: () -> Unit) {
7 | val timer = object : CountDownTimer(this, repeatMillis) {
8 | override fun onFinish() {
9 | after()
10 | }
11 |
12 | override fun onTick(millisUntilFinished: Long) {
13 | onTick(millisUntilFinished)
14 | }
15 |
16 | }
17 | timer.start()
18 | }
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **To Reproduce**
17 | How can we reproduce this?
18 |
19 | **Logs/Screenshots**
20 | If applicable, add logs or screenshots to help explain your problem.
21 |
22 | **Version**
23 | What library version are you using? Also does this occur on a specific API level or Android device.
24 |
--------------------------------------------------------------------------------
/lifecycle/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootProject.projectDir/android-library.gradle"
2 |
3 | apply plugin: "org.jetbrains.dokka"
4 | apply plugin: "com.vanniktech.maven.publish"
5 |
6 | afterEvaluate {
7 | dokka {
8 | outputDirectory = "$rootDir/docs/api"
9 | outputFormat = "gfm"
10 | configuration {
11 | includeNonPublic = false
12 | reportUndocumented = true
13 | jdkVersion = 8
14 | skipDeprecated = true
15 | skipEmptyPackages = true
16 | }
17 | }
18 | }
19 |
20 | dependencies {
21 | implementation(project(":library"))
22 | implementation deps.jetpack.lifecycle_extensions
23 | implementation deps.jetpack.lifecycle_livedata_ktx
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/lifecycle/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/recorder/RecordingCallbacks.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.recorder
2 |
3 | import dev.bmcreations.scrcast.ScrCast
4 | import java.io.File
5 |
6 | /**
7 | * Callback for [RecordingState] changes during a recording session.
8 | *
9 | * @see [RecordingState]
10 | * @see [ScrCast.setRecordingCallback]
11 | */
12 | interface RecordingCallbacks {
13 | /**
14 | * Triggered when the state changes for the current recording session.
15 | */
16 | fun onStateChange(state: RecordingState)
17 | fun onRecordingFinished(file: File)
18 | }
19 |
20 | /**
21 | * Kotlin accessible lambda for [RecordingCallbacks], used with [ScrCast.setRecordingCallback]
22 | */
23 | internal typealias RecordingStateChangeCallback = (RecordingState) -> Unit
24 | internal typealias RecordingOutputFileCallback = (File) -> Unit
25 |
--------------------------------------------------------------------------------
/lifecycle/src/androidTest/java/dev/bmcreations/scrcast/livedata/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.livedata
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("dev.bmcreations.scrcast.livedata.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/request/RecordScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.request
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.media.projection.MediaProjectionManager
7 | import androidx.activity.result.ActivityResult
8 | import androidx.activity.result.contract.ActivityResultContract
9 | import androidx.annotation.RestrictTo
10 | @RestrictTo(RestrictTo.Scope.LIBRARY)
11 |
12 | class RecordScreen : ActivityResultContract() {
13 | override fun createIntent(context: Context, input: Void?): Intent {
14 | val pm = context.getSystemService(MediaProjectionManager::class.java)
15 | return pm.createScreenCaptureIntent()
16 | }
17 |
18 | override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult {
19 | return ActivityResult(resultCode, intent)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/android-library.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 | apply plugin: "com.github.ben-manes.versions"
6 |
7 | android {
8 | compileSdkVersion androidbuild.compileSdkVersion
9 |
10 | defaultConfig {
11 | minSdkVersion androidbuild.minSdkVersion
12 | targetSdkVersion androidbuild.targetSdkVersion
13 | testInstrumentationRunner androidbuild.testInstrumentationRunner
14 | }
15 |
16 | androidExtensions {
17 | experimental = true
18 | }
19 |
20 | kotlinOptions {
21 | jvmTarget = "1.8"
22 | freeCompilerArgs += "-Xjvm-default=compatibility"
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | debug {
31 | minifyEnabled false
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/recorder/Action.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.recorder
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.RestrictTo
5 | import androidx.annotation.StringRes
6 | import dev.bmcreations.scrcast.R
7 |
8 | @RestrictTo(RestrictTo.Scope.LIBRARY)
9 | sealed class Action(
10 | val name: String,
11 | val requestId: Int,
12 | @StringRes val label: Int,
13 | @DrawableRes val icon: Int
14 | ) {
15 | object Pause : Action(
16 | ACTION_PAUSE, 1, R.string.pause, R.drawable.ic_pause)
17 | object Resume : Action(
18 | ACTION_RESUME, 0, R.string.resume, R.drawable.ic_resume)
19 | object Stop : Action(
20 | ACTION_STOP, 2, R.string.stop, R.drawable.ic_stop)
21 | }
22 |
23 | @RestrictTo(RestrictTo.Scope.LIBRARY)
24 | const val ACTION_PAUSE = "scrcast.internal.action.PAUSE"
25 | @RestrictTo(RestrictTo.Scope.LIBRARY)
26 | const val ACTION_RESUME = "scrcast.internal.action.RESUME"
27 | @RestrictTo(RestrictTo.Scope.LIBRARY)
28 | const val ACTION_STOP = "scrcast.internal.action.STOP"
29 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootProject.projectDir/android-library.gradle"
2 |
3 | apply plugin: "org.jetbrains.dokka"
4 | apply plugin: "com.vanniktech.maven.publish"
5 |
6 | afterEvaluate {
7 | dokka {
8 | outputDirectory = "$rootDir/docs/api"
9 | outputFormat = "gfm"
10 | configuration {
11 | includeNonPublic = false
12 | reportUndocumented = true
13 | jdkVersion = 8
14 | skipDeprecated = true
15 | skipEmptyPackages = true
16 |
17 | // suppress internal operations from logging to API docs
18 | perPackageOption {
19 | prefix = "dev.bmcreations.scrcast.internal"
20 | suppress = true
21 | }
22 | }
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation deps.dexter
28 | implementation deps.jetpack.activity
29 | implementation deps.jetpack.appcompat
30 | implementation deps.jetpack.fragment
31 | implementation deps.jetpack.localbroadcast
32 |
33 | implementation deps.kotlin.coroutinesCore
34 | implementation deps.kotlin.coroutinesAndroid
35 | }
36 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/recorder/State.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.recorder
2 |
3 | import androidx.annotation.RestrictTo
4 | import dev.bmcreations.scrcast.recorder.RecordingState
5 |
6 | @RestrictTo(RestrictTo.Scope.LIBRARY)
7 | const val STATE_RECORDING = "scrcast.internal.state.RECORDING"
8 |
9 | @RestrictTo(RestrictTo.Scope.LIBRARY)
10 | const val STATE_IDLE = "scrcast.internal.state.IDLE"
11 |
12 | @RestrictTo(RestrictTo.Scope.LIBRARY)
13 | const val STATE_PAUSED = "scrcast.internal.state.PAUSED"
14 |
15 | @RestrictTo(RestrictTo.Scope.LIBRARY)
16 | const val STATE_DELAY = "scrcast.internal.state.DELAY"
17 |
18 | @RestrictTo(RestrictTo.Scope.LIBRARY)
19 | const val EXTRA_DELAY_REMAINING = "extra_sec_remaining"
20 |
21 | @RestrictTo(RestrictTo.Scope.LIBRARY)
22 | const val EXTRA_ERROR = "extra_error"
23 |
24 | fun RecordingState.stateString(): String {
25 | return when (this) {
26 | RecordingState.Recording -> STATE_RECORDING
27 | is RecordingState.Idle -> STATE_IDLE
28 | RecordingState.Paused -> STATE_PAUSED
29 | is RecordingState.Delay -> STATE_DELAY
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | # Project information
2 | site_name: 'scrcast'
3 | site_description: 'Your drop in screen recording solution on Android'
4 | site_author: 'bmcreations'
5 | site_url: 'https://bmcreations.dev'
6 | remote_branch: gh-pages
7 |
8 | # Repository
9 | repo_name: 'scrcast'
10 | repo_url: 'https://github.com/bmc08gt/scrcast'
11 |
12 | # Copyright
13 | copyright: 'Copyright © 2020 bmcreations'
14 |
15 | # Configuration
16 | theme:
17 | name: 'material'
18 | language: 'en'
19 | palette:
20 | primary: 'white'
21 | accent: 'white'
22 | font:
23 | text: 'Roboto'
24 | code: 'Roboto Mono'
25 |
26 | # Navigation
27 | nav:
28 | - 'Overview': index.md
29 | - 'Lifecycle': lifecycle.md
30 | - 'API':
31 | - 'scrcast': api/library/index.md
32 | - 'scrcast-lifecycle': api/lifecycle/index.md
33 |
34 | # CSS
35 | extra_css:
36 | - 'css/site.css'
37 |
38 | # Extensions
39 | markdown_extensions:
40 | - admonition
41 | - codehilite:
42 | guess_lang: false
43 | - footnotes
44 | - toc:
45 | permalink: true
46 | - pymdownx.betterem
47 | - pymdownx.superfences
48 |
49 | # Plugins
50 | plugins:
51 | - search
52 | - minify:
53 | minify_html: true
54 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/recorder/receiver/RecordingNotificationReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.recorder.receiver
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.annotation.RestrictTo
7 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
8 | import dev.bmcreations.scrcast.internal.recorder.ACTION_PAUSE
9 | import dev.bmcreations.scrcast.internal.recorder.ACTION_RESUME
10 | import dev.bmcreations.scrcast.internal.recorder.ACTION_STOP
11 |
12 | @RestrictTo(RestrictTo.Scope.LIBRARY)
13 | class RecordingNotificationReceiver : BroadcastReceiver() {
14 |
15 | override fun onReceive(context: Context?, intent: Intent?) {
16 | context?.let { ctx ->
17 | val broadcaster = LocalBroadcastManager.getInstance(ctx)
18 |
19 | intent?.action?.let { action ->
20 | when (action) {
21 | ACTION_STOP,
22 | ACTION_PAUSE,
23 | ACTION_RESUME -> {
24 | broadcaster.sendBroadcast(Intent(action))
25 | }
26 | else -> {}
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-android-extensions'
5 | }
6 |
7 | android {
8 | compileSdkVersion 29
9 |
10 | defaultConfig {
11 | applicationId "dev.bmcreations.scrcast.app"
12 | minSdkVersion androidbuild.minSdkVersion
13 | targetSdkVersion androidbuild.targetSdkVersion
14 | versionCode 1
15 | versionName "1.0"
16 | testInstrumentationRunner androidbuild.testInstrumentationRunner
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation project(":library")
38 | implementation project(":lifecycle")
39 |
40 | implementation deps.kotlin.stdlib
41 | implementation deps.jetpack.corektx
42 | implementation deps.jetpack.constraintlayout
43 | implementation deps.jetpack.appcompat
44 | implementation deps.jetpack.material
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/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 | # Maven
24 | GROUP=dev.bmcreations
25 |
26 | POM_DESCRIPTION=Drop-in screen recording solution on Android
27 | POM_INCEPTION_YEAR=2020
28 |
29 | POM_URL=https://github.com/bmcreations/scrcast
30 | POM_SCM_URL=https://github.com/bmcreations/scrncast
31 | POM_SCM_CONNECTION=scm:git:git://github.com/bmcreations/scrcast.git
32 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/bmcreations/scrcast.git
33 |
34 | POM_LICENCE_NAME=The Apache License, Version 2.0
35 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
36 | POM_LICENCE_DIST=repo
37 |
38 | POM_DEVELOPER_ID=brandonmcansh
39 | POM_DEVELOPER_NAME=Brandon McAnsh
40 |
41 | systemProp.org.gradle.internal.publish.checksums.insecure=true
42 | systemProp.org.gradle.internal.http.socketTimeout=120000
43 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/recorder/notification/NotificationProvider.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.recorder.notification
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.content.Context
7 | import android.os.Build
8 | import androidx.annotation.RequiresApi
9 | import dev.bmcreations.scrcast.ScrCast
10 | import dev.bmcreations.scrcast.recorder.RecordingState
11 |
12 | /**
13 | * Provider contract for managing the recording notification used by [ScrCast]
14 | */
15 | abstract class NotificationProvider(private val context: Context) {
16 | /**
17 | * [NotificationManager] instantiated via the provided [Context], available to subclasses
18 | * for [NotificationManager.notify] and other purposes
19 | */
20 | protected val notificationManager: NotificationManager by lazy {
21 | context.getSystemService(NotificationManager::class.java)
22 | }
23 |
24 | @RequiresApi(Build.VERSION_CODES.O)
25 | /**
26 | * Required method for subclasses to define their own [NotificationChannel]
27 | */
28 | abstract fun createNotificationChannel()
29 | /**
30 | * Required method for subclasses to define their [Notification] unique identifier,
31 | * queried by [update] to update the notification on state changes if required.
32 | */
33 | abstract fun getNotificationId(): Int
34 | /**
35 | * Required method for subclasses to define their [NotificationChannel] unique identifier.
36 | */
37 | abstract fun getChannelId(): String
38 | /**
39 | * Required method for subclasses to provide their [Notification] to the recording service.
40 | */
41 | abstract fun get(state: RecordingState): Notification
42 |
43 | /**
44 | * Updates the current notification based on the new [RecordingState].
45 | *
46 | * Can be overridden to handle internal events related to your [Notification].
47 | *
48 | * (e.g handle timer during pause/resume)
49 | */
50 | open fun update(state: RecordingState) {
51 | notificationManager.notify(getNotificationId(), get(state))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/bmcreations/scrcast/app/list/SimpleNotificationProvider.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.app.list
2 |
3 | import android.app.Notification
4 | import android.content.Context
5 | import android.os.Build
6 | import dev.bmcreations.scrcast.app.R
7 | import dev.bmcreations.scrcast.recorder.RecordingState
8 | import dev.bmcreations.scrcast.recorder.notification.NotificationProvider
9 |
10 | class SimpleNotificationProvider(private val context: Context) : NotificationProvider(context) {
11 |
12 |
13 | init {
14 | createNotificationChannel()
15 | }
16 |
17 | override fun getChannelId(): String = CHANNEL_ID
18 |
19 | override fun get(state: RecordingState): Notification {
20 | val builder = with(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
21 | Notification.Builder(context, CHANNEL_ID)
22 | } else {
23 | Notification.Builder(context)
24 | }) {
25 | setOngoing(true)
26 | setContentTitle("scrcast-sample")
27 | setContentText(when (state) {
28 | RecordingState.Recording -> "state=recording"
29 | is RecordingState.Idle -> "state=idle"
30 | RecordingState.Paused -> "state=paused"
31 | is RecordingState.Delay -> "state=delay"
32 | })
33 |
34 | setSmallIcon(R.drawable.ic_camcorder)
35 | }
36 |
37 | return builder.build()
38 | }
39 |
40 | override fun createNotificationChannel() {
41 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
42 | val notificationChannel = android.app.NotificationChannel(
43 | CHANNEL_ID,
44 | CHANNEL_NAME,
45 | android.app.NotificationManager.IMPORTANCE_HIGH
46 | ).apply {
47 | lightColor = android.graphics.Color.BLUE
48 | lockscreenVisibility = Notification.VISIBILITY_PUBLIC
49 | }
50 |
51 | notificationManager.createNotificationChannel(notificationChannel)
52 | }
53 | }
54 |
55 | override fun getNotificationId(): Int = 2000
56 |
57 | companion object {
58 | private const val CHANNEL_ID = "1338"
59 | private const val CHANNEL_NAME = "Recording Service Provided"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/recorder/RecordingState.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.recorder
2 |
3 | import dev.bmcreations.scrcast.ScrCast
4 | /**
5 | * Explicit defined state's during a recording session via [ScrCast]
6 | */
7 | sealed class RecordingState {
8 | /** Defines when the session is in an active recording, non paused state */
9 | object Recording : RecordingState()
10 | /** Defines when the session is idle, either before a first session has started,
11 | * after a session has ended, or when an error has occurred.
12 | *
13 | * If it's idle become of an error, [error] will be non-null.
14 | */
15 | data class Idle(
16 | /** If this is non-null, we are Idle due to an error */
17 | val error: Throwable? = null
18 | ) : RecordingState()
19 | /** Defines when the session is in an active recording, but is currently paused */
20 | object Paused : RecordingState()
21 | /** Defines when the session is in a non idle, start is currently in a "start delay",
22 | * with the count of remaining seconds until the start is transferred to
23 | * either [Recording] if the session successfully starts, or to [Idle] with [Idle.error] being non-null.
24 | */
25 | data class Delay(
26 | /** Number of remaining seconds until the session is attempted to be started */
27 | val remainingSeconds: Int
28 | ): RecordingState()
29 |
30 | /**
31 | * Convenience state query for when the state is [Recording]
32 | */
33 | val isRecording: Boolean get() = this == Recording
34 | /**
35 | * Convenience state query for when the state is [Idle] and [Idle.error] is null
36 | * (e.g not in an error state)
37 | */
38 | val isIdle: Boolean get() = this is Idle && this.error == null
39 | /**
40 | * Convenience state query for when the state is [Idle] and [Idle.error] is non-null
41 | * (e.g in an error state)
42 | */
43 | val isError: Boolean get() = this is Idle && this.error != null
44 | /**
45 | * Convenience state query for when the state is [Paused]
46 | */
47 | val isPaused: Boolean get() = this == Paused
48 | /**
49 | * Convenience state query for when the state is [Delay]
50 | */
51 | val isInStartDelay: Boolean get() = this is Delay
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/bmcreations/scrcast/app/list/FABExtensions.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.app.list
2 |
3 | import android.animation.ObjectAnimator
4 | import android.annotation.SuppressLint
5 | import android.content.res.ColorStateList
6 | import android.view.animation.AccelerateDecelerateInterpolator
7 | import androidx.annotation.ColorInt
8 | import androidx.core.content.ContextCompat
9 | import com.google.android.material.floatingactionbutton.FloatingActionButton
10 | import dev.bmcreations.scrcast.app.R
11 | import dev.bmcreations.scrcast.recorder.RecordingState
12 | import dev.bmcreations.scrcast.recorder.RecordingState.Idle
13 | import dev.bmcreations.scrcast.recorder.RecordingState.Recording
14 |
15 | @SuppressLint("ObjectAnimatorBinding")
16 | private fun FloatingActionButton.animateColorChange(@ColorInt fromColor: Int, @ColorInt toColor: Int, startDelay: Long = 0) {
17 | val colorAnimator = ObjectAnimator.ofArgb(
18 | this,
19 | "backgroundTintColor",
20 | fromColor, toColor
21 | ).apply {
22 | setStartDelay(startDelay)
23 | interpolator = AccelerateDecelerateInterpolator()
24 | addUpdateListener { animation ->
25 | val animatedValue = animation.animatedValue as Int
26 | backgroundTintList = ColorStateList.valueOf(animatedValue)
27 | }
28 | }
29 |
30 | colorAnimator.start()
31 | }
32 |
33 | fun FloatingActionButton.reflectState(state: RecordingState) {
34 | if (state == Recording || state is Idle) {
35 | val isRecording = state == Recording
36 | setImageResource(if (isRecording) R.drawable.ic_stop else R.drawable.ic_camcorder)
37 | animateColorChange(
38 | if (isRecording) ContextCompat.getColor(
39 | context,
40 | R.color.teal200
41 | ) else ContextCompat.getColor(context, R.color.stop_recording),
42 | if (isRecording) ContextCompat.getColor(
43 | context,
44 | R.color.stop_recording
45 | ) else ContextCompat.getColor(context, R.color.teal200)
46 | )
47 | }
48 | }
49 |
50 | class FABExtensions {
51 | companion object {
52 | @JvmStatic
53 | fun reflectRecorderState(fab: FloatingActionButton, state: RecordingState) {
54 | fab.reflectState(state)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/lifecycle/src/main/java/dev/bmcreations/scrcast/lifecycle/ScrCastLifecycleObserver.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.lifecycle
2 |
3 | import androidx.lifecycle.*
4 | import dev.bmcreations.scrcast.ScrCast
5 | import dev.bmcreations.scrcast.recorder.RecordingState
6 |
7 | private class LiveEvent constructor(
8 | private val recorder: ScrCast,
9 | private val lifecycleOwner: LifecycleOwner,
10 | private val observer: Observer
11 | ) : LifecycleObserver {
12 | init {
13 | if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
14 | lifecycleOwner.lifecycle.addObserver(this)
15 | }
16 | }
17 |
18 | private var isActive: Boolean = false
19 |
20 | private fun shouldBeActive(): Boolean {
21 | return lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
22 | }
23 |
24 | private fun disposeObserver() {
25 | lifecycleOwner.lifecycle.removeObserver(this)
26 | }
27 |
28 | @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
29 | fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
30 | if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
31 | stopListening()
32 | disposeObserver()
33 | return
34 | }
35 | checkIfActiveStateChanged(shouldBeActive())
36 | }
37 |
38 | private fun checkIfActiveStateChanged(newActive: Boolean) {
39 | if (newActive == isActive) {
40 | return
41 | }
42 | val wasActive = isActive
43 | isActive = newActive
44 | val isActive = isActive
45 |
46 | if (!wasActive && isActive) {
47 | stopListening()
48 | recorder.onRecordingStateChange { observer.onChanged(it)
49 | }
50 | }
51 |
52 | if (wasActive && !isActive) {
53 | stopListening()
54 | }
55 | }
56 |
57 | private fun stopListening() {
58 | recorder.onRecordingStateChange { }
59 | }
60 | }
61 |
62 | /**
63 | * Lifecycle observer for [RecordingState] using AndroidX lifecycle components
64 | * @see [LifecycleObserver]
65 | */
66 | fun ScrCast.observeRecordingState(lifecycleOwner: LifecycleOwner, observer: Observer) {
67 | LiveEvent(this, lifecycleOwner, Observer { observer.onChanged(it) })
68 | }
69 |
70 |
71 | class ScrCastLifecycleObserver {
72 | companion object {
73 | /**
74 | * JVM accessible lifecycle observer for [RecordingState] using AndroidX lifecycle components
75 | * @see [LifecycleObserver]
76 | */
77 | @JvmStatic
78 | fun observeRecordingState(recorder: ScrCast, lifecycleOwner: LifecycleOwner, observer: Observer) {
79 | recorder.observeRecordingState(lifecycleOwner, observer)
80 | }
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/dependencies.gradle:
--------------------------------------------------------------------------------
1 | def androidbuild = [:]
2 |
3 | // Android build stuff
4 | androidbuild.minSdkVersion = 23
5 | androidbuild.targetSdkVersion = 29
6 | androidbuild.compileSdkVersion = 29
7 | androidbuild.testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
8 | ext.androidbuild = androidbuild
9 |
10 | ext.deps = [:]
11 |
12 | def deps = [:]
13 |
14 | def versions = [:]
15 | versions.plugin_build_tools = '4.1.1'
16 | versions.kotlin = '1.4.21'
17 | versions.kotlinx_coroutines = '1.4.1'
18 | versions.plugin_androidx_navigation_safe_args = '2.2.0-rc03'
19 | versions.plugin_dokka = '0.10.1'
20 |
21 | versions.android_ktx = '1.3.2'
22 | versions.androidx_lifecycle = '2.2.0'
23 | ext.versions = versions
24 |
25 | def plugins = [:]
26 | plugins.android_build_tools = "com.android.tools.build:gradle:$versions.plugin_build_tools"
27 | plugins.maven_publish = "com.vanniktech:gradle-maven-publish-plugin:0.13.0"
28 | plugins.google_services = "com.google.gms:google-services:$versions.plugin_google_services"
29 | plugins.firebase = "com.google.firebase:firebase-plugins:$versions.plugin_firebase"
30 | plugins.fabric = "io.fabric.tools:gradle:$versions.plugin_fabric"
31 | plugins.kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
32 | plugins.androidx_navigation_safe_args = "androidx.navigation:navigation-safe-args-gradle-plugin:$versions.plugin_androidx_navigation_safe_args"
33 | plugins.versions = "com.github.ben-manes:gradle-versions-plugin:0.17.0"
34 | plugins.dokka = "org.jetbrains.dokka:dokka-gradle-plugin:$versions.plugin_dokka"
35 | deps.plugins = plugins
36 |
37 | def jetpack = [:]
38 | jetpack.activity = "androidx.activity:activity-ktx:1.2.0-rc01"
39 | jetpack.appcompat = "androidx.appcompat:appcompat:1.2.0"
40 | jetpack.corektx = "androidx.core:core-ktx:$versions.android_ktx"
41 | jetpack.constraintlayout = "androidx.constraintlayout:constraintlayout:2.0.0-beta3"
42 | jetpack.fragment = "androidx.fragment:fragment-ktx:1.3.0-rc01"
43 | jetpack.material = "com.google.android.material:material:1.2.0"
44 | jetpack.lifecycle_extensions = "androidx.lifecycle:lifecycle-extensions:$versions.androidx_lifecycle"
45 | jetpack.localbroadcast = "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0"
46 | jetpack.lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.androidx_lifecycle"
47 | jetpack.lifecycle_livedata_ktx = "androidx.lifecycle:lifecycle-livedata-ktx:$versions.androidx_lifecycle"
48 | deps.jetpack = jetpack
49 |
50 | def kotlin = [:]
51 | kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
52 | kotlin.coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.kotlinx_coroutines"
53 | kotlin.coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlinx_coroutines"
54 | kotlin.reflect = "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
55 | deps.kotlin = kotlin
56 |
57 | deps.dexter = "com.karumi:dexter:6.2.2"
58 |
59 | ext.deps = deps
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
23 |
24 |
30 |
31 |
32 |
33 |
34 |
39 |
40 |
51 |
52 |
64 |
65 |
66 |
67 |
68 |
77 |
78 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/bmcreations/scrcast/app/list/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.app.list
2 |
3 |
4 | import android.os.Bundle
5 | import android.util.Log
6 | import android.widget.Toast
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.core.graphics.drawable.toBitmap
9 | import androidx.core.view.isVisible
10 | import com.google.android.material.snackbar.Snackbar
11 | import dev.bmcreations.scrcast.ScrCast
12 | import dev.bmcreations.scrcast.app.R
13 | import dev.bmcreations.scrcast.extensions.supportsPauseResume
14 | import dev.bmcreations.scrcast.recorder.RecordingState
15 | import kotlinx.android.synthetic.main.activity_main.*
16 |
17 |
18 | abstract class MainActivity : AppCompatActivity() {
19 |
20 | protected val recorder: ScrCast by lazy {
21 | ScrCast.use(this).apply {
22 | options {
23 | video {
24 | maxLengthSecs = 360
25 | }
26 | storage {
27 | directoryName = "scrcast-sample"
28 | }
29 | notification {
30 | icon = resources.getDrawable(R.drawable.ic_camcorder, null).toBitmap()
31 | channel {
32 | id = "1337"
33 | name = "Recording Service"
34 | }
35 | showStop = true
36 | showPause = true
37 | showTimer = true
38 | }
39 | moveTaskToBack = false
40 | stopOnScreenOff = true
41 | startDelayMs = 5_000
42 | }
43 |
44 | // ScrCast supports running your own notifications completely
45 | // simply provide a NotificationProvider
46 | //
47 | //setNotificationProvider(SimpleNotificationProvider(this@MainActivity))
48 | }
49 | }
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 | setContentView(R.layout.activity_main)
54 |
55 | pause_fab.hide()
56 | pause_fab.setOnClickListener {
57 | if (recorder.state.isPaused) {
58 | recorder.resume()
59 | } else {
60 | recorder.pause()
61 | }
62 | }
63 |
64 | fab.setOnClickListener {
65 | if (recorder.state.isRecording) {
66 | recorder.stopRecording()
67 | } else {
68 | recorder.record()
69 | }
70 | }
71 | }
72 |
73 | override fun onResume() {
74 | super.onResume()
75 | recorder.onRecordingComplete { file ->
76 | Snackbar.make(bottom_bar, "Recording located at ${file.absolutePath}", Snackbar.LENGTH_SHORT).show()
77 | }
78 | }
79 |
80 | override fun onStop() {
81 | super.onStop()
82 | recorder.onRecordingComplete { }
83 | }
84 |
85 | protected fun handleRecorderState(state: RecordingState) {
86 | Log.d("sample", "state change: state = $state")
87 | fab.reflectState(state)
88 |
89 | start_timer.isVisible = state.isInStartDelay
90 | if (supportsPauseResume) {
91 | if (state.isRecording || state.isPaused) {
92 | pause_fab.show()
93 | } else {
94 | pause_fab.hide()
95 | }
96 | }
97 |
98 | when (state) {
99 | is RecordingState.Delay -> start_timer.text = state.remainingSeconds.toString()
100 | RecordingState.Recording -> {
101 | pause_fab.setIconResource(R.drawable.ic_pause)
102 | pause_fab.text = "Pause"
103 | }
104 | is RecordingState.Idle -> {
105 | fab.isExpanded = false
106 | if (state.error != null) {
107 | Toast.makeText(this, "Unable to start recording", Toast.LENGTH_SHORT).show()
108 | }
109 | }
110 | RecordingState.Paused -> {
111 | pause_fab.setIconResource(R.drawable.ic_resume)
112 | pause_fab.text = "Resume"
113 | }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 | 
5 |
6 | A fully, featured replacement for screen recording needs backed by Kotlin with the power of Coroutines and Android Jetpack. scrcast is:
7 |
8 | * Easy to use: scrcast's API leverages Kotlin languages features for simplicity, ease of use, and little-to-no boilerplate. Simply configure and `record()`
9 | * Modern: scrcast is Kotlin-first and uses modern libraries including Coroutines and Android Jetpack.
10 |
11 | ## Download
12 |
13 | scrcast is available on `mavenCentral()`.
14 |
15 | `implementation ("dev.bmcreations:scrcast:$version")`
16 |
17 | ## Quick Start
18 |
19 | scrcast provides a variety of configuration options for capturing, storing, and providing user interactions with your screen recordings.
20 |
21 | ### Configuring
22 |
23 | ```kotlin
24 | val recorder = ScrCast.use(activity)
25 | recorder.apply {
26 | // configure options via DSL
27 | options {
28 | video {
29 | maxLengthSecs = 360
30 | }
31 | storage {
32 | directoryName = "scrcast-sample"
33 | }
34 | notification {
35 | title = "Super cool library"
36 | description = "shh session in progress"
37 | icon = resources.getDrawable(R.drawable.ic_camcorder, null).toBitmap()
38 | channel {
39 | id = "1337"
40 | name = "Recording Service"
41 | }
42 | showStop = true
43 | showPause = true
44 | showTimer = true
45 | }
46 | moveTaskToBack = false
47 | startDelayMs = 5_000
48 | }
49 | }
50 | ```
51 |
52 | You can find [full configuration details and documentation here](https://bmcreations.github.io/scrcast/).
53 |
54 | ### State
55 |
56 | interaction with `MediaRecorder`is abstracted in a easy to use and manage interface, via explict state-changing accessors.
57 |
58 | #### Start
59 |
60 | ```kotlin
61 | recorder.record()
62 | ```
63 |
64 | #### Stop
65 |
66 | ```kotlin
67 | recorder.stopRecording()
68 | ```
69 |
70 | #### Pause
71 |
72 | ```kotlin
73 | recorder.pause()
74 | ```
75 |
76 | #### Resume
77 |
78 | ```kotlin
79 | recorder.resume()
80 | ```
81 |
82 | ### Callbacks
83 |
84 | State changes are emitted via `RecordingCallbacks` as a single interface or via a discrete lambda `onRecordingStateChange`
85 |
86 | Completed recording output file is also emittable in `RecordingCallbacks` via
87 |
88 | ```kotlin
89 | fun onRecordingFinished(file: File)
90 | ```
91 |
92 | ## Requirements
93 |
94 | * AndroidX
95 | * `minSdkVersion` 23+
96 | * `compileSdkVersion` 28+
97 | * Java 8+
98 |
99 | Gradle (`.gradle`)
100 |
101 | ```kotlin
102 | android {
103 | compileOptions {
104 | sourceCompatibility JavaVersion.VERSION_1_8
105 | targetCompatibility JavaVersion.VERSION_1_8
106 | }
107 | }
108 |
109 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
110 | kotlinOptions {
111 | jvmTarget = "1.8"
112 | }
113 | }
114 | ```
115 |
116 | Gradle Kotlin DSL (`.gradle.kts`)
117 |
118 | ```kotlin
119 | android {
120 | compileOptions {
121 | sourceCompatibility = JavaVersion.VERSION_1_8
122 | targetCompatibility = JavaVersion.VERSION_1_8
123 | }
124 | }
125 |
126 | tasks.withType {
127 | kotlinOptions {
128 | jvmTarget = "1.8"
129 | }
130 | }
131 | ```
132 |
133 | ## License
134 |
135 | ```text
136 | Copyright 2020 bmcreations
137 |
138 | Licensed under the Apache License, Version 2.0 (the "License");
139 | you may not use this file except in compliance with the License.
140 | You may obtain a copy of the License at
141 |
142 | https://www.apache.org/licenses/LICENSE-2.0
143 |
144 | Unless required by applicable law or agreed to in writing, software
145 | distributed under the License is distributed on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
147 | See the License for the specific language governing permissions and
148 | limitations under the License.
149 | ```
150 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/config/dsl/OptionsDSL.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("MemberVisibilityCanBePrivate")
2 |
3 | package dev.bmcreations.scrcast.internal.config.dsl
4 |
5 | import android.graphics.Bitmap
6 | import androidx.annotation.ColorRes
7 | import androidx.annotation.RestrictTo
8 | import dev.bmcreations.scrcast.config.*
9 | import java.io.File
10 |
11 | @RestrictTo(RestrictTo.Scope.LIBRARY)
12 | class OptionsBuilder {
13 | private var video = VideoConfig()
14 | private var storage = StorageConfig()
15 | private var notification = NotificationConfig()
16 | var moveTaskToBack: Boolean = false
17 | var startDelayMs: Long = 0
18 | var stopOnScreenOff: Boolean = false
19 |
20 | @JvmSynthetic
21 | fun video(config: VideoConfigBuilder.() -> Unit) {
22 | video = VideoConfigBuilder().apply(config).build()
23 | }
24 |
25 | @JvmSynthetic
26 | fun storage(config: StorageConfigBuilder.() -> Unit) {
27 | storage = StorageConfigBuilder().apply(config).build()
28 | }
29 |
30 | @JvmSynthetic
31 | fun notification(config: NotificationConfigBuilder.() -> Unit) {
32 | notification = NotificationConfigBuilder()
33 | .apply(config).build()
34 | }
35 |
36 | fun build(): Options =
37 | Options(
38 | video,
39 | storage,
40 | notification,
41 | moveTaskToBack,
42 | startDelayMs,
43 | stopOnScreenOff
44 | )
45 | }
46 |
47 | class VideoConfigBuilder {
48 | private val defaultConfig = VideoConfig()
49 | var width: Int = defaultConfig.width
50 | var height: Int = defaultConfig.height
51 | var videoEncoder: Int = defaultConfig.videoEncoder
52 | var bitrate: Int = defaultConfig.bitrate
53 | var frameRate: Int = defaultConfig.frameRate
54 | var maxLengthSecs: Int = defaultConfig.maxLengthSecs
55 |
56 | fun build(): VideoConfig =
57 | VideoConfig(
58 | width,
59 | height,
60 | videoEncoder,
61 | bitrate,
62 | frameRate,
63 | maxLengthSecs
64 | )
65 | }
66 |
67 | class StorageConfigBuilder {
68 | private val defaultConfig = StorageConfig()
69 |
70 | var directoryName: String = defaultConfig.directoryName
71 | var directory: File = defaultConfig.directory
72 | var fileNameFormatter: FileFormatter = defaultConfig.fileNameFormatter
73 | var outputFormat: Int = defaultConfig.outputFormat
74 | var maxSizeMB: Float = defaultConfig.maxSizeMB
75 |
76 | fun build(): StorageConfig =
77 | StorageConfig(
78 | directoryName,
79 | directory,
80 | fileNameFormatter,
81 | outputFormat,
82 | maxSizeMB
83 | )
84 | }
85 |
86 |
87 | class NotificationConfigBuilder {
88 | private val defaultConfig = NotificationConfig()
89 |
90 | var title: String = defaultConfig.title
91 | var description: String = defaultConfig.description
92 | var icon: Bitmap? = defaultConfig.icon
93 | var id: Int = defaultConfig.id
94 | var showStop: Boolean = defaultConfig.showStop
95 | var showPause: Boolean = defaultConfig.showPause
96 | var showTimer: Boolean = defaultConfig.showTimer
97 | var useMediaStyle: Boolean = defaultConfig.useMediaStyle
98 |
99 | @ColorRes
100 | var accentColor: Int = defaultConfig.accentColor
101 | var colorAsBackground: Boolean = defaultConfig.colorAsBackground
102 |
103 | var channel: ChannelConfig = ChannelConfig()
104 |
105 | @JvmSynthetic
106 | fun channel(config: ChannelConfigBuilder.() -> Unit) {
107 | channel = ChannelConfigBuilder().apply(config).build()
108 | }
109 |
110 | fun build() = NotificationConfig(
111 | title = title,
112 | description = description,
113 | icon = icon,
114 | id = id,
115 | showStop = showStop,
116 | showPause = showPause,
117 | showTimer = showTimer,
118 | useMediaStyle = useMediaStyle,
119 | accentColor = accentColor,
120 | colorAsBackground = colorAsBackground,
121 | channel = channel
122 | )
123 | }
124 |
125 | class ChannelConfigBuilder {
126 | private val defaultConfig = ChannelConfig()
127 |
128 | var id: String = defaultConfig.id
129 | var name: String = defaultConfig.name
130 | var lightColor: Int = defaultConfig.lightColor
131 | var lockscreenVisibility: Int = defaultConfig.lockscreenVisibility
132 |
133 | fun build() = ChannelConfig(
134 | id,
135 | name,
136 | lightColor,
137 | lockscreenVisibility
138 | )
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/bmcreations/scrcast/app/list/jvm/JavaMainActivity.java:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.app.list.jvm;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.Canvas;
5 | import android.graphics.drawable.BitmapDrawable;
6 | import android.graphics.drawable.Drawable;
7 | import android.media.MediaRecorder;
8 | import android.os.Bundle;
9 | import android.view.View;
10 | import android.widget.Toast;
11 |
12 | import androidx.annotation.Nullable;
13 | import androidx.appcompat.app.AppCompatActivity;
14 | import androidx.core.content.ContextCompat;
15 | import androidx.lifecycle.Observer;
16 |
17 | import com.google.android.material.floatingactionbutton.FloatingActionButton;
18 | import com.google.android.material.textview.MaterialTextView;
19 |
20 | import org.jetbrains.annotations.NotNull;
21 |
22 | import java.io.File;
23 |
24 | import dev.bmcreations.scrcast.ScrCast;
25 | import dev.bmcreations.scrcast.app.R;
26 | import dev.bmcreations.scrcast.app.list.FABExtensions;
27 | import dev.bmcreations.scrcast.config.ChannelConfig;
28 | import dev.bmcreations.scrcast.config.Options;
29 | import dev.bmcreations.scrcast.config.StorageConfig;
30 | import dev.bmcreations.scrcast.config.VideoConfig;
31 | import dev.bmcreations.scrcast.internal.config.dsl.NotificationConfigBuilder;
32 | import dev.bmcreations.scrcast.lifecycle.ScrCastLifecycleObserver;
33 | import dev.bmcreations.scrcast.recorder.RecordingCallbacks;
34 | import dev.bmcreations.scrcast.recorder.RecordingState;
35 |
36 | public class JavaMainActivity extends AppCompatActivity {
37 |
38 | private FloatingActionButton fab;
39 | private MaterialTextView startTimer;
40 | private ScrCast recorder;
41 |
42 | @Override
43 | protected void onCreate(@Nullable Bundle savedInstanceState) {
44 | super.onCreate(savedInstanceState);
45 | setContentView(R.layout.activity_main);
46 | fab = findViewById(R.id.fab);
47 | startTimer = findViewById(R.id.start_timer);
48 |
49 | setupRecorder();
50 |
51 | bindViews();
52 | }
53 |
54 | private void setupRecorder() {
55 | recorder = ScrCast.use(this);
56 |
57 | // create configuration for video
58 | VideoConfig videoConfig = new VideoConfig(
59 | -1,
60 | -1,
61 | MediaRecorder.VideoEncoder.H264,
62 | 8_000_000,
63 | 360
64 | );
65 |
66 | // create configuration for storage
67 | StorageConfig storageConfig = new StorageConfig("scrcast-sample");
68 |
69 | // create configuration for notification channel for recording
70 | ChannelConfig channelConfig = new ChannelConfig("1337", "Recording Service");
71 |
72 | // create configuration for our notification
73 | Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_camcorder);
74 | NotificationConfigBuilder notificationConfig = new NotificationConfigBuilder();
75 | notificationConfig.setShowPause(true);
76 | notificationConfig.setIcon(drawableToBitmap(icon));
77 | notificationConfig.setShowStop(true);
78 | notificationConfig.setShowTimer(true);
79 | notificationConfig.setChannel(channelConfig);
80 |
81 | Options options = new Options(
82 | videoConfig,
83 | storageConfig,
84 | notificationConfig.build(),
85 | false,
86 | 5000,
87 | true
88 | );
89 |
90 | // set our options
91 | recorder.updateOptions(options);
92 |
93 | // listen for state changes
94 | recorder.setRecordingCallback(new RecordingCallbacks() {
95 | @Override
96 | public void onStateChange(@NotNull RecordingState state) {
97 | FABExtensions.reflectRecorderState(fab, state);
98 | startTimer.setVisibility(state instanceof RecordingState.Delay ? View.VISIBLE : View.GONE);
99 | if (state instanceof RecordingState.Delay) {
100 | startTimer.setText(((RecordingState.Delay) state).getRemainingSeconds());
101 | }
102 | }
103 |
104 | @Override
105 | public void onRecordingFinished(@NotNull File file) {
106 | Toast.makeText(
107 | JavaMainActivity.this,
108 | "result file is located at " + file.getAbsolutePath(),
109 | Toast.LENGTH_SHORT
110 | ).show();
111 | }
112 | });
113 | }
114 |
115 | private void bindViews() {
116 | fab.setOnClickListener(v -> {
117 | if (recorder.getState().isRecording()) {
118 | recorder.stopRecording();
119 | } else {
120 | recorder.record();
121 | }
122 | });
123 | }
124 |
125 | public static Bitmap drawableToBitmap (Drawable drawable) {
126 | Bitmap bitmap;
127 |
128 | if (drawable instanceof BitmapDrawable) {
129 | BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
130 | if(bitmapDrawable.getBitmap() != null) {
131 | return bitmapDrawable.getBitmap();
132 | }
133 | }
134 |
135 | if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
136 | bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
137 | } else {
138 | bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
139 | }
140 |
141 | Canvas canvas = new Canvas(bitmap);
142 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
143 | drawable.draw(canvas);
144 | return bitmap;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | scrcast sample
3 |
4 | "Material is the metaphor.\n\n"
5 |
6 | "A material metaphor is the unifying theory of a rationalized space and a system of motion."
7 | "The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
8 | "technologically advanced and open to imagination and magic.\n"
9 | "Surfaces and edges of the material provide visual cues that are grounded in reality. The "
10 | "use of familiar tactile attributes helps users quickly understand affordances. Yet the "
11 | "flexibility of the material creates new affordances that supercede those in the physical "
12 | "world, without breaking the rules of physics.\n"
13 | "The fundamentals of light, surface, and movement are key to conveying how objects move, "
14 | "interact, and exist in space and in relation to each other. Realistic lighting shows "
15 | "seams, divides space, and indicates moving parts.\n\n"
16 |
17 | "Bold, graphic, intentional.\n\n"
18 |
19 | "The foundational elements of print based design typography, grids, space, scale, color, "
20 | "and use of imagery guide visual treatments. These elements do far more than please the "
21 | "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
22 | "imagery, large scale typography, and intentional white space create a bold and graphic "
23 | "interface that immerse the user in the experience.\n"
24 | "An emphasis on user actions makes core functionality immediately apparent and provides "
25 | "waypoints for the user.\n\n"
26 |
27 | "Motion provides meaning.\n\n"
28 |
29 | "Motion respects and reinforces the user as the prime mover. Primary user actions are "
30 | "inflection points that initiate motion, transforming the whole design.\n"
31 | "All action takes place in a single environment. Objects are presented to the user without "
32 | "breaking the continuity of experience even as they transform and reorganize.\n"
33 | "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
34 | "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
35 |
36 | "3D world.\n\n"
37 |
38 | "The material environment is a 3D space, which means all objects have x, y, and z "
39 | "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
40 | "positive z-axis extending towards the viewer. Every sheet of material occupies a single "
41 | "position along the z-axis and has a standard 1dp thickness.\n"
42 | "On the web, the z-axis is used for layering and not for perspective. The 3D world is "
43 | "emulated by manipulating the y-axis.\n\n"
44 |
45 | "Light and shadow.\n\n"
46 |
47 | "Within the material environment, virtual lights illuminate the scene. Key lights create "
48 | "directional shadows, while ambient light creates soft shadows from all angles.\n"
49 | "Shadows in the material environment are cast by these two light sources. In Android "
50 | "development, shadows occur when light sources are blocked by sheets of material at "
51 | "various positions along the z-axis. On the web, shadows are depicted by manipulating the "
52 | "y-axis only. The following example shows the card with a height of 6dp.\n\n"
53 |
54 | "Resting elevation.\n\n"
55 |
56 | "All material objects, regardless of size, have a resting elevation, or default elevation "
57 | "that does not change. If an object changes elevation, it should return to its resting "
58 | "elevation as soon as possible.\n\n"
59 |
60 | "Component elevations.\n\n"
61 |
62 | "The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
63 | "does not vary from 6dp in one app to 16dp in another app).\n"
64 | "Components may have different resting elevations across platforms, depending on the depth "
65 | "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
66 |
67 | "Responsive elevation and dynamic elevation offsets.\n\n"
68 |
69 | "Some component types have responsive elevation, meaning they change elevation in response "
70 | "to user input (e.g., normal, focused, and pressed) or system events. These elevation "
71 | "changes are consistently implemented using dynamic elevation offsets.\n"
72 | "Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
73 | "to the component’s resting state. They ensure that elevation changes are consistent "
74 | "across actions and component types. For example, all components that lift on press have "
75 | "the same elevation change relative to their resting elevation.\n"
76 | "Once the input event is completed or cancelled, the component will return to its resting "
77 | "elevation.\n\n"
78 |
79 | "Avoiding elevation interference.\n\n"
80 |
81 | "Components with responsive elevations may encounter other components as they move between "
82 | "their resting elevations and dynamic elevation offsets. Because material cannot pass "
83 | "through other material, components avoid interfering with one another any number of ways, "
84 | "whether on a per component basis or using the entire app layout.\n"
85 | "On a component level, components can move or be removed before they cause interference. "
86 | "For example, a floating action button (FAB) can disappear or move off screen before a "
87 | "user picks up a card, or it can move if a snackbar appears.\n"
88 | "On the layout level, design your app layout to minimize opportunities for interference. "
89 | "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere "
90 | "when a user tries to pick up one of cards.\n\n"
91 |
92 | Settings
93 |
94 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/recorder/notification/RecorderNotificationProvider.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.recorder.notification
2 |
3 | import android.app.Notification
4 | import android.app.NotificationManager
5 | import android.app.PendingIntent
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.graphics.drawable.Icon
9 | import android.os.Build
10 | import androidx.annotation.RestrictTo
11 | import androidx.core.content.ContextCompat
12 | import dev.bmcreations.scrcast.config.NotificationConfig
13 | import dev.bmcreations.scrcast.extensions.supportsPauseResume
14 | import dev.bmcreations.scrcast.internal.recorder.Action
15 | import dev.bmcreations.scrcast.internal.recorder.receiver.RecordingNotificationReceiver
16 | import dev.bmcreations.scrcast.recorder.RecordingState
17 | import dev.bmcreations.scrcast.recorder.notification.NotificationProvider
18 |
19 | @RestrictTo(RestrictTo.Scope.LIBRARY)
20 | class RecorderNotificationProvider(
21 | private val context: Context,
22 | private val config: NotificationConfig
23 | ): NotificationProvider(context) {
24 |
25 | init {
26 | createNotificationChannel()
27 | }
28 |
29 | private var startTime: Long = 0
30 | private var elapsedTime: Long = 0
31 |
32 | override fun createNotificationChannel() {
33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
34 | val notificationChannel = with(config.channel) {
35 | android.app.NotificationChannel(
36 | id,
37 | name,
38 | NotificationManager.IMPORTANCE_NONE
39 | ).apply {
40 | lightColor = config.channel.lightColor
41 | lockscreenVisibility = config.channel.lockscreenVisibility
42 | }
43 | }
44 |
45 | notificationManager.createNotificationChannel(notificationChannel)
46 | }
47 | }
48 |
49 | override fun getChannelId(): String = config.channel.id
50 |
51 | override fun get(state: RecordingState): Notification {
52 | val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
53 | Notification.Builder(context, config.channel.id)
54 | } else {
55 | Notification.Builder(context)
56 | }
57 |
58 | builder.apply {
59 | setOngoing(true)
60 |
61 | if (config.icon != null) {
62 | setSmallIcon(Icon.createWithBitmap(config.icon))
63 | } else {
64 | setSmallIcon(android.R.drawable.ic_dialog_alert)
65 |
66 | }
67 |
68 | if (config.useMediaStyle) {
69 | style = Notification.MediaStyle().setShowActionsInCompactView(0)
70 | }
71 |
72 | setColor(ContextCompat.getColor(context, config.accentColor))
73 |
74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
75 | if (config.colorAsBackground) {
76 | setColorized(true)
77 | }
78 | }
79 | setContentTitle(config.title)
80 | if (config.description.isEmpty().not()) {
81 | setContentText(config.description)
82 | }
83 |
84 | addTimer(state)
85 |
86 | if (config.showPause) {
87 | addPauseResume(state)
88 | }
89 |
90 | if (config.showStop) {
91 | addStop()
92 | }
93 | }
94 |
95 | return builder.build()
96 | }
97 |
98 | override fun getNotificationId(): Int = config.id
99 |
100 | override fun update(state: RecordingState) {
101 | if (state.isPaused) {
102 | elapsedTime += System.currentTimeMillis() - startTime
103 | }
104 | super.update(state)
105 | }
106 |
107 | private fun Notification.Builder.addTimer(state: RecordingState) {
108 | if (config.showTimer) {
109 | when (state) {
110 | is RecordingState.Idle,
111 | is RecordingState.Delay -> {
112 | startTime = System.currentTimeMillis()
113 | setWhen(startTime)
114 | setUsesChronometer(true)
115 | }
116 | RecordingState.Recording -> {
117 | setWhen(System.currentTimeMillis() - elapsedTime)
118 | setUsesChronometer(true)
119 |
120 | startTime = System.currentTimeMillis()
121 | }
122 | RecordingState.Paused -> {
123 | setUsesChronometer(false)
124 | }
125 | }
126 | }
127 | }
128 |
129 | private fun Notification.Builder.addPauseResume(state: RecordingState) {
130 | if (supportsPauseResume) {
131 | with(if (state.isPaused) Action.Resume else Action.Pause) {
132 | val actionIntent = Intent(
133 | context,
134 | RecordingNotificationReceiver::class.java
135 | ).apply {
136 | action = name
137 | }
138 |
139 | val actionPendingIntent: PendingIntent =
140 | PendingIntent.getBroadcast(
141 | context, requestId, actionIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
142 | )
143 |
144 | addAction(
145 | Notification.Action.Builder(
146 | Icon.createWithResource(context, icon),
147 | context.getString(label),
148 | actionPendingIntent
149 | ).build()
150 | )
151 | }
152 | }
153 | }
154 |
155 | private fun Notification.Builder.addStop() {
156 | with (Action.Stop) {
157 | val stopIntent = Intent(
158 | context,
159 | RecordingNotificationReceiver::class.java
160 | ).apply {
161 | action = name
162 | }
163 | val stopPendingIntent: PendingIntent =
164 | PendingIntent.getBroadcast(
165 | context, requestId, stopIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
166 | )
167 |
168 | addAction(
169 | Notification.Action.Builder(
170 | Icon.createWithResource(context, icon),
171 | context.getString(label),
172 | stopPendingIntent
173 | ).build()
174 | )
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.3.0 (2021-01-12)
2 |
3 | * **actions:** add dokka trigger for releases ([0072e4f](https://github.com/bmcreations/scrcast/commit/0072e4f5fa6315d181e056f88033963b3a166ac5))
4 | * **readme:** add github action CI badge ([07d9e23](https://github.com/bmcreations/scrcast/commit/07d9e23031b7f50c590c845083b81b590dd4c2fd))
5 | * chore: update mavan publish dependency; begin setup for action deploy ([526af18](https://github.com/bmcreations/scrcast/commit/526af18))
6 |
7 | ### Bug Fixes
8 |
9 | * **scrcast:** fix media playback after recording; replace dispatcher with Activity Results API ([97788e9](https://github.com/bmcreations/scrcast/commit/97788e999c9816bf4000c4ddce564f373d176155)), closes [#46](https://github.com/bmcreations/scrcast/issues/46) [#48](https://github.com/bmcreations/scrcast/issues/48)
10 |
11 | ### Features
12 |
13 | * **notifications:** Update default notification user experience ([#41](https://github.com/bmcreations/scrcast/issues/41)) ([15cc998](https://github.com/bmcreations/scrcast/commit/15cc998277a05670e63a8bfdb58c8690332e5206)), closes [#40](https://github.com/bmcreations/scrcast/issues/40)
14 |
15 | ## 0.2.0 (2020-07-24)
16 |
17 | * feat: add support for maven deploy ([b1938c0](https://github.com/bmcreations/scrcast/commit/b1938c0))
18 | * refactor(options/storage): have mediaStorageLocation self-generate ([b9f6909](https://github.com/bmcreations/scrcast/commit/b9f6909))
19 | * refactor(scrcast): allow querying current options of instance ([745a35c](https://github.com/bmcreations/scrcast/commit/745a35c))
20 | * docs(scrcast): add docs to exposed options ([b080054](https://github.com/bmcreations/scrcast/commit/b080054))
21 | * chore(versioning): bump to 0.2.0 ([415cd4b](https://github.com/bmcreations/scrcast/commit/415cd4b))
22 | * style: update README with logos ([e39b1b0](https://github.com/bmcreations/scrcast/commit/e39b1b0))
23 | * fix(readme): update documentation link after transfer ([b65bb67](https://github.com/bmcreations/scrcast/commit/b65bb67))
24 |
25 |
26 |
27 | ## 0.1.0 (2020-07-19)
28 |
29 | * chore: add docs generate and deploy script ([49faa12](https://github.com/bmcreations/scrcast/commit/49faa12))
30 | * chore: add dokka to dependencies.gradle ([8a7ef45](https://github.com/bmcreations/scrcast/commit/8a7ef45))
31 | * chore: add JVM sample with options, stage toggling ([877869e](https://github.com/bmcreations/scrcast/commit/877869e))
32 | * chore: add maven-publish (#35) ([ac728dd](https://github.com/bmcreations/scrcast/commit/ac728dd)), closes [#35](https://github.com/bmcreations/scrcast/issues/35)
33 | * chore: handle failures starting media recorder elegantly ([7ef6a65](https://github.com/bmcreations/scrcast/commit/7ef6a65))
34 | * chore: remove changes to idea files ([2aae161](https://github.com/bmcreations/scrcast/commit/2aae161))
35 | * chore: remove jitpack credential auth for now ([cba8661](https://github.com/bmcreations/scrcast/commit/cba8661))
36 | * chore: remove maven plugin until we are public ([c878c32](https://github.com/bmcreations/scrcast/commit/c878c32))
37 | * chore: remove tests ([1668466](https://github.com/bmcreations/scrcast/commit/1668466))
38 | * chore: setup for jitpack ([217abea](https://github.com/bmcreations/scrcast/commit/217abea))
39 | * chore: setup private jitpack ([a2e33cb](https://github.com/bmcreations/scrcast/commit/a2e33cb))
40 | * chore: switch back to jitpack for now ([0710f26](https://github.com/bmcreations/scrcast/commit/0710f26))
41 | * chore: upgrade gradle; reorganize receiver package ([6e5f9d8](https://github.com/bmcreations/scrcast/commit/6e5f9d8))
42 | * chore(actions): swap install to build ([4393981](https://github.com/bmcreations/scrcast/commit/4393981))
43 | * chore(actions): update build step & rm PR trigger ([96294b7](https://github.com/bmcreations/scrcast/commit/96294b7))
44 | * chore(deps): unexpose coroutines ([bb84041](https://github.com/bmcreations/scrcast/commit/bb84041))
45 | * chore(gradle): register gradle for IDE management ([bd7461b](https://github.com/bmcreations/scrcast/commit/bd7461b))
46 | * chore(options): allow JVM overloads for config constructors ([9347810](https://github.com/bmcreations/scrcast/commit/9347810))
47 | * chore(sample): add simple notification provider excert sample and init in app ([c98f136](https://github.com/bmcreations/scrcast/commit/c98f136))
48 | * chore(sample): kill filewatcher from sample ([99a75c8](https://github.com/bmcreations/scrcast/commit/99a75c8))
49 | * chore(sample): set output directory for sample ([4fbcd4c](https://github.com/bmcreations/scrcast/commit/4fbcd4c))
50 | * fix: lazy load local broadcast manager ([e6b58a0](https://github.com/bmcreations/scrcast/commit/e6b58a0))
51 | * fix(notification): hide pause actions when enabled if < API 24 ([5e3cf73](https://github.com/bmcreations/scrcast/commit/5e3cf73))
52 | * fix(options): handle dynamic video sizing in scrcast instead of DSL ([5245fb9](https://github.com/bmcreations/scrcast/commit/5245fb9))
53 | * fix(readme): correct gh pages linkage ([8b19456](https://github.com/bmcreations/scrcast/commit/8b19456))
54 | * fix(recorder/service): create recorder _before_ creating virtual display from its surface ([98e0c96](https://github.com/bmcreations/scrcast/commit/98e0c96))
55 | * fix(scrcast): allow directoryName and directory to be mutually exclusive ([019090d](https://github.com/bmcreations/scrcast/commit/019090d))
56 | * fix(scrcast): don't scan for output file until state is reset to idle ([3a32a21](https://github.com/bmcreations/scrcast/commit/3a32a21))
57 | * fix(scrcast/options): remove TS_2 conditional; increase bitrate to 8 Mbps ([5d5f8f7](https://github.com/bmcreations/scrcast/commit/5d5f8f7))
58 | * fix(scrcast/recorder): start notification chronometer after start delay ([7914e9e](https://github.com/bmcreations/scrcast/commit/7914e9e))
59 | * docs: Add getting started guide stub for scrcast ([445e15b](https://github.com/bmcreations/scrcast/commit/445e15b))
60 | * docs: add initial README with TODO's (#33) ([7688b9a](https://github.com/bmcreations/scrcast/commit/7688b9a)), closes [#33](https://github.com/bmcreations/scrcast/issues/33)
61 | * docs: finish README ([2e8c49f](https://github.com/bmcreations/scrcast/commit/2e8c49f))
62 | * docs: update README and deploy ([0a035c3](https://github.com/bmcreations/scrcast/commit/0a035c3))
63 | * docs(options): add all the docs around the configuration optiosn available ([6fb6538](https://github.com/bmcreations/scrcast/commit/6fb6538))
64 | * refactor: categorize options ([eb8ad07](https://github.com/bmcreations/scrcast/commit/eb8ad07))
65 | * refactor: extract internal operations to internal package ([5726310](https://github.com/bmcreations/scrcast/commit/5726310))
66 | * refactor: lower default bitrate to 4 Mbps ([fa47ef3](https://github.com/bmcreations/scrcast/commit/fa47ef3)), closes [#14](https://github.com/bmcreations/scrcast/issues/14)
67 | * refactor(notifications): create abstract base for notification management ([df36f8c](https://github.com/bmcreations/scrcast/commit/df36f8c))
68 | * refactor(options): convert outputDirectory to File; add fileName formatter configuration ([fb45810](https://github.com/bmcreations/scrcast/commit/fb45810))
69 | * refactor(options): revamp as DSL ([06ed22d](https://github.com/bmcreations/scrcast/commit/06ed22d))
70 | * refactor(options/storage): convert file name formatter to hihger-order lambda invokation ([cced5bc](https://github.com/bmcreations/scrcast/commit/cced5bc))
71 | * refactor(scrcast): make state more explicit and expose start delay countdown ([2c60790](https://github.com/bmcreations/scrcast/commit/2c60790))
72 | * refactor(scrcast): make state more explicit and expose start delay countdown ([817aaba](https://github.com/bmcreations/scrcast/commit/817aaba))
73 | * refactor(scrcast): rebrand state change listener as RecordingCallbacks ([cf2a6a4](https://github.com/bmcreations/scrcast/commit/cf2a6a4))
74 | * refactor(scrcast): reorganize packages and use device width/height as default ([337c8cd](https://github.com/bmcreations/scrcast/commit/337c8cd))
75 | * refactor(scrcast): use action's for receiver broadcast instead of state ([788a34e](https://github.com/bmcreations/scrcast/commit/788a34e))
76 | * feat: add dokka ([b040521](https://github.com/bmcreations/scrcast/commit/b040521))
77 | * feat: add max video duration limit ([a795e84](https://github.com/bmcreations/scrcast/commit/a795e84))
78 | * feat(main/list): add filewatcher to observe recording changes; hook up state listener for recorder ([727122b](https://github.com/bmcreations/scrcast/commit/727122b))
79 | * feat(notifications): add pause/resume action; have chronometer respect pause ([83921a6](https://github.com/bmcreations/scrcast/commit/83921a6))
80 | * feat(options): add ability to move activity/task to back on recording start ([f596623](https://github.com/bmcreations/scrcast/commit/f596623)), closes [#4](https://github.com/bmcreations/scrcast/issues/4)
81 | * feat(options): add max file size limit ([61247ae](https://github.com/bmcreations/scrcast/commit/61247ae))
82 | * feat(options): add notification customization ([2294c20](https://github.com/bmcreations/scrcast/commit/2294c20))
83 | * feat(options): add startDelay ([99de25c](https://github.com/bmcreations/scrcast/commit/99de25c)), closes [#16](https://github.com/bmcreations/scrcast/issues/16)
84 | * feat(options): allow providing only directory name, using Movies external as absolute path ([5c91609](https://github.com/bmcreations/scrcast/commit/5c91609))
85 | * feat(options/notifications): add timer support ([d4af7b8](https://github.com/bmcreations/scrcast/commit/d4af7b8))
86 | * feat(sample): configure notification options ([306be79](https://github.com/bmcreations/scrcast/commit/306be79))
87 | * feat(sample): enable stop on screen off ([8ba5e6a](https://github.com/bmcreations/scrcast/commit/8ba5e6a))
88 | * feat(scrcast): add pause/resume functionality with kotlin demo ([c7eb16a](https://github.com/bmcreations/scrcast/commit/c7eb16a))
89 | * feat(scrcast): add support for ending recording on screen off event ([aa0d4e7](https://github.com/bmcreations/scrcast/commit/aa0d4e7))
90 | * feat(scrcast): allow providing notification provider via bound service ([37c08cd](https://github.com/bmcreations/scrcast/commit/37c08cd))
91 | * feat(scrcast): make stateful via local broadcast; expose output directory ([4e6d2cd](https://github.com/bmcreations/scrcast/commit/4e6d2cd))
92 | * style(sample): animate recorder FAB on state change ([1f2e5c7](https://github.com/bmcreations/scrcast/commit/1f2e5c7))
93 | * initial commit for drop-in screen recording library w/ vanilla sample app ([440f146](https://github.com/bmcreations/scrcast/commit/440f146))
94 | * Setup github actions ([80a541e](https://github.com/bmcreations/scrcast/commit/80a541e))
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/config/Options.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.config
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.graphics.Bitmap
7 | import android.graphics.Color
8 | import android.media.MediaRecorder
9 | import android.os.Build
10 | import android.os.Environment
11 | import android.os.Parcelable
12 | import android.util.DisplayMetrics
13 | import android.util.Log
14 | import androidx.annotation.ColorInt
15 | import androidx.annotation.ColorRes
16 | import dev.bmcreations.scrcast.R
17 | import kotlinx.android.parcel.Parcelize
18 | import kotlinx.android.parcel.RawValue
19 | import kotlin.jvm.functions.FunctionN
20 | import java.io.File
21 | import java.text.SimpleDateFormat
22 | import java.util.*
23 |
24 | import dev.bmcreations.scrcast.ScrCast
25 | import dev.bmcreations.scrcast.recorder.RecordingState
26 | import dev.bmcreations.scrcast.recorder.RecordingCallbacks
27 | import kotlinx.android.parcel.IgnoredOnParcel
28 |
29 | /**
30 | * Type alias foe the [StorageConfig.fileNameFormatter] lambda.
31 | */
32 | typealias FileFormatter = () -> String
33 |
34 | /**
35 | * An immutable data object representing the Available configuration options for
36 | * storage, recording, and providing user interaction with the screen recording.
37 | *
38 | * @see [ScrCast.options]
39 | */
40 | @Parcelize
41 | data class Options @JvmOverloads constructor(
42 | /** @see [VideoConfig] */
43 | val video: VideoConfig = VideoConfig(),
44 | /** @see [StorageConfig] */
45 | val storage: StorageConfig = StorageConfig(),
46 | /** @see [NotificationConfig] */
47 | val notification: NotificationConfig = NotificationConfig(),
48 | /**
49 | * If the activity task should be moved the back once video starts recording.
50 | * Defaults to false.
51 | */
52 | val moveTaskToBack: Boolean = false,
53 | /**
54 | * The time (in milliseconds) to delay the recording after calling [ScrCast.record].
55 | *
56 | * Will emit [RecordingState.Delay] with a countdown from [startDelayMs] down to zero (in seconds).
57 | *
58 | * @see [RecordingState]
59 | * @see [RecordingCallbacks]
60 | */
61 | val startDelayMs: Long = 0,
62 | /**
63 | * If enabled, any current in-progress recording session will be ending once the device screen
64 | * is turned off.
65 | */
66 | val stopOnScreenOff: Boolean = false
67 | ): Parcelable
68 |
69 | /**
70 | * An immutable data class representing configuration options for the recording.
71 | */
72 | @Parcelize
73 | data class VideoConfig @JvmOverloads constructor(
74 | /**
75 | * Width of the video recording frame.
76 | *
77 | * A value of `-1` will allow [ScrCast] to query the [DisplayMetrics.widthPixels] from the device for use.
78 | *
79 | * @see [MediaRecorder.setVideoSize]
80 | */
81 | val width: Int = -1,
82 | /**
83 | * Height of the video recording frame.
84 | *
85 | * A value of `-1` will allow [ScrCast] to query the [DisplayMetrics.heightPixels] from the device for use.
86 | *
87 | * @see [MediaRecorder.setVideoSize]
88 | */
89 | val height: Int = -1,
90 | /**
91 | * Defines the video encoding to be used for the recording.
92 | *
93 | * @see [MediaRecorder.VideoEncoder]
94 | * @see [MediaRecorder.setVideoEncoder]
95 | *
96 | * A value of [-1] will allow [ScrCast] to query the [DisplayMetrics.widthPixels] from the device for use.
97 | *
98 | * @see [MediaRecorder.setVideoSize]
99 | */
100 | val videoEncoder: Int = MediaRecorder.VideoEncoder.H264,
101 | /**
102 | * Defines the video bitrate to be used for the recording.
103 | *
104 | * A higher bitrate will result in a better quality recording, but will also result in a larger
105 | * output file.
106 | *
107 | * @see [MediaRecorder.setVideoEncodingBitRate]
108 | */
109 | val bitrate: Int = 8_000_000,
110 | /**
111 | * Defines the video frame rate to be used for the recording.
112 | *
113 | * A higher frame rate will result in a smoother recording, but will also result in a larger
114 | * output file.
115 | *
116 | * @see [MediaRecorder.setVideoFrameRate]
117 | */
118 | val frameRate: Int = 60,
119 | /**
120 | * Defines the maximum length of time (in seconds) desired for the recording. If the recording session hits this defined
121 | * time limit, the recording will auto end.
122 | *
123 | * @see [MediaRecorder.setMaxDuration]
124 | */
125 | val maxLengthSecs: Int = 0
126 | ): Parcelable
127 |
128 | /**
129 | * An immutable data class representing configuration options for the storage of the output file.
130 | */
131 | @Parcelize
132 | data class StorageConfig @JvmOverloads constructor(
133 | /**
134 | * The directory name for this recording to be stored in.
135 | * Will end up being located at [mediaStorageLocation]/[fileNameFormatter]
136 | *
137 | * @see [mediaStorageLocation]
138 | * @see [fileNameFormatter]
139 | */
140 | val directoryName: String = "scrcast",
141 | /**
142 | * The parent directory for all storage operations
143 | * Will end up being located at [mediaStorageLocation]/[fileNameFormatter]
144 | *
145 | * @see [mediaStorageLocation]
146 | * @see [fileNameFormatter]
147 | */
148 | val directory: File = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
149 | /**
150 | * the formatting of the file name for the resulting screen recording. This is done via a higher order lambda
151 | * in Kotlin and a [FunctionN] to ensure the formatter will run for each recording independently of one another.
152 | *
153 | * Defaults to the current time in `MM_dd_yyyy_hhmmss` format.
154 | *
155 | * Will end up being located at [mediaStorageLocation]/[fileNameFormatter]
156 | *
157 | * @see [mediaStorageLocation]
158 | */
159 | val fileNameFormatter: @RawValue FileFormatter = { SimpleDateFormat("MM_dd_yyyy_hhmmss", Locale.getDefault()).format(Date()) },
160 | /**
161 | * The resulting video format of the screen recording file.
162 | *
163 | * @see [MediaRecorder.OutputFormat]
164 | */
165 | val outputFormat: Int = MediaRecorder.OutputFormat.MPEG_4,
166 | /**
167 | * Defines the maximum size (in MB) desired for the recording. If the recording session hits this defined
168 | * size limit, the recording will auto end.
169 | *
170 | * @see [MediaRecorder.setMaxFileSize]
171 | */
172 | val maxSizeMB: Float = 0f
173 | ): Parcelable {
174 | /**
175 | * The resulting directory location of the screen recordings, derived from [directory] and [directoryName]
176 | *
177 | * @see [directory]
178 | * @see [directoryName]
179 | */
180 | @IgnoredOnParcel
181 | val mediaStorageLocation: File?
182 | get() {
183 | val location = File(directory, directoryName)
184 | return with(location) {
185 | apply {
186 | if (!exists()) {
187 | if (!mkdirs()) {
188 | Log.d("scrcast", "failed to create designated output directory")
189 | return@with null
190 | }
191 | }
192 | return this
193 | }
194 | }
195 | }
196 | }
197 |
198 | /**
199 | * An immutable data class representing configuration options for the
200 | * notification presented from the foreground service.
201 | */
202 | @Parcelize
203 | data class NotificationConfig @JvmOverloads constructor(
204 | /**
205 | * The title displayed in the notification
206 | *
207 | * @see [Notification.Builder.setContentTitle]
208 | */
209 | val title: String = "Recording screen",
210 | /**
211 | * The message/description displayed in the notification
212 | *
213 | * @see [Notification.Builder.setContentText]
214 | */
215 | val description: String = "",
216 | /**
217 | * The icon displayed in the notification
218 | *
219 | * @see [Notification.Builder.setSmallIcon]
220 | */
221 | val icon: Bitmap? = null,
222 | /**
223 | * The unique identifier for the displayed notification
224 | *
225 | * @see [NotificationManager.notify]
226 | */
227 | val id: Int = 101,
228 | /**
229 | * Whether to add a notification action for stopping the current recording.
230 | */
231 | val showStop: Boolean = false,
232 | /**
233 | * Whether to add a notification action for pause/resume of the current recording (visible action
234 | * is dependent on the state of the current recording session).
235 | *
236 | * NOTE: This option will only make an effect on [Build.VERSION_CODES.N] and above
237 | *
238 | */
239 | val showPause: Boolean = false,
240 | /**
241 | * Whether to add a the current recording time to the notification via a chronometer.
242 | *
243 | * @see [Notification.Builder.setUsesChronometer]
244 | */
245 | val showTimer: Boolean = false,
246 | /**
247 | * Whether to have pause/resume and stop actions show as media style icons
248 | *
249 | */
250 | val useMediaStyle: Boolean = false,
251 | /**
252 | * The accent color resource for the notification
253 | *
254 | * NOTE: On [Build.VERSION_CODES.M] and below, this will set the background color of the notification
255 | * and on [Build.VERSION_CODES.N] and above, will result as the accent color unless [colorAsBackground] is enabled.
256 | *
257 | * Defaults to [Color.RED]
258 | */
259 | @ColorRes
260 | val accentColor: Int = R.color.recorder_notification_background,
261 | /**
262 | * When enabled the notification will use [accentColor] as the background.
263 | *
264 | */
265 | val colorAsBackground: Boolean = true,
266 | /** @see [ChannelConfig] */
267 | val channel: ChannelConfig = ChannelConfig()
268 | ): Parcelable
269 |
270 | /**
271 | * An immutable data class representing configuration options for the
272 | * notification channel of the recording notifications.
273 | *
274 | * @see [NotificationChannel]
275 | */
276 | @Parcelize
277 | data class ChannelConfig @JvmOverloads constructor(
278 | /**
279 | * The unique identifier for the notification channel
280 | *
281 | * @see [NotificationChannel]
282 | */
283 | val id: String = "1337",
284 | /**
285 | * The name for the notification channel
286 | *
287 | * @see [NotificationChannel]
288 | */
289 | val name: String = "Recording Service",
290 | /**
291 | * The LED color to used when this notification presents itself
292 | *
293 | * @see [NotificationChannel]
294 | */
295 | val lightColor: Int = Color.BLUE,
296 | /**
297 | * The visibility restrictions for the notifications in this channel.
298 | *
299 | * @see [NotificationChannel]
300 | * @see [Notification.VISIBILITY_PRIVATE]
301 | * @see [Notification.VISIBILITY_PUBLIC]
302 | * @see [Notification.VISIBILITY_SECRET]
303 | */
304 | val lockscreenVisibility: Int = Notification.VISIBILITY_PRIVATE
305 | ): Parcelable
306 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/internal/recorder/service/RecorderService.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast.internal.recorder.service
2 |
3 | import android.app.Service
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import android.hardware.display.DisplayManager
9 | import android.hardware.display.VirtualDisplay
10 | import android.media.MediaRecorder
11 | import android.media.MediaRecorder.*
12 | import android.media.projection.MediaProjection
13 | import android.media.projection.MediaProjectionManager
14 | import android.os.Binder
15 | import android.os.Build
16 | import android.os.Handler
17 | import android.os.IBinder
18 | import android.util.Log
19 | import androidx.annotation.RestrictTo
20 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
21 | import dev.bmcreations.scrcast.config.Options
22 | import dev.bmcreations.scrcast.internal.extensions.countdown
23 | import dev.bmcreations.scrcast.internal.recorder.*
24 | import dev.bmcreations.scrcast.recorder.*
25 | import dev.bmcreations.scrcast.recorder.notification.NotificationProvider
26 | import kotlinx.coroutines.Dispatchers
27 | import kotlinx.coroutines.GlobalScope
28 | import kotlinx.coroutines.launch
29 | import java.io.File
30 |
31 | @RestrictTo(RestrictTo.Scope.LIBRARY)
32 | class RecorderService : Service() {
33 |
34 | private val projectionManager: MediaProjectionManager by lazy {
35 | getSystemService(MediaProjectionManager::class.java)
36 | }
37 |
38 | private val broadcaster by lazy {
39 | LocalBroadcastManager.getInstance(this)
40 | }
41 |
42 | private val binder = LocalBinder()
43 |
44 | private lateinit var notificationProvider: NotificationProvider
45 |
46 | private val pauseResumeHandler = object : BroadcastReceiver() {
47 | override fun onReceive(context: Context?, intent: Intent?) {
48 | when (intent?.action) {
49 | ACTION_PAUSE -> pause()
50 | ACTION_RESUME -> resume()
51 | ACTION_STOP -> stopRecording()
52 | }
53 | }
54 | }
55 | private val screenHandler = object : BroadcastReceiver() {
56 | override fun onReceive(context: Context?, intent: Intent?) {
57 | when (intent?.action) {
58 | Intent.ACTION_SCREEN_OFF -> {
59 | Log.d("scrcast", "stopping recording with screen off per request")
60 | if (state == RecordingState.Recording) {
61 | stopRecording()
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
68 | private var state: RecordingState = RecordingState.Idle()
69 | set(value) {
70 | field = value
71 | broadcaster.sendBroadcast(Intent(value.stateString()).apply {
72 | if (value is RecordingState.Delay) {
73 | putExtra(EXTRA_DELAY_REMAINING, value.remainingSeconds)
74 | } else if (value is RecordingState.Idle) {
75 | putExtra(EXTRA_ERROR, value.error)
76 | }
77 | })
78 | }
79 |
80 | private var options: Options = Options()
81 | private lateinit var outputFile: String
82 | private var rotation = 0
83 | private val orientation by lazy {
84 | orientations.get(rotation + 90)
85 | }
86 | private var dpi: Float = 0f
87 |
88 | private var requestCode: Int = -1
89 | private var requestData: Intent = Intent()
90 |
91 | private var mediaProjection: MediaProjection? = null
92 | private var mediaProjectionCallback = MediaProjectionCallback()
93 |
94 | private var _virtualDisplay: VirtualDisplay? = null
95 | private val virtualDisplay: VirtualDisplay?
96 | get() {
97 | if (_virtualDisplay == null) {
98 | _virtualDisplay = mediaProjection?.createVirtualDisplay(
99 | "SrcCast",
100 | options.video.width,
101 | options.video.height,
102 | dpi.toInt(),
103 | DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
104 | mediaRecorder?.surface,
105 | null,
106 | null
107 | )
108 | }
109 | return _virtualDisplay
110 | }
111 |
112 | private var mediaRecorder: MediaRecorder? = null
113 |
114 | private fun createRecorder() {
115 | Log.d("scrcast", "createRecorder()")
116 | mediaRecorder = MediaRecorder().apply {
117 | setAudioSource(AudioSource.MIC)
118 | setVideoSource(VideoSource.SURFACE)
119 | setOutputFormat(options.storage.outputFormat)
120 | setAudioEncoder(AudioEncoder.HE_AAC)
121 | setOutputFile(outputFile)
122 | with(options.video) {
123 | setVideoSize(width, height)
124 | setVideoEncoder(videoEncoder)
125 | setVideoEncodingBitRate(bitrate)
126 | setVideoFrameRate(frameRate)
127 | if (maxLengthSecs > 0) {
128 | setMaxDuration(maxLengthSecs * 1000)
129 | }
130 | }
131 | with(options.storage) {
132 | if (maxSizeMB > 0) {
133 | setMaxFileSize((maxSizeMB * (1024 * 1024)).toLong())
134 | }
135 | }
136 | setOnInfoListener { _, what, _ ->
137 | when (what) {
138 | MEDIA_RECORDER_INFO_MAX_DURATION_REACHED -> {
139 | Log.d(
140 | "scrcast",
141 | "max duration of ${options.video.maxLengthSecs} seconds reached. Stopping reconrding..."
142 | )
143 | stopRecording()
144 | }
145 | MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING -> Log.d(
146 | "scrcast",
147 | "Approaching max file size of ${options.storage.maxSizeMB}MB"
148 | )
149 | MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED -> {
150 | Log.d(
151 | "scrcast",
152 | "max file size of ${options.storage.maxSizeMB}MB reached. Stopping reconrding..."
153 | )
154 | stopRecording()
155 | }
156 |
157 | }
158 | }
159 | setOrientationHint(orientation)
160 | }
161 | mediaRecorder?.prepare()
162 | }
163 |
164 | fun setNotificationProvider(provider: NotificationProvider) {
165 | notificationProvider = provider
166 | }
167 |
168 | private fun pause() {
169 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
170 | if (state.isRecording) {
171 | mediaRecorder?.pause()
172 | }
173 | state = RecordingState.Paused
174 |
175 | notificationProvider.update(state)
176 | }
177 | }
178 |
179 | private fun resume() {
180 | when (state) {
181 | is RecordingState.Idle -> startRecording()
182 | RecordingState.Paused -> {
183 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
184 | mediaRecorder?.resume()
185 | state = RecordingState.Recording
186 | notificationProvider.update(state)
187 | }
188 | }
189 | }
190 | }
191 |
192 | private fun startRecording(code: Int = requestCode, data: Intent = requestData) {
193 | requestCode = code
194 | requestData = data
195 |
196 | if (options.startDelayMs > 0) {
197 | options.startDelayMs.countdown(
198 | repeatMillis = 1_000,
199 | onTick = { state = RecordingState.Delay((it / 1000).toInt() + 1) },
200 | after = { recordInternal(code, data) }
201 | )
202 | } else {
203 | recordInternal(code, data)
204 | }
205 | }
206 |
207 | private fun recordInternal(code: Int, data: Intent) {
208 | GlobalScope.launch(Dispatchers.Main) {
209 | startForeground(
210 | notificationProvider.getNotificationId(),
211 | notificationProvider.get(state)
212 | )
213 | mediaProjection = projectionManager.getMediaProjection(code, data)
214 |
215 | if (options.stopOnScreenOff) {
216 | with(IntentFilter(Intent.ACTION_SCREEN_OFF)) {
217 | registerReceiver(screenHandler, this)
218 | }
219 | }
220 |
221 | with(IntentFilter(ACTION_PAUSE).apply {
222 | addAction(ACTION_RESUME)
223 | addAction(ACTION_STOP)
224 | }) {
225 | broadcaster.registerReceiver(pauseResumeHandler, this)
226 | }
227 |
228 | mediaProjection?.registerCallback(mediaProjectionCallback, Handler())
229 | createRecorder()
230 | virtualDisplay // touch
231 | try {
232 | mediaRecorder?.start()
233 | state = RecordingState.Recording
234 | notificationProvider.update(state)
235 | } catch (e: Exception) {
236 | stopRecording(e)
237 | }
238 | }
239 | }
240 |
241 | private fun stopRecording(error: Throwable? = null) {
242 | mediaProjection?.stop()
243 |
244 | state = RecordingState.Idle(error)
245 | stopForeground(true)
246 | }
247 |
248 | private fun cleanupProjection() {
249 | mediaProjection?.unregisterCallback(mediaProjectionCallback)
250 | mediaProjection = null
251 |
252 | _virtualDisplay?.release()
253 |
254 | runCatching {
255 | mediaRecorder?.stop()
256 | mediaRecorder?.reset()
257 | mediaRecorder?.release()
258 | }
259 | }
260 |
261 | private inner class MediaProjectionCallback : MediaProjection.Callback() {
262 | override fun onStop() {
263 | Log.d("scrcast", "projection on stop")
264 | cleanupProjection()
265 | }
266 | }
267 |
268 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
269 | intent?.let {
270 | options = it.getParcelableExtra("options") ?: Options()
271 | rotation = it.getIntExtra("rotation", 0)
272 | dpi = it.getFloatExtra("dpi", 0f)
273 | outputFile = it.getStringExtra("outputFile") ?: ""
274 |
275 | startRecording(
276 | code = it.getIntExtra("code", -1),
277 | data = it.getParcelableExtra("data") ?: Intent()
278 | )
279 | }
280 |
281 | return START_STICKY
282 | }
283 |
284 | override fun onDestroy() {
285 | Log.d("scrcast", "onDestroy: service")
286 | stopRecording()
287 | if (options.stopOnScreenOff) {
288 | unregisterReceiver(screenHandler)
289 | }
290 | try {
291 | broadcaster.unregisterReceiver(pauseResumeHandler)
292 | } catch (swallow: Exception) {
293 | }
294 |
295 | super.onDestroy()
296 | }
297 |
298 | override fun onBind(p0: Intent?): IBinder? = binder
299 |
300 | // Class used for the client Binder.
301 | inner class LocalBinder : Binder() {
302 | // Return this instance of MyService so clients can call public methods
303 | val service: RecorderService
304 | get() = this@RecorderService
305 | }
306 | }
307 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 bmcreations, Inc.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/library/src/main/java/dev/bmcreations/scrcast/ScrCast.kt:
--------------------------------------------------------------------------------
1 | package dev.bmcreations.scrcast
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.*
6 | import android.content.pm.PackageManager
7 | import android.media.MediaRecorder
8 | import android.media.MediaScannerConnection
9 | import android.media.projection.MediaProjectionManager
10 | import android.os.Build
11 | import android.os.IBinder
12 | import android.util.DisplayMetrics
13 | import android.util.Log
14 | import androidx.activity.ComponentActivity
15 | import androidx.activity.result.ActivityResult
16 | import androidx.activity.result.launch
17 | import androidx.core.app.ActivityCompat
18 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
19 | import com.karumi.dexter.Dexter
20 | import com.karumi.dexter.MultiplePermissionsReport
21 | import com.karumi.dexter.PermissionToken
22 | import com.karumi.dexter.listener.PermissionRequest
23 | import com.karumi.dexter.listener.multi.CompositeMultiplePermissionsListener
24 | import com.karumi.dexter.listener.multi.DialogOnAnyDeniedMultiplePermissionsListener
25 | import com.karumi.dexter.listener.multi.MultiplePermissionsListener
26 | import dev.bmcreations.scrcast.config.Options
27 | import dev.bmcreations.scrcast.internal.config.dsl.OptionsBuilder
28 | import dev.bmcreations.scrcast.extensions.supportsPauseResume
29 | import dev.bmcreations.scrcast.internal.recorder.*
30 | import dev.bmcreations.scrcast.recorder.*
31 | import dev.bmcreations.scrcast.recorder.RecordingState.*
32 | import dev.bmcreations.scrcast.recorder.RecordingStateChangeCallback
33 | import dev.bmcreations.scrcast.recorder.notification.NotificationProvider
34 | import dev.bmcreations.scrcast.internal.recorder.notification.RecorderNotificationProvider
35 | import dev.bmcreations.scrcast.internal.recorder.service.RecorderService
36 | import dev.bmcreations.scrcast.internal.request.RecordScreen
37 | import java.io.File
38 |
39 | /**
40 | * Main Interface for accessing [ScrCast] Library
41 | */
42 | class ScrCast private constructor(private val activity: ComponentActivity) {
43 |
44 | /**
45 | * The current [RecordingState] of the recorder
46 | *
47 | * @see [RecordingState]
48 | */
49 | var state: RecordingState = Idle()
50 | private set(value) {
51 | val was = field
52 | field = value
53 | onStateChange?.invoke(value)
54 | if (was == Recording && value is Idle) {
55 | try {
56 | broadcaster.unregisterReceiver(recordingStateHandler)
57 | } catch (swallow: Exception) { }
58 |
59 | activity.unbindService(connection)
60 | activity.stopService(recordingSession)
61 | scanForOutputFile()
62 | }
63 | }
64 |
65 | /** Defines callbacks for service binding, passed to bindService() */
66 | private val connection = object : ServiceConnection {
67 |
68 | override fun onServiceConnected(className: ComponentName, service: IBinder) {
69 | // We've bound to LocalService, cast the IBinder and get LocalService instance
70 | val binder = service as RecorderService.LocalBinder
71 | serviceBinder = binder.service
72 | serviceBinder?.setNotificationProvider(notificationProvider ?: defaultNotificationProvider)
73 | }
74 |
75 | override fun onServiceDisconnected(arg0: ComponentName) {
76 | serviceBinder = null
77 | }
78 | }
79 |
80 | private var recordingSession: Intent? = null
81 |
82 | private val dialogPermissionListener: DialogOnAnyDeniedMultiplePermissionsListener = DialogOnAnyDeniedMultiplePermissionsListener.Builder
83 | .withContext(activity)
84 | .withTitle("Storage permissions")
85 | .withMessage("Storage permissions are needed to store the screen recording")
86 | .withButtonText(android.R.string.ok)
87 | .withIcon(R.drawable.ic_storage_permission_dialog)
88 | .build()
89 |
90 | private val defaultNotificationProvider by lazy {
91 | RecorderNotificationProvider(
92 | activity,
93 | options.notification
94 | )
95 | }
96 | private var notificationProvider: NotificationProvider? = null
97 |
98 | private var onStateChange: RecordingStateChangeCallback? = null
99 | private var onRecordingOutput: RecordingOutputFileCallback? = null
100 |
101 | private val metrics by lazy {
102 | DisplayMetrics().apply { activity.windowManager.defaultDisplay.getMetrics(this) }
103 | }
104 |
105 | private val dpi by lazy { metrics.density }
106 |
107 | /**
108 | * Current [Options] for this instance.
109 | *
110 | * Available for client read/write persistence for user defaults.
111 | *
112 | * @see [options]
113 | * @see [updateOptions]
114 | * @see [Options]
115 | */
116 | var options = Options()
117 | private set
118 |
119 | private var serviceBinder: RecorderService? = null
120 |
121 | private val broadcaster by lazy {
122 | LocalBroadcastManager.getInstance(activity)
123 | }
124 |
125 | private val recordingStateHandler = object : BroadcastReceiver() {
126 | override fun onReceive(p0: Context?, p1: Intent?) {
127 | p1?.action?.let { action ->
128 | when(action) {
129 | STATE_RECORDING -> state = Recording
130 | STATE_IDLE -> state = Idle(p1.extras?.get(EXTRA_ERROR) as? Throwable)
131 | STATE_DELAY -> {
132 | state = Delay(p1.extras?.getInt(EXTRA_DELAY_REMAINING) ?: 0)
133 | }
134 | STATE_PAUSED -> state = Paused
135 | }
136 | }
137 | }
138 | }
139 |
140 | private val outputDirectory: File?
141 | get() = options.storage.mediaStorageLocation
142 |
143 | private var _outputFile: File? = null
144 | private val outputFile: File?
145 | get() {
146 | if (_outputFile == null) {
147 | outputDirectory?.let { dir ->
148 | _outputFile = File("${dir.path}${File.separator}${options.storage.fileNameFormatter()}.mp4")
149 | } ?: return null
150 | }
151 | return _outputFile
152 | }
153 |
154 | private val permissionListener = object : MultiplePermissionsListener {
155 | override fun onPermissionsChecked(p0: MultiplePermissionsReport?) {
156 | startRecording()
157 | }
158 |
159 | override fun onPermissionRationaleShouldBeShown(
160 | p0: MutableList?,
161 | p1: PermissionToken?
162 | ) {
163 | p1?.continuePermissionRequest()
164 | }
165 | }
166 |
167 | private val startRecording = activity.registerForActivityResult(RecordScreen()) { result ->
168 | if (result.resultCode == Activity.RESULT_OK) {
169 | if (options.moveTaskToBack) activity.moveTaskToBack(true)
170 | val output = outputFile
171 | if (output != null) {
172 | startService(result, output)
173 | }
174 | }
175 | }
176 |
177 | /**
178 | * Updates the configurations of [ScrCast] via a DSL.
179 | *
180 | * @see [Options]
181 | *
182 | * This method is not accessible to the JVM.
183 | */
184 | @JvmSynthetic
185 | fun options(opts: OptionsBuilder.() -> Unit) {
186 | options = handleDynamicVideoSize(OptionsBuilder().apply(opts).build())
187 | }
188 | /**
189 | * Updates the configurations of [ScrCast].
190 | *
191 | * @see [Options]
192 | */
193 | fun updateOptions(options: Options) {
194 | this.options = handleDynamicVideoSize(options)
195 | }
196 |
197 | /**
198 | * Set the recording callbacks, emitting changes of [RecordingState] as they occur and a link to the output [File]
199 | */
200 | fun setRecordingCallback(listener : RecordingCallbacks?) {
201 | onStateChange = { listener?.onStateChange(it) }
202 | onRecordingOutput = { listener?.onRecordingFinished(it) }
203 | }
204 |
205 | /**
206 | * Set an explicit state change listener, as a kotlin lambda, emitting changes of [RecordingState] as they occur.
207 | *
208 | * This is an alternative to providing the combined [RecordingCallbacks] if you are only interested in state changes or
209 | * want to define them independently.
210 | *
211 | * This method is not accessible to the JVM.
212 | */
213 | @JvmSynthetic
214 | fun onRecordingStateChange(callback: RecordingStateChangeCallback) {
215 | onStateChange = callback
216 | }
217 |
218 | /**
219 | * Set an explicit output file listener, as a kotlin lambda.
220 | *
221 | * This is an alternative to providing the combined [RecordingCallbacks] if you are only interested in the output file or
222 | * want to define them independently.
223 | *
224 | * This method is not accessible to the JVM.
225 | */
226 | @JvmSynthetic
227 | fun onRecordingComplete(callback: RecordingOutputFileCallback) {
228 | onRecordingOutput = callback
229 | }
230 |
231 | /**
232 | * Convenience method for clients to easily check if the required permissions are enabled for storage
233 | * Even though we internally will bubble up the permission request and handle the allow/deny,
234 | * some clients may want to onboard users via an OOBE or some UX state involving previously recorded files.
235 | */
236 | fun hasStoragePermissions(): Boolean {
237 | val perms = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
238 | return perms.all { ActivityCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED }
239 | }
240 |
241 | /**
242 | * Set the [NotificationProvider] for the [ScrCast] instance.
243 | *
244 | * @see [NotificationProvider]
245 | */
246 | fun setNotificationProvider(provider: NotificationProvider) {
247 | notificationProvider = provider
248 | }
249 |
250 | /**
251 | * Triggers a recording session based on the configuration's defined by [options]
252 | *
253 | * @see [updateOptions]
254 | * @see [Options]
255 | * @see [MediaRecorder.start]
256 | */
257 | fun record() {
258 | when (state) {
259 | is Idle -> {
260 | Dexter.withContext(activity)
261 | .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
262 | Manifest.permission.RECORD_AUDIO)
263 | .withListener(CompositeMultiplePermissionsListener(permissionListener, dialogPermissionListener))
264 | .check()
265 | }
266 | Paused -> resume()
267 | Recording -> stopRecording()
268 | is Delay -> { /* Prevent erroneous calls to record while in start delay */}
269 | }
270 | }
271 |
272 | /**
273 | * Triggers the end to a recording session that was started via [record]
274 | *
275 | * @see [MediaRecorder.stop]
276 | */
277 | fun stopRecording() {
278 | broadcaster.sendBroadcast(Intent(Action.Stop.name))
279 | }
280 |
281 | /**
282 | * Pauses a recording session that was started via [record]
283 | *
284 | * This only invokes a change to the recording state if the target device
285 | * is [Build.VERSION_CODES.N] or higher.
286 | *
287 | * @see [MediaRecorder.pause]
288 | */
289 | fun pause() {
290 | if (supportsPauseResume) {
291 | if (state.isRecording) {
292 | broadcaster.sendBroadcast(Intent(Action.Pause.name))
293 | }
294 | }
295 | }
296 |
297 | /**
298 | * Resumed a recording session that was paused via [pause], or triggers a new recording
299 | * if the [state] is not paused.
300 | *
301 | * * This only invokes a change to the recording state if the target device
302 | * is [Build.VERSION_CODES.N] or higher.
303 | *
304 | * @see [MediaRecorder.resume]
305 | */
306 | fun resume() {
307 | if (supportsPauseResume) {
308 | if (state.isPaused) {
309 | broadcaster.sendBroadcast(Intent(Action.Resume.name))
310 | } else {
311 | record()
312 | }
313 | }
314 | }
315 |
316 | private fun handleDynamicVideoSize(options: Options): Options {
317 | var reconfig: Options = options
318 | if (options.video.width == -1) {
319 | reconfig = reconfig.copy(video = reconfig.video.copy(width = metrics.widthPixels))
320 | }
321 | if (options.video.height == -1) {
322 | reconfig = reconfig.copy(video = reconfig.video.copy(height = metrics.heightPixels))
323 | }
324 | return reconfig
325 | }
326 |
327 | private fun scanForOutputFile() {
328 | MediaScannerConnection.scanFile(activity, arrayOf(outputFile.toString()), null) { path, uri ->
329 | Log.i("scrcast", "scanned: $path")
330 | Log.i("scrcast", "-> uri=$uri")
331 | if (uri != null) {
332 | onRecordingOutput?.invoke(File(path))
333 | }
334 | _outputFile = null
335 | }
336 | }
337 |
338 | private fun startRecording() {
339 | startRecording.launch()
340 | }
341 |
342 | private fun startService(result: ActivityResult, file : File) {
343 | recordingSession = Intent(activity, RecorderService::class.java).apply {
344 | putExtra("code", result.resultCode)
345 | putExtra("data", result.data)
346 | putExtra("options", options)
347 | putExtra("outputFile", file.absolutePath)
348 | putExtra("dpi", dpi)
349 | putExtra("rotation", activity.windowManager.defaultDisplay.rotation)
350 | }
351 |
352 | broadcaster.registerReceiver(
353 | recordingStateHandler,
354 | IntentFilter().apply {
355 | addAction(STATE_IDLE)
356 | addAction(STATE_RECORDING)
357 | addAction(STATE_PAUSED)
358 | addAction(STATE_DELAY)
359 | }
360 | )
361 |
362 | activity.bindService(recordingSession, connection, Context.BIND_AUTO_CREATE)
363 | activity.startService(recordingSession)
364 | }
365 |
366 | companion object {
367 | /**
368 | * Instance creator for [ScrCast].
369 | *
370 | * Requires an [Activity] reference for media projection creation, as well
371 | * as auto video-sizing in [Options].
372 | *
373 | * @see [Options.video]
374 | */
375 | @JvmStatic
376 | fun use(activity: ComponentActivity): ScrCast {
377 | return ScrCast(activity)
378 | }
379 | }
380 | }
381 |
--------------------------------------------------------------------------------