├── settings.gradle ├── screenshots ├── home_dark.png ├── home_light.png └── notification.png ├── app ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── drawables.xml │ │ │ │ ├── shapes.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── string_licenses.xml │ │ │ │ ├── array.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable │ │ │ │ ├── ic_stop.xml │ │ │ │ ├── ic_record.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── ic_menu.xml │ │ │ │ ├── ic_sort.xml │ │ │ │ ├── ic_cancel.xml │ │ │ │ ├── ic_feedback.xml │ │ │ │ ├── ic_licenses.xml │ │ │ │ ├── ic_rename.xml │ │ │ │ ├── ic_info.xml │ │ │ │ ├── ic_privacy.xml │ │ │ │ ├── ic_share.xml │ │ │ │ ├── ic_settings.xml │ │ │ │ ├── ic_open_in_new.xml │ │ │ │ ├── ic_notification_icon.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── layout │ │ │ │ ├── fragment_licenses.xml │ │ │ │ ├── about_lib_intro.xml │ │ │ │ ├── fragment_bottom_nav_drawer.xml │ │ │ │ ├── fragment_more_settings.xml │ │ │ │ ├── fragment_privacy_policy.xml │ │ │ │ ├── fragment_recordings.xml │ │ │ │ ├── fragment_home.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── dialog_rename_file.xml │ │ │ │ ├── about_library.xml │ │ │ │ ├── item_recording.xml │ │ │ │ └── fragment_about.xml │ │ │ ├── values-v23 │ │ │ │ └── themes.xml │ │ │ ├── values-night-v23 │ │ │ │ └── themes.xml │ │ │ ├── menu │ │ │ │ ├── home.xml │ │ │ │ ├── privacy_policy.xml │ │ │ │ ├── bottom_nav_drawer_menu.xml │ │ │ │ ├── item_selected.xml │ │ │ │ └── more_options.xml │ │ │ ├── xml │ │ │ │ ├── shortcut.xml │ │ │ │ └── settings.xml │ │ │ ├── navigation │ │ │ │ └── navigation.xml │ │ │ └── values-ar │ │ │ │ └── strings.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── ibashkimi │ │ │ │ └── screenrecorder │ │ │ │ ├── services │ │ │ │ ├── RecorderState.kt │ │ │ │ ├── Options.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── RecordingSession.kt │ │ │ │ └── Recorder.kt │ │ │ │ ├── data │ │ │ │ ├── Recording.kt │ │ │ │ ├── DataSource.kt │ │ │ │ ├── DataManager.kt │ │ │ │ ├── SAFDataSource.kt │ │ │ │ └── MediaStoreDataSource.kt │ │ │ │ ├── home │ │ │ │ ├── BottomNavigationDialog.kt │ │ │ │ └── MoreSettingsDialog.kt │ │ │ │ ├── Theme.kt │ │ │ │ ├── recordings │ │ │ │ ├── RecordingSelectionUtils.kt │ │ │ │ ├── RecordingsViewModel.kt │ │ │ │ ├── RecordingListFragment.kt │ │ │ │ └── RecordingAdapter.kt │ │ │ │ ├── about │ │ │ │ ├── PrivacyPolicyFragment.kt │ │ │ │ ├── AboutFragment.kt │ │ │ │ └── LicensesFragment.kt │ │ │ │ ├── NotificationUtils.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── settings │ │ │ │ ├── Utils.kt │ │ │ │ └── SettingsFragment.kt │ │ ├── assets │ │ │ └── privacy_policy.html │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── ibashkimi │ │ │ └── screenrecorder │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── ibashkimi │ │ └── screenrecorder │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── README.md ├── gradlew.bat ├── versions.gradle ├── gradlew └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' -------------------------------------------------------------------------------- /screenshots/home_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/screenshots/home_dark.png -------------------------------------------------------------------------------- /screenshots/home_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/screenshots/home_light.png -------------------------------------------------------------------------------- /screenshots/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/screenshots/notification.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indritbashkimi/ScreenKap/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/indritbashkimi/ScreenKap/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/indritbashkimi/ScreenKap/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/indritbashkimi/ScreenKap/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/indritbashkimi/ScreenKap/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #26A69A 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 08 09:07:18 CEST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https://services.gradle.org/distributions/gradle-6.5-all.zip 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local configuration file (sdk path, etc) 2 | local.properties 3 | 4 | # Gradle generated files 5 | .gradle/ 6 | build 7 | app/release 8 | 9 | # Signing files 10 | .signing/ 11 | 12 | # User-specific configurations 13 | .idea/ 14 | *.iml 15 | 16 | # OS-specific files 17 | .DS_Store 18 | .DS_Store? 19 | ._* 20 | .Spotlight-V100 21 | .Trashes 22 | ehthumbs.db 23 | Thumbs.db 24 | .directory 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cancel.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/ibashkimi/screenrecorder/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.ibashkimi.screenrecorder; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_licenses.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rename.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_privacy.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 64dp 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 16dp 19 | 16dp 20 | 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | kotlin.code.style=official 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | 21 | #android.injected.testOnly=false 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ibashkimi/screenrecorder/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.ibashkimi.screenrecorder; 2 | 3 | import android.content.Context; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import androidx.test.InstrumentationRegistry; 9 | import androidx.test.runner.AndroidJUnit4; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Instrumentation test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() throws Exception { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.orpheusdroid.screenrecorder", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_licenses.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/services/RecorderState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.services 18 | 19 | import androidx.lifecycle.MutableLiveData 20 | 21 | object RecorderState { 22 | val state: MutableLiveData = MutableLiveData() 23 | 24 | enum class State { 25 | RECORDING, STOPPED, PAUSED 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/res/values-night-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/about_lib_intro.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/drawables.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | @android:drawable/ic_media_pause 19 | @android:drawable/ic_media_play 20 | @drawable/ic_stop 21 | -------------------------------------------------------------------------------- /app/src/main/res/menu/home.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/privacy_policy.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/data/Recording.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.data 18 | 19 | import android.net.Uri 20 | import android.os.Parcelable 21 | import kotlinx.android.parcel.Parcelize 22 | 23 | @Parcelize 24 | data class Recording( 25 | val uri: Uri, 26 | val title: String, 27 | val duration: Int, // seconds 28 | val size: Long, // bytes 29 | val modified: Long, // millis 30 | val isPending: Boolean = false 31 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_bottom_nav_drawer.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_more_settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_privacy_policy.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav_drawer_menu.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_in_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/data/DataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.data 18 | 19 | import android.content.ContentValues 20 | import android.net.Uri 21 | import kotlinx.coroutines.flow.Flow 22 | 23 | 24 | interface DataSource { 25 | 26 | fun create(folderUri: Uri, name: String, mimeType: String, extra: ContentValues?): Uri? 27 | 28 | fun delete(uri: Uri) 29 | 30 | fun delete(uris: List) 31 | 32 | fun recordings(): Flow> 33 | 34 | fun rename(uri: Uri, newName: String) 35 | 36 | fun update(uri: Uri, values: ContentValues) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/services/Options.kt: -------------------------------------------------------------------------------- 1 | package com.ibashkimi.screenrecorder.services 2 | 3 | import android.media.MediaRecorder 4 | import android.net.Uri 5 | import android.os.Parcelable 6 | import kotlinx.android.parcel.Parcelize 7 | 8 | 9 | data class Options( 10 | val video: VideoOptions, 11 | val audio: AudioOptions, 12 | val output: OutputOptions 13 | ) 14 | 15 | data class VideoOptions( 16 | val resolution: Resolution, 17 | val encoder: Int = MediaRecorder.VideoEncoder.H264, 18 | val fps: Int = 30, 19 | val bitrate: Int = 7130317, 20 | val virtualDisplayDpi: Int 21 | ) 22 | 23 | data class Resolution(val width: Int, val height: Int) 24 | 25 | sealed class AudioOptions { 26 | object NoAudio : AudioOptions() 27 | data class RecordAudio( 28 | val source: Int = MediaRecorder.AudioSource.DEFAULT, 29 | val samplingRate: Int = 44100, 30 | val encoder: Int = MediaRecorder.AudioEncoder.AAC, 31 | val bitRate: Int = 128000 32 | ) : AudioOptions() 33 | } 34 | 35 | class OutputOptions(val uri: SaveUri, val format: Int = MediaRecorder.OutputFormat.DEFAULT) 36 | 37 | @Parcelize 38 | data class SaveUri(val uri: Uri, val type: UriType) : Parcelable 39 | 40 | @Parcelize 41 | enum class UriType : Parcelable { 42 | MEDIA_STORE, SAF 43 | } -------------------------------------------------------------------------------- /app/src/main/res/values/shapes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 19 | 20 | 26 | 27 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/services/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.ibashkimi.screenrecorder.services 2 | 3 | import android.media.MediaRecorder 4 | import com.ibashkimi.screenrecorder.data.DataManager 5 | import com.ibashkimi.screenrecorder.settings.PreferenceHelper 6 | 7 | fun PreferenceHelper.generateOptions(dataManager: DataManager): Options? { 8 | val saveUri = saveLocation ?: return null 9 | val folderUri = saveUri.uri 10 | 11 | val uri = dataManager 12 | .create(folderUri, filename, "video/mp4", null) ?: return null 13 | 14 | return Options( 15 | video = VideoOptions( 16 | resolution = resolution.run { 17 | Resolution(first, second) 18 | }, 19 | bitrate = videoBitrate, 20 | encoder = videoEncoder, 21 | fps = fps, 22 | virtualDisplayDpi = displayMetrics.densityDpi 23 | ), 24 | audio = if (recordAudio) { 25 | AudioOptions.RecordAudio( 26 | source = MediaRecorder.AudioSource.MIC, 27 | samplingRate = audioSamplingRate, 28 | encoder = audioEncoder, 29 | bitRate = audioBitrate 30 | ) 31 | } else AudioOptions.NoAudio, 32 | output = OutputOptions( 33 | uri = SaveUri(uri, saveUri.type) 34 | ) 35 | ) 36 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html) 2 | 3 | # ScreenKap 4 | 5 | ScreenKap is a simple screen recorder app for Android. 6 | It uses [MediaStore](https://developer.android.com/reference/android/provider/MediaStore) or [Storage Access Framework](https://developer.android.com/guide/topics/providers/document-provider) API's to access files so it doesn't require the storage permission. 7 | 8 | Get it on Google Play 9 | 10 | ## Screenshots 11 | |||| 12 | 13 | ## License 14 | Copyright (c) 2020 Indrit Bashkimi 15 | 16 | Licensed under the Apache License, Version 2.0 (the "License"); 17 | you may not use this file except in compliance with the License. 18 | You may obtain a copy of the License at 19 | 20 | http://www.apache.org/licenses/LICENSE-2.0 21 | 22 | Unless required by applicable law or agreed to in writing, software 23 | distributed under the License is distributed on an "AS IS" BASIS, 24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | See the License for the specific language governing permissions and 26 | limitations under the License. 27 | -------------------------------------------------------------------------------- /app/src/main/res/menu/item_selected.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 25 | 30 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 32 | 33 | 36 | 37 | 9 | 10 | 11 | 12 |

Screen Kap Privacy Policy

13 |

Last updated: May 19th, 2019

14 |

This page is used to inform website visitors regarding our 15 | policies with the collection, use, and disclosure of Personal 16 | Information if anyone decided to use our Service.

17 |

We will not use or share your information with anyone except as 18 | described in this Privacy Policy.

19 | 20 |

Information Collection and Use

21 |

We do not collect personal data used to identify you. Personal 22 | data means personally identifiable information that specifically 23 | identifies you as an individual (e.g. your name, email address).

24 | 25 |

Changes to This Privacy Policy

26 |

We may update our Privacy Policy from time to time. Thus, you are 27 | advised to review this page periodically for any changes. We will 28 | notify you of any changes by posting the new Privacy Policy on this 29 | page. These changes are effective immediately after they are posted 30 | on this page.

31 | 32 |

Contact Us

33 |

If you have any questions or suggestions about our Privacy Policy, 34 | please contact us at bashkimidev@gmail.com.

35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_recordings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/values/string_licenses.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | Apache License, Version 2.0 20 | 21 | Android Jetpack 22 | https://developer.android.com/jetpack 23 | 24 | Material Components for Android 25 | https://github.com/material-components/material-components-android 26 | 27 | Kotlin 28 | https://github.com/JetBrains/kotlin 29 | 30 | Glide 31 | BSD, part MIT and Apache 2.0 32 | https://github.com/bumptech/glide 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notification_icon.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/home/BottomNavigationDialog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.home 18 | 19 | import android.os.Bundle 20 | import android.view.LayoutInflater 21 | import android.view.View 22 | import android.view.ViewGroup 23 | import androidx.navigation.fragment.findNavController 24 | import androidx.navigation.ui.NavigationUI 25 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 26 | import com.ibashkimi.screenrecorder.databinding.FragmentBottomNavDrawerBinding 27 | 28 | class BottomNavigationDialog : BottomSheetDialogFragment() { 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, 32 | container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View { 35 | return FragmentBottomNavDrawerBinding.inflate(inflater, container, false).run { 36 | navigationView.setNavigationItemSelectedListener { 37 | dismiss() 38 | NavigationUI.onNavDestinationSelected(it, findNavController()) 39 | } 40 | root 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/data/DataManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.data 18 | 19 | import android.content.ContentValues 20 | import android.net.Uri 21 | import kotlinx.coroutines.flow.Flow 22 | 23 | 24 | class DataManager(private val source: DataSource) { 25 | 26 | fun create(folderUri: Uri, name: String, mimeType: String, extra: ContentValues?): Uri? { 27 | return source.create(folderUri, name, mimeType, extra) 28 | } 29 | 30 | fun delete(recording: Recording) { 31 | delete(recording.uri) 32 | } 33 | 34 | fun delete(uri: Uri) { 35 | source.delete(uri) 36 | } 37 | 38 | fun delete(uris: List) { 39 | source.delete(uris) 40 | } 41 | 42 | fun rename(recording: Recording, newName: String) { 43 | rename(recording.uri, newName) 44 | } 45 | 46 | fun rename(uri: Uri, newName: String) { 47 | source.rename(uri, newName) 48 | } 49 | 50 | fun recordings(): Flow> { 51 | return source.recordings() 52 | } 53 | 54 | fun update(uri: Uri, values: ContentValues) { 55 | source.update(uri, values) 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 24 | 25 | 36 | 37 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder 18 | 19 | import android.app.Activity 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.appcompat.app.AppCompatDelegate 22 | 23 | 24 | fun applyGlobalNightMode(nightMode: String) { 25 | AppCompatDelegate.setDefaultNightMode( 26 | when (nightMode) { 27 | "dark" -> AppCompatDelegate.MODE_NIGHT_YES 28 | "system_default" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 29 | "battery_saver" -> AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY 30 | "light" -> AppCompatDelegate.MODE_NIGHT_NO 31 | else -> throw IllegalArgumentException("Invalid night mode $nightMode.") 32 | } 33 | ) 34 | } 35 | 36 | fun Activity.applyNightMode(nightMode: String) { 37 | applyGlobalNightMode(nightMode) 38 | recreate() 39 | } 40 | 41 | @Suppress("unused") 42 | fun AppCompatActivity.applyLocalNightMode(nightMode: String) { 43 | delegate.localNightMode = when (nightMode) { 44 | "dark" -> AppCompatDelegate.MODE_NIGHT_YES 45 | "system_default" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 46 | "battery_saver" -> AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY 47 | "light" -> AppCompatDelegate.MODE_NIGHT_NO 48 | else -> throw IllegalArgumentException("Invalid night mode $nightMode.") 49 | } 50 | delegate.applyDayNight() 51 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 28 | 29 | 34 | 35 | 36 | 37 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/recordings/RecordingSelectionUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.recordings 18 | 19 | import android.view.MotionEvent 20 | import androidx.recyclerview.selection.ItemDetailsLookup 21 | import androidx.recyclerview.selection.ItemKeyProvider 22 | import androidx.recyclerview.widget.RecyclerView 23 | import com.ibashkimi.screenrecorder.data.Recording 24 | 25 | class RecordingDetails(private val adapterPosition: Int, val recording: Recording) : 26 | ItemDetailsLookup.ItemDetails() { 27 | override fun getSelectionKey(): Recording? { 28 | return recording 29 | } 30 | 31 | override fun getPosition(): Int { 32 | return adapterPosition 33 | } 34 | } 35 | 36 | class RecordingDetailsLookup(private val recyclerView: RecyclerView) : 37 | ItemDetailsLookup() { 38 | override fun getItemDetails(e: MotionEvent): ItemDetails? { 39 | return recyclerView.findChildViewUnder(e.x, e.y)?.let { 40 | (recyclerView.getChildViewHolder(it) as? RecordingViewHolder)?.getItemDetails() 41 | } 42 | } 43 | } 44 | 45 | class RecordingKeyProvider(var adapter: RecordingAdapter) : 46 | ItemKeyProvider(SCOPE_CACHED) { 47 | override fun getKey(position: Int): Recording? { 48 | return adapter.data[position] 49 | } 50 | 51 | override fun getPosition(key: Recording): Int { 52 | return adapter.data.indexOf(key) 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_rename_file.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 31 | 32 | 40 | 41 | 42 | 43 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/about/PrivacyPolicyFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.about 18 | 19 | import android.net.Uri 20 | import android.os.Bundle 21 | import android.view.* 22 | import androidx.browser.customtabs.CustomTabsIntent 23 | import androidx.fragment.app.Fragment 24 | import com.ibashkimi.screenrecorder.R 25 | import com.ibashkimi.screenrecorder.databinding.FragmentPrivacyPolicyBinding 26 | 27 | class PrivacyPolicyFragment : Fragment() { 28 | 29 | override fun onActivityCreated(savedInstanceState: Bundle?) { 30 | super.onActivityCreated(savedInstanceState) 31 | setHasOptionsMenu(true) 32 | } 33 | 34 | override fun onCreateView( 35 | inflater: LayoutInflater, 36 | container: ViewGroup?, 37 | savedInstanceState: Bundle? 38 | ): View { 39 | return FragmentPrivacyPolicyBinding.inflate(inflater, container, false).run { 40 | wvPrivacyPolicy.loadUrl("file:///android_asset/privacy_policy.html") 41 | root 42 | } 43 | } 44 | 45 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 46 | inflater.inflate(R.menu.privacy_policy, menu) 47 | } 48 | 49 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 50 | if (item.itemId == R.id.open_website) { 51 | CustomTabsIntent.Builder().build().launchUrl( 52 | requireContext(), 53 | Uri.parse(getString(R.string.privacy_policy_website)) 54 | ) 55 | return true 56 | } 57 | return super.onOptionsItemSelected(item) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/about_library.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | 32 | 39 | 40 | 47 | 48 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/menu/more_options.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 21 | 22 | 25 | 29 | 33 | 37 | 38 | 39 | 40 | 43 | 44 | 47 | 51 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder 18 | 19 | import android.annotation.TargetApi 20 | import android.app.Notification 21 | import android.app.NotificationChannel 22 | import android.app.NotificationManager 23 | import android.content.Context 24 | import android.graphics.Color 25 | import android.os.Build 26 | import androidx.annotation.RequiresApi 27 | 28 | const val RECORDING_NOTIFICATION_CHANNEL_ID = "channel_recording" 29 | const val FINISH_NOTIFICATION_CHANNEL_ID = "channel_finished" 30 | 31 | @TargetApi(26) 32 | fun Context.createNotificationChannels() { 33 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 34 | return 35 | } 36 | (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) 37 | .createNotificationChannels( 38 | listOf( 39 | channelRecording(), 40 | channelFinish() 41 | ) 42 | ) 43 | } 44 | 45 | @RequiresApi(Build.VERSION_CODES.O) 46 | private fun Context.channelRecording() = NotificationChannel( 47 | RECORDING_NOTIFICATION_CHANNEL_ID, 48 | getString(R.string.notification_channel_recording), 49 | NotificationManager.IMPORTANCE_LOW 50 | ).apply { 51 | enableLights(true) 52 | lightColor = Color.RED 53 | setShowBadge(false) 54 | enableVibration(false) 55 | lockscreenVisibility = Notification.VISIBILITY_PUBLIC 56 | } 57 | 58 | @RequiresApi(Build.VERSION_CODES.O) 59 | private fun Context.channelFinish() = NotificationChannel( 60 | FINISH_NOTIFICATION_CHANNEL_ID, 61 | getString(R.string.notification_channel_finish), 62 | NotificationManager.IMPORTANCE_DEFAULT 63 | ).apply { 64 | //enableLights(true) 65 | setShowBadge(true) 66 | enableVibration(false) 67 | lockscreenVisibility = Notification.VISIBILITY_PUBLIC 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder 18 | 19 | import android.os.Bundle 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.appcompat.widget.Toolbar 22 | import androidx.navigation.NavController 23 | import androidx.navigation.findNavController 24 | import androidx.navigation.ui.AppBarConfiguration 25 | import androidx.navigation.ui.NavigationUI 26 | import com.ibashkimi.screenrecorder.settings.PreferenceHelper 27 | 28 | class MainActivity : AppCompatActivity() { 29 | 30 | private lateinit var navController: NavController 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | if (savedInstanceState == null) { 34 | onFirstCreate() 35 | } 36 | 37 | super.onCreate(savedInstanceState) 38 | 39 | setContentView(R.layout.activity_main) 40 | 41 | navController = findNavController(R.id.main_nav_host_fragment) 42 | 43 | findViewById(R.id.toolbar)?.let { 44 | setSupportActionBar(it) 45 | val appBarConfiguration = AppBarConfiguration( 46 | setOf(R.id.home, R.id.navigation_dialog, R.id.more_settings_dialog) 47 | ) 48 | NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration) 49 | } 50 | } 51 | 52 | /** 53 | * Called the first time the activity is created. 54 | */ 55 | private fun onFirstCreate() { 56 | PreferenceHelper(this).apply { 57 | // Apply theme before onCreate 58 | applyNightMode(nightMode) 59 | initIfFirstTimeAnd { 60 | createNotificationChannels() 61 | } 62 | } 63 | } 64 | 65 | override fun onSupportNavigateUp() = navController.navigateUp() 66 | 67 | companion object { 68 | const val ACTION_TOGGLE_RECORDING = "com.ibashkimi.screenrecorder.TOGGLE_RECORDING" 69 | } 70 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/about/AboutFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.about 18 | 19 | import android.content.Intent 20 | import android.net.Uri 21 | import android.os.Bundle 22 | import android.view.LayoutInflater 23 | import android.view.View 24 | import android.view.ViewGroup 25 | import androidx.browser.customtabs.CustomTabsIntent 26 | import androidx.fragment.app.Fragment 27 | import androidx.navigation.Navigation 28 | import com.ibashkimi.screenrecorder.BuildConfig 29 | import com.ibashkimi.screenrecorder.R 30 | import com.ibashkimi.screenrecorder.databinding.FragmentAboutBinding 31 | 32 | 33 | class AboutFragment : Fragment() { 34 | 35 | override fun onCreateView( 36 | inflater: LayoutInflater, 37 | container: ViewGroup?, 38 | savedInstanceState: Bundle? 39 | ): View { 40 | return FragmentAboutBinding.inflate(inflater, container, false).run { 41 | version.text = getString(R.string.version, BuildConfig.VERSION_NAME) 42 | sourceCode.setOnClickListener { 43 | CustomTabsIntent.Builder().build().launchUrl( 44 | requireContext(), 45 | Uri.parse(requireContext().getString(R.string.app_source_code)) 46 | ) 47 | } 48 | sendFeedback.setOnClickListener { 49 | sendFeedback() 50 | } 51 | privacyPolicy.setOnClickListener( 52 | Navigation.createNavigateOnClickListener(R.id.action_about_to_privacy_policy) 53 | ) 54 | licenses.setOnClickListener( 55 | Navigation.createNavigateOnClickListener(R.id.action_about_to_licenses) 56 | ) 57 | root 58 | } 59 | } 60 | 61 | private fun sendFeedback() { 62 | val address = getString(R.string.author_email) 63 | val subject = getString(R.string.feedback_subject) 64 | 65 | val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$address")) 66 | emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) 67 | 68 | val chooserTitle = getString(R.string.feedback_chooser_title) 69 | startActivity(Intent.createChooser(emailIntent, chooserTitle)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.application' 18 | apply plugin: 'kotlin-android' 19 | apply plugin: 'kotlin-android-extensions' 20 | apply plugin: 'kotlin-kapt' 21 | apply plugin: 'androidx.navigation.safeargs' 22 | 23 | android { 24 | compileSdkVersion versions.android.compileSdk 25 | buildToolsVersion versions.android.buildTools 26 | defaultConfig { 27 | applicationId "com.ibashkimi.screenrecorder" 28 | minSdkVersion versions.android.minSdk 29 | targetSdkVersion versions.android.targetSdk 30 | versionCode 11 31 | versionName "1.1" 32 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 33 | } 34 | viewBinding { 35 | enabled = true 36 | } 37 | buildTypes { 38 | debug { 39 | applicationIdSuffix ".debug" 40 | } 41 | release { 42 | minifyEnabled true 43 | shrinkResources true 44 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 45 | } 46 | } 47 | 48 | compileOptions { 49 | sourceCompatibility JavaVersion.VERSION_1_8 50 | targetCompatibility JavaVersion.VERSION_1_8 51 | } 52 | kotlinOptions { 53 | jvmTarget = "1.8" 54 | } 55 | } 56 | 57 | dependencies { 58 | implementation fileTree(include: ['*.jar'], dir: 'libs') 59 | 60 | testImplementation deps.test.junit 61 | androidTestImplementation deps.test.espresso.core 62 | 63 | implementation deps.kotlin.stdlib 64 | implementation deps.kotlin.coroutines 65 | implementation deps.google.material 66 | implementation deps.androidx.appcompat 67 | implementation deps.androidx.browser 68 | implementation deps.androidx.cardview 69 | implementation deps.androidx.corektx 70 | implementation deps.androidx.documentfile 71 | implementation deps.androidx.localbroadcastmanager 72 | implementation deps.androidx.preference 73 | implementation deps.androidx.recyclerviewSelection 74 | implementation deps.androidx.swiperefreshlayout 75 | implementation deps.lifecycle.runtime 76 | implementation deps.lifecycle.extensions 77 | implementation deps.lifecycle.common 78 | implementation deps.lifecycle.livedata 79 | implementation deps.navigation.fragment 80 | implementation deps.navigation.ui 81 | 82 | implementation 'com.github.bumptech.glide:glide:4.11.0' 83 | kapt 'com.github.bumptech.glide:compiler:4.11.0' 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/navigation.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 28 | 31 | 34 | 37 | 40 | 41 | 44 | 47 | 52 | 55 | 58 | 59 | 64 | 68 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/home/MoreSettingsDialog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.home 18 | 19 | import android.os.Bundle 20 | import android.view.LayoutInflater 21 | import android.view.View 22 | import android.view.ViewGroup 23 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 24 | import com.ibashkimi.screenrecorder.R 25 | import com.ibashkimi.screenrecorder.databinding.FragmentMoreSettingsBinding 26 | import com.ibashkimi.screenrecorder.settings.PreferenceHelper 27 | 28 | class MoreSettingsDialog : BottomSheetDialogFragment() { 29 | override fun onCreateView( 30 | inflater: LayoutInflater, 31 | container: ViewGroup?, 32 | savedInstanceState: Bundle? 33 | ): View { 34 | return FragmentMoreSettingsBinding.inflate(inflater, container, false).run { 35 | val prefs = PreferenceHelper(requireContext()) 36 | navigationView.menu.findItem( 37 | when (prefs.sortBy) { 38 | PreferenceHelper.SortBy.NAME -> R.id.menu_sort_by_name 39 | PreferenceHelper.SortBy.DATE -> R.id.menu_sort_by_last_edit 40 | PreferenceHelper.SortBy.SIZE -> R.id.menu_sort_by_size 41 | PreferenceHelper.SortBy.DURATION -> TODO() 42 | } 43 | ).isChecked = true 44 | navigationView.menu.findItem( 45 | when (prefs.orderBy) { 46 | PreferenceHelper.OrderBy.ASCENDING -> R.id.menu_order_ascending 47 | PreferenceHelper.OrderBy.DESCENDING -> R.id.menu_order_descending 48 | } 49 | ).isChecked = true 50 | navigationView.setNavigationItemSelectedListener { 51 | when (it.groupId) { 52 | R.id.group_sort_by -> { 53 | prefs.sortBy = when (it.itemId) { 54 | R.id.menu_sort_by_name -> PreferenceHelper.SortBy.NAME 55 | R.id.menu_sort_by_last_edit -> PreferenceHelper.SortBy.DATE 56 | R.id.menu_sort_by_size -> PreferenceHelper.SortBy.SIZE 57 | else -> throw IllegalArgumentException("Unknown sort option.") 58 | } 59 | } 60 | R.id.group_order_by -> { 61 | prefs.orderBy = when (it.itemId) { 62 | R.id.menu_order_ascending -> PreferenceHelper.OrderBy.ASCENDING 63 | R.id.menu_order_descending -> PreferenceHelper.OrderBy.DESCENDING 64 | else -> throw IllegalArgumentException("Unknown order option.") 65 | } 66 | } 67 | } 68 | dismiss() 69 | true 70 | } 71 | root 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/recordings/RecordingsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.recordings 18 | 19 | import android.app.Application 20 | import android.content.Context 21 | import androidx.lifecycle.AndroidViewModel 22 | import androidx.lifecycle.LiveData 23 | import androidx.lifecycle.asLiveData 24 | import com.ibashkimi.screenrecorder.data.DataManager 25 | import com.ibashkimi.screenrecorder.data.MediaStoreDataSource 26 | import com.ibashkimi.screenrecorder.data.Recording 27 | import com.ibashkimi.screenrecorder.data.SAFDataSource 28 | import com.ibashkimi.screenrecorder.services.RecorderState 29 | import com.ibashkimi.screenrecorder.services.SaveUri 30 | import com.ibashkimi.screenrecorder.services.UriType 31 | import com.ibashkimi.screenrecorder.settings.PreferenceHelper 32 | import kotlinx.coroutines.Dispatchers 33 | import kotlinx.coroutines.ExperimentalCoroutinesApi 34 | import kotlinx.coroutines.flow.combine 35 | import kotlinx.coroutines.flow.emptyFlow 36 | import kotlinx.coroutines.flow.flatMapLatest 37 | import kotlinx.coroutines.flow.flowOn 38 | 39 | 40 | @ExperimentalCoroutinesApi 41 | class RecordingsViewModel(app: Application) : AndroidViewModel(app) { 42 | 43 | private val context: Context = getApplication() 44 | 45 | private var dataManager: DataManager? = null 46 | 47 | val recorderState = RecorderState.state 48 | 49 | val recordings: LiveData> 50 | 51 | init { 52 | val preferences = PreferenceHelper(context) 53 | recordings = preferences.saveLocationFlow.flatMapLatest { 54 | dataManager = createDataManager(it) 55 | dataManager?.recordings() ?: emptyFlow() 56 | }.flowOn(Dispatchers.IO).combine(preferences.sortOrderOptionsFlow) { recordings, options -> 57 | processData(recordings, options) 58 | }.flowOn(Dispatchers.Default).asLiveData() 59 | } 60 | 61 | private fun createDataManager(saveUri: SaveUri?) = when (saveUri?.type) { 62 | UriType.MEDIA_STORE -> DataManager(MediaStoreDataSource(context, saveUri.uri)) 63 | UriType.SAF -> DataManager(SAFDataSource(context, saveUri.uri)) 64 | else -> null 65 | } 66 | 67 | fun rename(recording: Recording, newName: String) { 68 | dataManager?.rename(recording, newName) 69 | } 70 | 71 | fun deleteRecording(recording: Recording) { 72 | dataManager?.delete(recording) 73 | } 74 | 75 | fun deleteRecordings(recordings: List) { 76 | dataManager?.delete(recordings.map { it.uri }) 77 | } 78 | 79 | private fun processData( 80 | recordings: List, 81 | options: PreferenceHelper.SortOrderOptions 82 | ): List { 83 | return recordings.filter { !it.isPending }.run { 84 | when (options.sortBy) { 85 | PreferenceHelper.SortBy.NAME -> sortedBy { it.title } 86 | PreferenceHelper.SortBy.DATE -> sortedBy { it.modified } 87 | PreferenceHelper.SortBy.DURATION -> sortedBy { it.duration } 88 | PreferenceHelper.SortBy.SIZE -> sortedBy { it.size } 89 | }.run { 90 | when (options.orderBy) { 91 | PreferenceHelper.OrderBy.ASCENDING -> this 92 | PreferenceHelper.OrderBy.DESCENDING -> reversed() 93 | } 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/services/RecordingSession.kt: -------------------------------------------------------------------------------- 1 | package com.ibashkimi.screenrecorder.services 2 | 3 | import android.app.Activity 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.Build 8 | import android.provider.MediaStore 9 | import com.ibashkimi.screenrecorder.data.DataManager 10 | 11 | class RecordingSession( 12 | val context: Context, 13 | val options: Options, 14 | private val dataManager: DataManager 15 | ) { 16 | 17 | private var recorder: Recorder? = null 18 | 19 | var state: RecorderState.State 20 | get() = RecorderState.state.value ?: RecorderState.State.STOPPED 21 | set(value) { 22 | RecorderState.state.value = value 23 | } 24 | 25 | var startTime: Long = 0 26 | 27 | var elapsedTime: Long = 0 28 | 29 | fun start(intent: Intent): Boolean { 30 | return if (state == RecorderState.State.STOPPED) { 31 | intent.getParcelableExtra(RecorderService.RECORDER_INTENT_DATA)?.let { intentData -> 32 | val result = 33 | intent.getIntExtra(RecorderService.RECORDER_INTENT_RESULT, Activity.RESULT_OK) 34 | val newRecorder = Recorder(context) 35 | if (newRecorder.start(result, intentData, options)) { 36 | startTime = System.currentTimeMillis() 37 | state = RecorderState.State.RECORDING 38 | recorder = newRecorder 39 | true 40 | } else { 41 | state = RecorderState.State.STOPPED 42 | //recorder = null 43 | false 44 | } 45 | } ?: false 46 | } else { 47 | true 48 | } 49 | } 50 | 51 | fun pause(): Boolean { 52 | return recorder?.let { 53 | when (state) { 54 | RecorderState.State.RECORDING -> { 55 | it.pause() 56 | //calculate total elapsed time until pause 57 | elapsedTime += System.currentTimeMillis() - startTime 58 | state = RecorderState.State.PAUSED 59 | true 60 | } 61 | RecorderState.State.STOPPED -> false 62 | RecorderState.State.PAUSED -> true 63 | } 64 | } ?: false 65 | } 66 | 67 | fun resume(): Boolean { 68 | return recorder?.let { 69 | return when (state) { 70 | RecorderState.State.PAUSED -> { 71 | it.resume() 72 | //Reset startTime to current time again 73 | startTime = System.currentTimeMillis() 74 | state = RecorderState.State.RECORDING 75 | true 76 | } 77 | RecorderState.State.RECORDING -> true 78 | RecorderState.State.STOPPED -> false 79 | } 80 | } ?: false 81 | } 82 | 83 | fun stop(): Boolean { 84 | recorder?.let { 85 | if (state == RecorderState.State.RECORDING || state == RecorderState.State.PAUSED) { 86 | if (it.stop()) { 87 | val now = System.currentTimeMillis() 88 | val values = ContentValues() 89 | values.put(MediaStore.Video.Media.DATE_ADDED, now) 90 | values.put(MediaStore.Video.Media.DATE_MODIFIED, now / 1000) 91 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 92 | values.put(MediaStore.Video.Media.IS_PENDING, 0) 93 | } 94 | 95 | dataManager.update(options.output.uri.uri, values) 96 | } 97 | } 98 | } 99 | 100 | recorder = null 101 | state = RecorderState.State.STOPPED 102 | return true 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/settings/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.ibashkimi.screenrecorder.settings 2 | 3 | import android.content.SharedPreferences 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.channels.awaitClose 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.callbackFlow 10 | 11 | class PreferenceChangedLiveData( 12 | private val sharedPreferences: SharedPreferences, 13 | private val keys: List 14 | ) : MutableLiveData(), SharedPreferences.OnSharedPreferenceChangeListener { 15 | 16 | override fun onActive() { 17 | sharedPreferences.registerOnSharedPreferenceChangeListener(this) 18 | } 19 | 20 | override fun onInactive() { 21 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) 22 | } 23 | 24 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { 25 | if (key in keys) { 26 | value = key 27 | } 28 | } 29 | } 30 | 31 | class PreferenceLiveData( 32 | private val sharedPreferences: SharedPreferences, 33 | private val key: String, 34 | private val loadFirst: Boolean = false, 35 | private val onKeyChanged: (String) -> T 36 | ) : MutableLiveData(), SharedPreferences.OnSharedPreferenceChangeListener { 37 | 38 | override fun onActive() { 39 | if (loadFirst) { 40 | value = onKeyChanged(key) 41 | } 42 | sharedPreferences.registerOnSharedPreferenceChangeListener(this) 43 | } 44 | 45 | override fun onInactive() { 46 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) 47 | } 48 | 49 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, changed: String) { 50 | if (key == changed) { 51 | value = onKeyChanged(key) 52 | } 53 | } 54 | } 55 | 56 | @ExperimentalCoroutinesApi 57 | fun preferencesChangedFlow( 58 | sharedPreferences: SharedPreferences, 59 | keys: List, 60 | emitOnCreate: Boolean = false 61 | ): Flow = callbackFlow { 62 | val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> 63 | if (key in keys) { 64 | offer(key) 65 | } 66 | } 67 | if (emitOnCreate) { 68 | offer(keys.first()) 69 | } 70 | sharedPreferences.registerOnSharedPreferenceChangeListener(listener) 71 | awaitClose { 72 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) 73 | } 74 | 75 | } 76 | 77 | @ExperimentalCoroutinesApi 78 | fun preferenceFlow( 79 | sharedPreferences: SharedPreferences, 80 | key: String, 81 | loadFirst: Boolean = false, 82 | onKeyChanged: (String) -> T 83 | ): Flow = callbackFlow { 84 | val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changed -> 85 | if (key == changed) { 86 | offer(onKeyChanged(key)) 87 | } 88 | } 89 | if (loadFirst) { 90 | offer(onKeyChanged(key)) 91 | } 92 | sharedPreferences.registerOnSharedPreferenceChangeListener(listener) 93 | awaitClose { 94 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) 95 | } 96 | } 97 | 98 | fun PreferenceHelper.preferenceFlow( 99 | key: String, 100 | loadFirst: Boolean = false, 101 | onKeyChanged: (String) -> T 102 | ): Flow = 103 | preferenceFlow(sharedPreferences, key, loadFirst, onKeyChanged) 104 | 105 | 106 | fun createPreferenceChangedLiveData( 107 | sharedPreferences: SharedPreferences, 108 | keys: List 109 | ): LiveData { 110 | return PreferenceChangedLiveData(sharedPreferences, keys) 111 | } 112 | 113 | fun createPreferenceLiveData( 114 | sharedPreferences: SharedPreferences, 115 | key: String, 116 | onChanged: (SharedPreferences, String) -> T 117 | ): LiveData { 118 | return PreferenceLiveData(sharedPreferences, key) { 119 | onChanged(sharedPreferences, it) 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/main/res/values/array.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 25 20 | 30 21 | 50 22 | 60 23 | 24 | 25 | 26 | 720P 27 | 1080P 28 | 1440P 29 | 4K 30 | 8K 31 | 32 | 33 | 34 | 480 35 | 720 36 | 1080 37 | 1440 38 | 2160 39 | 4320 40 | 41 | 42 | 43 | 2 Mbit 44 | 4 Mbit 45 | 6 Mbit 46 | 8 Mbit 47 | 10 Mbit 48 | 12 Mbit 49 | 16 Mbit 50 | 24 Mbit 51 | 52 | 53 | 54 | 2097152 55 | 4194304 56 | 6291456 57 | 8388608 58 | 10485760 59 | 12582912 60 | 16777216 61 | 25165824 62 | 63 | 64 | 65 | yyyyMMdd_hhmmss 66 | ddMMyyyy_hhmmss 67 | yyMMdd_hhmmss 68 | ddMMyy_hhmmss 69 | hhMMss_yyyymmdd 70 | hhMMss_ddmmyyyy 71 | hhMMss_yymmdd 72 | hhMMss_ddmmyy 73 | 74 | 75 | 76 | System default 77 | Always 78 | Never 79 | 80 | 81 | 82 | system_default 83 | dark 84 | light 85 | 86 | 87 | 88 | default 89 | H264 90 | HEVC 91 | 92 | 93 | 94 | Default 95 | H264 96 | HEVC 97 | 98 | 99 | 100 | 101 | 44100 102 | 48000 103 | 104 | 105 | 44.1 kHz 106 | 48 kHz 107 | 108 | 109 | 110 | 64000 111 | 96000 112 | 128000 113 | 192000 114 | 256000 115 | 320000 116 | 117 | 118 | 64 kbit/s 119 | 96 kbit/s 120 | 128 kbit/s 121 | 192 kbit/s 122 | 256 kbit/s 123 | 320 kbit/s 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_recording.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 33 | 34 | 41 | 42 | 49 | 50 | 58 | 59 | 64 | 65 | 72 | 73 | 80 | 81 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/recordings/RecordingListFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.recordings 18 | 19 | import android.content.Intent 20 | import android.os.Bundle 21 | import android.view.LayoutInflater 22 | import android.view.View 23 | import android.view.ViewGroup 24 | import android.widget.TextView 25 | import androidx.fragment.app.Fragment 26 | import androidx.fragment.app.viewModels 27 | import androidx.lifecycle.Observer 28 | import androidx.recyclerview.selection.SelectionTracker 29 | import androidx.recyclerview.selection.StorageStrategy 30 | import androidx.recyclerview.widget.LinearLayoutManager 31 | import androidx.recyclerview.widget.RecyclerView 32 | import com.ibashkimi.screenrecorder.R 33 | import com.ibashkimi.screenrecorder.data.Recording 34 | 35 | 36 | abstract class RecordingListFragment : Fragment() { 37 | 38 | private lateinit var messageView: TextView 39 | 40 | private lateinit var recordingsAdapter: RecordingAdapter 41 | 42 | protected lateinit var selectionTracker: SelectionTracker 43 | 44 | protected val viewModel: RecordingsViewModel by viewModels() 45 | 46 | final override fun onCreateView( 47 | inflater: LayoutInflater, 48 | container: ViewGroup?, 49 | savedInstanceState: Bundle? 50 | ): View? { 51 | val root = inflater.inflate(layoutRes, container, false) 52 | 53 | messageView = root.findViewById(R.id.message_no_video) 54 | messageView.visibility = View.GONE 55 | 56 | root.findViewById(R.id.videos_list).apply { 57 | val linearLayoutManager = LinearLayoutManager(context) 58 | layoutManager = linearLayoutManager 59 | recordingsAdapter = RecordingAdapter() 60 | adapter = recordingsAdapter 61 | selectionTracker = SelectionTracker.Builder( 62 | "recording-selection-id", 63 | this, 64 | RecordingKeyProvider(recordingsAdapter), 65 | RecordingDetailsLookup(this), 66 | StorageStrategy.createParcelableStorage(Recording::class.java) 67 | ) 68 | .withOnItemActivatedListener { item, _ -> 69 | onRecordingClick(item.selectionKey!!) 70 | return@withOnItemActivatedListener true 71 | } 72 | .build() 73 | savedInstanceState?.let { selectionTracker.onRestoreInstanceState(it) } 74 | recordingsAdapter.selectionTracker = selectionTracker 75 | } 76 | 77 | viewModel.recordings.observe(viewLifecycleOwner, Observer { 78 | onDataLoaded(it) 79 | }) 80 | 81 | return root 82 | } 83 | 84 | abstract val layoutRes: Int 85 | 86 | protected open fun onRecordingClick(recording: Recording) { 87 | val intent = Intent() 88 | intent.setAction(Intent.ACTION_VIEW) 89 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 90 | .setDataAndType( 91 | recording.uri, 92 | requireContext().contentResolver.getType(recording.uri) 93 | ) 94 | startActivity(intent) 95 | } 96 | 97 | protected fun onDataLoaded(data: List) { 98 | messageView.visibility = if (data.isEmpty()) View.VISIBLE else View.GONE 99 | recordingsAdapter.updateData(data) 100 | } 101 | 102 | protected fun rename(recording: Recording, newName: String) { 103 | viewModel.rename(recording, newName) 104 | } 105 | 106 | protected fun delete(recording: Recording) { 107 | viewModel.deleteRecording(recording) 108 | } 109 | 110 | protected fun delete(recordings: List) { 111 | viewModel.deleteRecordings(recordings) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/recordings/RecordingAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.recordings 18 | 19 | import android.net.Uri 20 | import android.text.format.DateUtils 21 | import android.view.LayoutInflater 22 | import android.view.ViewGroup 23 | import android.widget.ImageView 24 | import androidx.core.view.isVisible 25 | import androidx.recyclerview.selection.ItemDetailsLookup 26 | import androidx.recyclerview.selection.SelectionTracker 27 | import androidx.recyclerview.widget.RecyclerView 28 | import com.bumptech.glide.Glide 29 | import com.ibashkimi.screenrecorder.data.Recording 30 | import com.ibashkimi.screenrecorder.databinding.ItemRecordingBinding 31 | import java.text.DecimalFormat 32 | import kotlin.math.log10 33 | import kotlin.math.pow 34 | 35 | 36 | class RecordingAdapter(items: List = emptyList()) : 37 | RecyclerView.Adapter() { 38 | 39 | val data: ArrayList = ArrayList(items.size).apply { addAll(items) } 40 | 41 | lateinit var selectionTracker: SelectionTracker 42 | 43 | fun updateData(newData: List) { 44 | data.clear() 45 | data.addAll(newData) 46 | notifyDataSetChanged() 47 | } 48 | 49 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 50 | val binding = 51 | ItemRecordingBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false) 52 | return RecordingViewHolder(binding) 53 | } 54 | 55 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 56 | val recording = data[position] 57 | val recordingViewHolder = holder as RecordingViewHolder 58 | recordingViewHolder.bind(data[position], position, selectionTracker.isSelected(recording)) 59 | } 60 | 61 | override fun getItemCount() = data.size 62 | } 63 | 64 | class RecordingViewHolder(private val binding: ItemRecordingBinding) : 65 | RecyclerView.ViewHolder(binding.root) { 66 | 67 | var recording: Recording? = null 68 | var pos: Int = -1 69 | 70 | fun bind(recording: Recording, position: Int, isSelected: Boolean) { 71 | this.recording = recording 72 | this.pos = position 73 | binding.apply { 74 | foreground.isVisible = isSelected 75 | thumbnail.load(recording.uri) 76 | title.text = recording.title 77 | duration.text = toTime(recording.duration.toLong()) 78 | modified.text = DateUtils.getRelativeTimeSpanString(recording.modified) 79 | size.text = getFileSize(recording.size) 80 | } 81 | } 82 | 83 | fun getItemDetails(): ItemDetailsLookup.ItemDetails = 84 | RecordingDetails(pos, recording!!) 85 | 86 | private fun ImageView.load(uri: Uri) { 87 | Glide.with(this) 88 | .asBitmap() 89 | .centerCrop() 90 | .load(uri) 91 | .into(this) 92 | } 93 | 94 | private fun getFileSize(size: Long): String { 95 | if (size <= 0) 96 | return "0" 97 | val units = arrayOf("B", "KB", "MB", "GB", "TB") 98 | val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt() 99 | return DecimalFormat("#,##0.#").format(size / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] 100 | } 101 | 102 | private fun toTime(millis: Long): String { 103 | /*if (Build.VERSION.SDK_INT >= 26) { 104 | return LocalTime.ofSecondOfDay(millis / 1000).toString() 105 | }*/ 106 | val hours: Long = (millis / (1000 * 60 * 60)) 107 | val minutes = (millis % (1000 * 60 * 60) / (1000 * 60)) 108 | val seconds = (millis % (1000 * 60 * 60) % (1000 * 60) / 1000) 109 | return String.format("%02d:%02d:%02d", hours, minutes, seconds) 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/data/SAFDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.data 18 | 19 | import android.content.* 20 | import android.media.MediaMetadataRetriever 21 | import android.net.Uri 22 | import android.provider.DocumentsContract 23 | import androidx.documentfile.provider.DocumentFile 24 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 25 | import com.ibashkimi.screenrecorder.services.RecorderService 26 | import kotlinx.coroutines.channels.awaitClose 27 | import kotlinx.coroutines.flow.Flow 28 | import kotlinx.coroutines.flow.callbackFlow 29 | 30 | 31 | class SAFDataSource(val context: Context, val uri: Uri) : DataSource { 32 | 33 | override fun create( 34 | folderUri: Uri, 35 | name: String, 36 | mimeType: String, 37 | extra: ContentValues? 38 | ): Uri? { 39 | return DocumentFile.fromTreeUri(context, folderUri)?.createFile(mimeType, name)?.uri 40 | } 41 | 42 | override fun delete(uri: Uri) { 43 | DocumentsContract.deleteDocument(context.contentResolver, uri) 44 | notifyObservers() 45 | } 46 | 47 | override fun delete(uris: List) { 48 | uris.forEach { DocumentsContract.deleteDocument(context.contentResolver, it) } 49 | notifyObservers() 50 | } 51 | 52 | override fun rename(uri: Uri, newName: String) { 53 | DocumentsContract.renameDocument(context.contentResolver, uri, newName) 54 | notifyObservers() 55 | } 56 | 57 | fun fetchRecordings(): List { 58 | val recordings = ArrayList() 59 | DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { 60 | if (it.type == "video/mp4") { 61 | context.contentResolver.openFileDescriptor(it.uri, "r")?.fileDescriptor?.let { fd -> 62 | val retriever = MediaMetadataRetriever() 63 | retriever.setDataSource(fd) 64 | val time: String? = 65 | retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) 66 | retriever.release() 67 | time?.toInt() 68 | }?.let { duration -> 69 | // Duration is null when the file is corrupted or it is recording 70 | recordings.add( 71 | Recording( 72 | it.uri, it.name 73 | ?: "", duration, it.length(), it.lastModified() 74 | ) 75 | ) 76 | } 77 | } 78 | } 79 | return recordings 80 | } 81 | 82 | override fun recordings(): Flow> = callbackFlow { 83 | val receiver = object : BroadcastReceiver() { 84 | override fun onReceive(context: Context, intent: Intent) { 85 | offer(fetchRecordings()) 86 | } 87 | } 88 | registerReceiver(receiver) 89 | offer(fetchRecordings()) 90 | awaitClose { 91 | unregisterReceiver(receiver) 92 | } 93 | } 94 | 95 | override fun update(uri: Uri, values: ContentValues) { 96 | notifyObservers() 97 | } 98 | 99 | private fun registerReceiver(receiver: BroadcastReceiver) { 100 | LocalBroadcastManager.getInstance(context).registerReceiver(receiver, 101 | IntentFilter().apply { 102 | addAction(ACTION_CONTENT_CHANGED) 103 | addAction(RecorderService.ACTION_RECORDING_COMPLETED) 104 | addAction(RecorderService.ACTION_RECORDING_DELETED) 105 | }) 106 | } 107 | 108 | private fun unregisterReceiver(receiver: BroadcastReceiver) { 109 | LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver) 110 | } 111 | 112 | private fun notifyObservers() { 113 | LocalBroadcastManager.getInstance(context) 114 | .sendBroadcast(Intent(RecorderService.ACTION_RECORDING_DELETED)) 115 | } 116 | 117 | companion object { 118 | const val ACTION_CONTENT_CHANGED = "action_content_changed" 119 | } 120 | } -------------------------------------------------------------------------------- /versions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | def versions = [ 18 | android : [ 19 | compileSdk: 30, 20 | buildTools: '30.0.3', 21 | minSdk : 21, 22 | targetSdk : 30 23 | ], 24 | androidx: [ 25 | appcompat : '1.2.0', 26 | browser : '1.3.0', 27 | cardview : '1.0.0', 28 | core : '1.3.2', 29 | documentfile : '1.0.1', 30 | fragment : '1.3.0', 31 | lifecycle : '2.2.0', 32 | localbroadcastmanager : '1.0.0', 33 | navigation : '2.3.3', 34 | paging : '2.1.2', 35 | preference : '1.1.1', 36 | recyclerview : '1.1.0', 37 | recyclerviewSelection : '1.1.0', 38 | swiperefreshlayout : '1.0.0' 39 | ], 40 | google : [ 41 | material: '1.3.0' 42 | ], 43 | kotlin : '1.4.30', 44 | test : [ 45 | runner : '1.1.1', 46 | espresso: '3.2.0' 47 | ] 48 | ] 49 | 50 | def libraries = [ 51 | androidx : [ 52 | appcompat : "androidx.appcompat:appcompat:${versions.androidx.appcompat}", 53 | browser : "androidx.browser:browser:${versions.androidx.browser}", 54 | cardview : "androidx.cardview:cardview:${versions.androidx.cardview}", 55 | corektx : "androidx.core:core-ktx:${versions.androidx.core}", 56 | documentfile : "androidx.documentfile:documentfile:${versions.androidx.documentfile}", 57 | fragment : "androidx.fragment:fragment-ktx:${versions.androidx.fragment}", 58 | localbroadcastmanager: "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidx.localbroadcastmanager}", 59 | preference : "androidx.preference:preference:${versions.androidx.preference}", 60 | recyclerview : "androidx.recyclerview:recyclerview:${versions.androidx.recyclerview}", 61 | recyclerviewSelection: "androidx.recyclerview:recyclerview-selection:${versions.androidx.recyclerviewSelection}", 62 | swiperefreshlayout : "androidx.swiperefreshlayout:swiperefreshlayout:${versions.androidx.swiperefreshlayout}", 63 | ], 64 | google : [ 65 | material: "com.google.android.material:material:${versions.google.material}" 66 | ], 67 | kotlin : [ 68 | stdlib : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin", 69 | test : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin", 70 | plugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin", 71 | coroutines: 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7' 72 | ], 73 | lifecycle : [ 74 | common : "androidx.lifecycle:lifecycle-common-java8:${versions.androidx.lifecycle}", 75 | compiler : "androidx.lifecycle:lifecycle-compiler:${versions.androidx.lifecycle}", 76 | extensions: "androidx.lifecycle:lifecycle-extensions:${versions.androidx.lifecycle}", 77 | livedata : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidx.lifecycle}", 78 | runtime : "androidx.lifecycle:lifecycle-runtime:${versions.androidx.lifecycle}", 79 | viewmodel : "androidx.lifecycle:lifecycle-viewmodel:${versions.androidx.lifecycle}" 80 | ], 81 | navigation: [ 82 | fragment : "androidx.navigation:navigation-fragment-ktx:${versions.androidx.navigation}", 83 | safeArgs: [ 84 | plugin: "androidx.navigation:navigation-safe-args-gradle-plugin:${versions.androidx.navigation}" 85 | ], 86 | ui : "androidx.navigation:navigation-ui-ktx:${versions.androidx.navigation}", 87 | ], 88 | test : [ 89 | junit : 'junit:junit:4.12', 90 | runner : "androidx.test:runner:${versions.test.runner}", 91 | espresso: [ 92 | core: "androidx.test.espresso:espresso-core:${versions.test.espresso}" 93 | ] 94 | ] 95 | ] 96 | 97 | ext.versions = versions 98 | ext.deps = libraries 99 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | حول 5 | الإعدادات 6 | مشاركة 7 | سياسة الخصوصية 8 | 9 | 10 | التسجيلات 11 | المزيد من الإعدادات 12 | لا تسجيلات 13 | ابدأ التسجيل 14 | الاسم 15 | التاريخ 16 | الحجم 17 | التصنيف حسب 18 | اطلب 19 | تصاعدي 20 | تنازليًا 21 | @string/share 22 | حذف 23 | هل تريد حذف هذا الملف؟ 24 | هل تريد حذف جميع التسجيلات المحددة؟ 25 | إدراج اسم جديد 26 | اسم جديد 27 | الصورة المصغرة 28 | اختر مكان حفظ التسجيلات 29 | حدد الدليل 30 | 31 | 32 | تسجيل الشاشة 33 | التسجيل 34 | توقف 35 | إيقاف مؤقت 36 | استئناف 37 | حذف 38 | أثناء التسجيل 39 | انتهى التسجيل 40 | تم حفظ الفيديو 41 | افتح مقاطع الفيديو المسجلة 42 | خطأ في التسجيل 43 | تم إيقاف التسجيل مؤقتًا 44 | 45 | 46 | المظهر 47 | مظهر داكن 48 | save_location 49 | save_location_type 50 | المجلد 51 | إعادة تعيين 52 | التغيير 53 | 54 | فيديو 55 | الدقة 56 | إطارات في الثانية 57 | معدل البت 58 | التشفير 59 | 60 | صوت 61 | تسجيل الصوت 62 | تسجيل الصوت من الميكروفون مع فيديو الشاشة 63 | معدل أخذ العينات 64 | معدل البت 65 | 66 | الإخراج 67 | التنسيق 68 | بادئة 69 | 70 | مطلوب إذن لتسجيل الصوت 71 | اسأل مرة أخرى 72 | فتح الإعدادات 73 | 74 | 75 | 76 | @string/about 77 | تعليقات Screen Kap 78 | إرسال بريد إلكتروني ... 79 | الإصدار %s 80 | المساعدة 81 | إرسال تعليقات 82 | قانوني 83 | التراخيص 84 | يستخدم هذا التطبيق مكتبات مفتوحة المصدر التالية. 85 | سياسة الخصوصية 86 | هذا التطبيق مفتوح المصدر ومرخص بموجب Apache 2.0. 87 | شفرة المصدر 88 | فتح موقع الويب 89 | 90 | تبديل 91 | تبديل التسجيل 92 | تم تعطيل التبديل 93 | 94 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 41 | 42 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 65 | 66 | 73 | 74 | 81 | 82 | 83 | 84 | 85 | 90 | 98 | 106 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/about/LicensesFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.about 18 | 19 | import android.net.Uri 20 | import android.os.Bundle 21 | import android.view.LayoutInflater 22 | import android.view.View 23 | import android.view.ViewGroup 24 | import androidx.annotation.StringRes 25 | import androidx.browser.customtabs.CustomTabsIntent 26 | import androidx.fragment.app.Fragment 27 | import androidx.recyclerview.widget.LinearLayoutManager 28 | import androidx.recyclerview.widget.RecyclerView 29 | import com.ibashkimi.screenrecorder.R 30 | import com.ibashkimi.screenrecorder.databinding.AboutLibIntroBinding 31 | import com.ibashkimi.screenrecorder.databinding.AboutLibraryBinding 32 | import com.ibashkimi.screenrecorder.databinding.FragmentLicensesBinding 33 | import java.security.InvalidParameterException 34 | 35 | 36 | class LicensesFragment : Fragment() { 37 | 38 | private val intro: Int = R.string.about_lib_intro 39 | 40 | private val libraries: Array = arrayOf( 41 | Library( 42 | R.string.android_jetpack_name, 43 | R.string.android_jetpack_website, 44 | R.string.apache_v2 45 | ), 46 | Library( 47 | R.string.kotlin_name, 48 | R.string.kotlin_website, 49 | R.string.apache_v2 50 | ), 51 | Library( 52 | R.string.glide_name, 53 | R.string.glide_website, 54 | R.string.glide_license 55 | ), 56 | Library( 57 | R.string.material_components_name, 58 | R.string.material_components_website, 59 | R.string.apache_v2 60 | ) 61 | ) 62 | 63 | override fun onCreateView( 64 | inflater: LayoutInflater, 65 | container: ViewGroup?, 66 | savedInstanceState: Bundle? 67 | ): View { 68 | return FragmentLicensesBinding.inflate(inflater, container, false).run { 69 | recyclerView.layoutManager = LinearLayoutManager(context) 70 | recyclerView.adapter = LibraryAdapter(libraries) 71 | root 72 | } 73 | } 74 | 75 | 76 | private inner class LibraryAdapter(val libs: Array) : 77 | RecyclerView.Adapter() { 78 | 79 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 80 | return when (viewType) { 81 | VIEW_TYPE_INTRO -> { 82 | LibraryIntroHolder(AboutLibIntroBinding.inflate(parent.inflater, parent, false)) 83 | } 84 | VIEW_TYPE_LIBRARY -> { 85 | LibraryHolder(AboutLibraryBinding.inflate(parent.inflater, parent, false)) 86 | } 87 | else -> throw InvalidParameterException() 88 | } 89 | } 90 | 91 | private val ViewGroup.inflater: LayoutInflater 92 | get() = LayoutInflater.from(context) 93 | 94 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 95 | if (getItemViewType(position) == VIEW_TYPE_LIBRARY) { 96 | bindLibrary(holder as LibraryHolder, libs[position - 1]) 97 | } else { 98 | (holder as LibraryIntroHolder).binding.intro.setText(intro) 99 | } 100 | } 101 | 102 | override fun getItemViewType(position: Int): Int { 103 | return if (position == 0) VIEW_TYPE_INTRO else VIEW_TYPE_LIBRARY 104 | } 105 | 106 | override fun getItemCount(): Int { 107 | return libs.size + 1 // + 1 for the static intro view 108 | } 109 | 110 | private fun bindLibrary(holder: LibraryHolder, lib: Library) { 111 | holder.binding.apply { 112 | libraryName.setText(lib.name) 113 | libraryLink.setText(lib.website) 114 | libraryLicense.setText(lib.license) 115 | 116 | val clickListener: View.OnClickListener = View.OnClickListener { 117 | val position = holder.adapterPosition 118 | if (position != RecyclerView.NO_POSITION) { 119 | CustomTabsIntent.Builder().build().launchUrl( 120 | requireContext(), 121 | Uri.parse(getString(lib.website)) 122 | ) 123 | } 124 | } 125 | root.setOnClickListener(clickListener) 126 | } 127 | } 128 | } 129 | 130 | private class LibraryHolder(val binding: AboutLibraryBinding) : 131 | RecyclerView.ViewHolder(binding.root) 132 | 133 | private class LibraryIntroHolder(val binding: AboutLibIntroBinding) : 134 | RecyclerView.ViewHolder(binding.root) 135 | 136 | /** 137 | * Models an open source library we want to credit 138 | */ 139 | data class Library( 140 | @StringRes val name: Int, 141 | @StringRes val website: Int, 142 | @StringRes val license: Int 143 | ) 144 | 145 | companion object { 146 | 147 | private const val VIEW_TYPE_INTRO = 0 148 | private const val VIEW_TYPE_LIBRARY = 1 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/services/Recorder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.services 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.hardware.display.DisplayManager 22 | import android.hardware.display.VirtualDisplay 23 | import android.media.MediaRecorder 24 | import android.media.projection.MediaProjection 25 | import android.media.projection.MediaProjectionManager 26 | import android.os.Build 27 | import android.util.Log 28 | import java.io.IOException 29 | 30 | 31 | class Recorder(private val context: Context) { 32 | var isRecording: Boolean = false 33 | 34 | private var mediaRecorder: MediaRecorder? = null 35 | private var mediaProjection: MediaProjection? = null 36 | private var virtualDisplay: VirtualDisplay? = null 37 | private var mediaProjectionCallback: MediaProjectionCallback? = null 38 | 39 | fun start(result: Int, data: Intent, options: Options): Boolean { 40 | if (isRecording) { 41 | throw IllegalStateException("start called but Recorder is already recording.") 42 | } 43 | val newMediaRecorder = MediaRecorder() 44 | if (!newMediaRecorder.init(options)) { 45 | isRecording = false 46 | return false 47 | } 48 | mediaRecorder = newMediaRecorder 49 | 50 | //Set Callback for MediaProjection 51 | mediaProjectionCallback = MediaProjectionCallback() 52 | val projectionManager = 53 | context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager 54 | 55 | //Initialize MediaProjection using data received from Intent 56 | mediaProjection = projectionManager.getMediaProjection(result, data)?.apply { 57 | registerCallback(mediaProjectionCallback, null) 58 | virtualDisplay = createVirtualDisplay( 59 | "ScreenRecorder", 60 | options.video.resolution.width, 61 | options.video.resolution.height, 62 | options.video.virtualDisplayDpi, 63 | DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, 64 | newMediaRecorder.surface, 65 | null, 66 | null 67 | ) 68 | } 69 | 70 | return try { 71 | newMediaRecorder.start() 72 | isRecording = true 73 | true 74 | } catch (e: IllegalStateException) { 75 | isRecording = false 76 | mediaProjection?.stop() 77 | mediaRecorder = null 78 | mediaProjection = null 79 | mediaProjectionCallback = null 80 | false 81 | } 82 | } 83 | 84 | fun stop(): Boolean { 85 | return stopScreenSharing() 86 | } 87 | 88 | fun pause() { 89 | if (!isRecording) { 90 | throw IllegalStateException("Called pause but Recorder is not recording.") 91 | } 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 93 | mediaRecorder?.pause() 94 | } 95 | } 96 | 97 | fun resume() { 98 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 99 | mediaRecorder?.resume() 100 | } 101 | } 102 | 103 | private fun MediaRecorder.init(options: Options): Boolean { 104 | val fileDescriptor = context.contentResolver 105 | .openFileDescriptor(options.output.uri.uri, "w")?.fileDescriptor ?: return false 106 | 107 | try { 108 | setVideoSource(MediaRecorder.VideoSource.SURFACE) 109 | setOutputFile(fileDescriptor) 110 | when (options.audio) { 111 | is AudioOptions.RecordAudio -> { 112 | setAudioSource(options.audio.source) 113 | setAudioEncodingBitRate(options.audio.bitRate) 114 | setAudioSamplingRate(options.audio.samplingRate) 115 | } 116 | } 117 | setOutputFormat(options.output.format) 118 | setVideoSize(options.video.resolution.width, options.video.resolution.height) 119 | setVideoEncoder(options.video.encoder) 120 | if (options.audio is AudioOptions.RecordAudio) { 121 | setAudioEncoder(options.audio.encoder) 122 | } 123 | setVideoEncodingBitRate(options.video.bitrate) 124 | setVideoFrameRate(options.video.fps) 125 | 126 | prepare() 127 | return true 128 | } catch (e: IOException) { 129 | e.printStackTrace() 130 | return false 131 | } 132 | } 133 | 134 | private fun stopScreenSharing(): Boolean { 135 | if (virtualDisplay == null) { 136 | Log.d("Recorder", "Virtual display is null. Screen sharing already stopped") 137 | return true 138 | } 139 | var success: Boolean 140 | try { 141 | mediaRecorder?.stop() 142 | Log.i("Recorder", "MediaProjection Stopped") 143 | success = true 144 | } catch (e: RuntimeException) { 145 | Log.e( 146 | "Recorder", 147 | "Fatal exception! Destroying media projection failed." + "\n" + e.message 148 | ) 149 | success = false 150 | } finally { 151 | mediaRecorder?.reset() 152 | virtualDisplay?.release() 153 | mediaRecorder?.release() 154 | mediaProjection?.let { 155 | it.unregisterCallback(mediaProjectionCallback) 156 | it.stop() 157 | mediaProjection = null 158 | } 159 | } 160 | isRecording = false 161 | return success 162 | } 163 | 164 | private inner class MediaProjectionCallback : MediaProjection.Callback() { 165 | override fun onStop() { 166 | stopScreenSharing() 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/data/MediaStoreDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.data 18 | 19 | import android.annotation.SuppressLint 20 | import android.content.ContentProviderOperation 21 | import android.content.ContentUris 22 | import android.content.ContentValues 23 | import android.content.Context 24 | import android.database.ContentObserver 25 | import android.net.Uri 26 | import android.os.Build 27 | import android.os.Handler 28 | import android.os.HandlerThread 29 | import android.provider.MediaStore 30 | import kotlinx.coroutines.ExperimentalCoroutinesApi 31 | import kotlinx.coroutines.channels.awaitClose 32 | import kotlinx.coroutines.flow.Flow 33 | import kotlinx.coroutines.flow.callbackFlow 34 | 35 | 36 | class MediaStoreDataSource(val context: Context, val uri: Uri) : DataSource { 37 | 38 | override fun create( 39 | folderUri: Uri, 40 | name: String, 41 | mimeType: String, 42 | extra: ContentValues? 43 | ): Uri? { 44 | val values = ContentValues() 45 | //values.put(MediaStore.Video.Media.TITLE, fileTitle) 46 | values.put(MediaStore.Video.Media.DISPLAY_NAME, name) 47 | val now = System.currentTimeMillis() 48 | // DATE_ADDED is in milliseconds 49 | // DATE_MODIFIED is in seconds 50 | values.put(MediaStore.Video.Media.DATE_ADDED, now) 51 | values.put(MediaStore.Video.Media.DATE_MODIFIED, now / 1000) 52 | values.put(MediaStore.Video.Media.MIME_TYPE, mimeType) 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 54 | values.put(MediaStore.Video.Media.IS_PENDING, 1) 55 | extra?.apply { 56 | if (containsKey(MediaStore.Video.Media.RELATIVE_PATH)) 57 | values.put( 58 | MediaStore.Video.Media.RELATIVE_PATH, 59 | extra.getAsString(MediaStore.Video.Media.RELATIVE_PATH) 60 | ) 61 | } 62 | } 63 | 64 | return context.contentResolver.insert(folderUri, values) 65 | } 66 | 67 | override fun delete(uri: Uri) { 68 | context.contentResolver.delete(uri, null, null) 69 | } 70 | 71 | override fun delete(uris: List) { 72 | val operations = ArrayList(uris.size) 73 | uris.forEach { operations.add(ContentProviderOperation.newDelete(it).build()) } 74 | context.contentResolver.applyBatch(MediaStore.AUTHORITY, operations) 75 | } 76 | 77 | override fun rename(uri: Uri, newName: String) { 78 | val values = ContentValues() 79 | //values.put(MediaStore.Video.Media.TITLE, newName) 80 | values.put(MediaStore.Video.Media.DISPLAY_NAME, newName) 81 | // DATE_MODIFIED is in secondsContentProviderOperation 82 | //values.put(MediaStore.Video.Media.DATE_MODIFIED, System.currentTimeMillis() / 1000) 83 | context.contentResolver.update(uri, values, null, null) 84 | } 85 | 86 | fun fetchRecordings(): List { 87 | val recordings = ArrayList() 88 | @SuppressLint("InlinedApi") 89 | val projection = arrayOf( 90 | MediaStore.Video.Media._ID, 91 | MediaStore.Video.Media.DISPLAY_NAME, 92 | MediaStore.Video.Media.DATE_MODIFIED, 93 | MediaStore.Video.Media.SIZE, 94 | MediaStore.Video.Media.DURATION, 95 | MediaStore.Video.Media.IS_PENDING 96 | ) 97 | 98 | // Note: newUri works also with DocumentFile uris. Otherwise this is not necessary. 99 | //val newUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)) 100 | context.contentResolver.query( 101 | uri, 102 | projection, null, null, null 103 | )?.apply { 104 | if (moveToFirst()) { 105 | do { 106 | val isPending: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 107 | getInt(getColumnIndexOrThrow(MediaStore.Video.Media.IS_PENDING)) == 1 108 | } else { 109 | false 110 | } 111 | val displayName = getString( 112 | getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME) 113 | ) 114 | val modified = getLong( 115 | getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED) 116 | ) 117 | val size = getLong( 118 | getColumnIndexOrThrow(MediaStore.Video.Media.SIZE) 119 | ) 120 | val duration = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 121 | getInt(getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)) 122 | } else { 123 | 0 124 | } 125 | val fileUri = ContentUris.withAppendedId( 126 | uri, 127 | getLong(getColumnIndex(MediaStore.Video.Media._ID)) 128 | ) 129 | // DATE_MODIFIED is in seconds 130 | recordings.add( 131 | Recording( 132 | fileUri, 133 | displayName, 134 | duration, 135 | size, 136 | modified * 1000, 137 | isPending 138 | ) 139 | ) 140 | } while (moveToNext()) 141 | } 142 | close() 143 | } 144 | return recordings 145 | } 146 | 147 | override fun update(uri: Uri, values: ContentValues) { 148 | context.contentResolver.update(uri, values, null, null) 149 | } 150 | 151 | @ExperimentalCoroutinesApi 152 | override fun recordings(): Flow> = callbackFlow { 153 | val thread = HandlerThread("ContentObserverHandler") 154 | thread.start() 155 | val contentObserver = object : ContentObserver(Handler(thread.looper)) { 156 | override fun onChange(selfChange: Boolean) { 157 | onChange(selfChange, null) 158 | } 159 | 160 | override fun onChange(selfChange: Boolean, uri: Uri?) { 161 | offer(fetchRecordings()) 162 | } 163 | 164 | override fun deliverSelfNotifications() = true 165 | } 166 | context.contentResolver.registerContentObserver(uri, true, contentObserver) 167 | offer(fetchRecordings()) 168 | awaitClose { 169 | context.contentResolver.unregisterContentObserver(contentObserver) 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Screen Kap 19 | 20 | 21 | About 22 | Settings 23 | Share 24 | Privacy Policy 25 | 26 | 27 | Recordings 28 | More settings 29 | No recordings 30 | Start recording 31 | Name 32 | Date 33 | Size 34 | Sort by 35 | Order by 36 | Ascending 37 | Descending 38 | @string/share 39 | Delete 40 | Delete this file? 41 | Delete all selected recordings? 42 | Insert new name 43 | New name 44 | Thumbnail 45 | Choose where to save recordings 46 | Select Directory 47 | 48 | 49 | Screen recording 50 | Recording 51 | Stop 52 | Pause 53 | Resume 54 | Delete 55 | While recording 56 | Recording finished 57 | Video saved 58 | Open the recorded videos 59 | Recording error 60 | Recording paused 61 | 62 | 63 | dark_theme 64 | record_audio 65 | video_resolution 66 | fps 67 | video_bitrate 68 | filename 69 | file_prefix 70 | video_encoder 71 | audio_sampling_rate 72 | audio_encoder 73 | audio_encoder_bitrate 74 | orientation 75 | Appearance 76 | Dark theme 77 | save_location 78 | save_location_type 79 | Folder 80 | Reset 81 | Change 82 | Save location 83 | Recordings are saved in %s 84 | Click to choose where to save the recordings 85 | 86 | Video 87 | Resolution 88 | Frames per second 89 | Bit rate 90 | Encoder 91 | 92 | Audio 93 | Record audio 94 | Records audio from mic along with screen video 95 | Sampling rate 96 | Bitrate 97 | 98 | Output 99 | Format 100 | Prefix 101 | 102 | Permission to record audio is needed 103 | Ask again 104 | Open settings 105 | 106 | 107 | @string/about 108 | Screen Kap feedback 109 | Send email… 110 | bashkimidev@gmail.com 111 | Version %s 112 | Help 113 | Send feedback 114 | Legal 115 | Licenses 116 | This app uses the following open source libraries. 117 | Privacy Policy 118 | This app is open source and is licensed under Apache 2.0. 119 | Source code 120 | https://github.com/indritbashkimi/ScreenKap 121 | Open website 122 | https://screen-kap.web.app/ 123 | 124 | Toggle 125 | Toggle recording 126 | Toggle disabled 127 | 128 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_about.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 31 | 32 | 37 | 38 | 44 | 45 | 54 | 55 | 63 | 64 | 75 | 76 | 85 | 86 | 93 | 94 | 102 | 103 | 104 | 105 | 116 | 117 | 126 | 127 | 134 | 135 | 143 | 144 | 145 | 146 | 155 | 156 | 163 | 164 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /app/src/main/java/com/ibashkimi/screenrecorder/settings/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Indrit Bashkimi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibashkimi.screenrecorder.settings 18 | 19 | import android.Manifest 20 | import android.app.Activity 21 | import android.content.Context 22 | import android.content.Intent 23 | import android.content.SharedPreferences 24 | import android.content.pm.PackageManager 25 | import android.net.Uri 26 | import android.os.Build 27 | import android.os.Bundle 28 | import android.util.DisplayMetrics 29 | import android.view.WindowManager 30 | import androidx.core.content.ContextCompat 31 | import androidx.preference.* 32 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 33 | import com.ibashkimi.screenrecorder.R 34 | import com.ibashkimi.screenrecorder.applyNightMode 35 | import com.ibashkimi.screenrecorder.services.UriType 36 | 37 | 38 | class SettingsFragment : PreferenceFragmentCompat(), 39 | SharedPreferences.OnSharedPreferenceChangeListener { 40 | 41 | private lateinit var recordAudio: SwitchPreference 42 | 43 | private lateinit var preferenceHelper: PreferenceHelper 44 | 45 | private val realDisplayMetrics: DisplayMetrics 46 | get() { 47 | val metrics = DisplayMetrics() 48 | val window = requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 50 | requireContext().display!! 51 | } else { 52 | @Suppress("DEPRECATION") 53 | window.defaultDisplay 54 | }.getRealMetrics(metrics) 55 | return metrics 56 | } 57 | 58 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 59 | addPreferencesFromResource(R.xml.settings) 60 | 61 | preferenceHelper = PreferenceHelper(requireContext(), preferenceScreen.sharedPreferences) 62 | 63 | findPreference(getString(R.string.pref_key_resolution))?.let { 64 | initResolutionPreference(it) 65 | } 66 | 67 | recordAudio = findPreference(getString(R.string.pref_key_audio))!! 68 | checkAudioRecPermission() 69 | 70 | notifyPreferenceChanged(R.string.pref_key_file_prefix, R.string.pref_key_save_location) 71 | 72 | } 73 | 74 | override fun onPreferenceTreeClick(preference: Preference): Boolean { 75 | if (preference.key == getString(R.string.pref_key_save_location)) { 76 | MaterialAlertDialogBuilder(requireContext()) 77 | .setTitle(R.string.pref_save_location_dialog_title) 78 | .setMessage( 79 | getString( 80 | R.string.pref_save_location_dialog_message, 81 | preferenceHelper.saveLocation?.uri?.toReadableString() 82 | ) 83 | ) 84 | .setNeutralButton(R.string.reset_save_location) { dialog, _ -> 85 | preferenceHelper.resetSaveLocation() 86 | dialog.dismiss() 87 | } 88 | .setNegativeButton(android.R.string.cancel) { dialog, _ -> 89 | dialog.cancel() 90 | } 91 | .setPositiveButton(R.string.change_save_location) { dialog, _ -> 92 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 93 | intent.putExtra( 94 | "android.provider.extra.INITIAL_URI", 95 | preferenceHelper.saveLocation?.uri 96 | ) 97 | startActivityForResult(intent, REQUEST_DOCUMENT_TREE) 98 | dialog.dismiss() 99 | } 100 | .show() 101 | return true 102 | } 103 | return super.onPreferenceTreeClick(preference) 104 | } 105 | 106 | override fun onResume() { 107 | super.onResume() 108 | preferenceScreen.sharedPreferences 109 | .registerOnSharedPreferenceChangeListener(this) 110 | } 111 | 112 | override fun onPause() { 113 | super.onPause() 114 | preferenceScreen.sharedPreferences 115 | .unregisterOnSharedPreferenceChangeListener(this) 116 | } 117 | 118 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { 119 | when (key) { 120 | getString(R.string.pref_key_dark_theme) -> { 121 | requireActivity().applyNightMode( 122 | PreferenceHelper(requireContext()).nightMode 123 | ) 124 | } 125 | getString(R.string.pref_key_save_location) -> { 126 | val uri = preferenceHelper.saveLocation?.uri 127 | findPreference(key)?.summary = 128 | uri?.toReadableString() 129 | ?: "Click to choose where to save the recordings" // todo 130 | } 131 | getString(R.string.pref_key_resolution) -> initResolutionPreference( 132 | findPreference( 133 | key 134 | ) as ListPreference 135 | ) 136 | getString(R.string.pref_key_audio) -> if (recordAudio.isChecked) { 137 | requestAudioPermission() 138 | } 139 | getString(R.string.pref_key_file_prefix) -> { 140 | val prefix = sharedPreferences.getString(key, "")!! 141 | val trimmedPrefix = prefix.trim() 142 | if (prefix != trimmedPrefix) { 143 | sharedPreferences.edit().putString(key, trimmedPrefix).apply() 144 | } else { 145 | findPreference(key)?.summary = prefix 146 | } 147 | } 148 | } 149 | } 150 | 151 | override fun onRequestPermissionsResult( 152 | requestCode: Int, 153 | permissions: Array, 154 | grantResults: IntArray 155 | ) { 156 | when (requestCode) { 157 | REQUEST_AUDIO_PERMISSION -> { 158 | checkAudioRecPermission() 159 | } 160 | } 161 | } 162 | 163 | private fun checkAudioRecPermission() { 164 | if (recordAudio.isChecked) { 165 | if (ContextCompat.checkSelfPermission( 166 | requireContext(), 167 | Manifest.permission.RECORD_AUDIO 168 | ) 169 | != PackageManager.PERMISSION_GRANTED 170 | ) { 171 | recordAudio.isChecked = false 172 | if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { 173 | MaterialAlertDialogBuilder(requireContext()) 174 | .setMessage(R.string.audio_permission_need) 175 | .setPositiveButton(R.string.dialog_ask_again) { _, _ -> 176 | requestAudioPermission() 177 | } 178 | .setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> 179 | dialogInterface.dismiss() 180 | } 181 | .show() 182 | } else { 183 | MaterialAlertDialogBuilder(requireContext()) 184 | .setMessage(R.string.audio_permission_need) 185 | .setPositiveButton(R.string.dialog_open_app_settings) { _, _ -> 186 | startActivity( 187 | Intent( 188 | android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 189 | Uri.fromParts("package", requireActivity().packageName, null) 190 | ) 191 | ) 192 | } 193 | .setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> 194 | dialogInterface.dismiss() 195 | } 196 | .show() 197 | } 198 | } 199 | } 200 | } 201 | 202 | private fun requestAudioPermission() { 203 | requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_AUDIO_PERMISSION) 204 | } 205 | 206 | private fun initResolutionPreference(resolutionPreference: ListPreference) { 207 | val resolutionValues = resources.getStringArray(R.array.resolution_values) 208 | .filter { it.toInt() <= realDisplayMetrics.widthPixels } 209 | val resolutionTitles = resources.getStringArray(R.array.resolution_entries) 210 | .slice(resolutionValues.indices) 211 | resolutionPreference.entryValues = resolutionValues.toTypedArray() 212 | resolutionPreference.entries = resolutionTitles.toTypedArray() 213 | } 214 | 215 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 216 | if (requestCode == REQUEST_DOCUMENT_TREE) { 217 | if (resultCode == Activity.RESULT_OK) { 218 | val uri: Uri = data!!.data!! 219 | requireContext().contentResolver.apply { 220 | takePersistableUriPermission( 221 | uri, 222 | Intent.FLAG_GRANT_READ_URI_PERMISSION or 223 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 224 | ) 225 | persistedUriPermissions.filter { it.uri == uri }.apply { 226 | if (isNotEmpty()) { 227 | preferenceHelper.setSaveLocation(uri, UriType.SAF) 228 | notifyPreferenceChanged(R.string.pref_key_save_location) 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | private fun notifyPreferenceChanged(vararg keyStringRes: Int) { 237 | keyStringRes.forEach { 238 | onSharedPreferenceChanged(preferenceScreen.sharedPreferences, getString(it)) 239 | } 240 | } 241 | 242 | private fun Uri.toReadableString(): String? = this.lastPathSegment?.split(":")?.last() 243 | 244 | companion object { 245 | private const val REQUEST_AUDIO_PERMISSION = 222 246 | const val REQUEST_DOCUMENT_TREE = 22 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 [yyyy] [name of copyright owner] 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 | --------------------------------------------------------------------------------