├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── tech │ │ └── thdev │ │ └── mediaprojectionexample │ │ ├── ApplicationTest.kt │ │ └── MainActivityTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── tech │ │ │ └── thdev │ │ │ └── mediaprojectionexample │ │ │ └── ui │ │ │ ├── main │ │ │ └── MainActivity.kt │ │ │ ├── mediaprojection │ │ │ └── MediaProjectionActivity.kt │ │ │ ├── service │ │ │ ├── VideoViewService.kt │ │ │ └── WindowTouchEvent.kt │ │ │ ├── surface │ │ │ └── SurfaceViewHolder.kt │ │ │ └── util │ │ │ └── DeviceUtil.kt │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_media_projection.xml │ │ ├── content_main.xml │ │ ├── content_media_projection.xml │ │ └── window_video_view.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── tech │ └── thdev │ └── mediaprojectionexample │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mediaProjectionLibrary ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── tech │ │ └── thdev │ │ └── media_projection_library │ │ ├── MediaProjectionStatus.kt │ │ ├── constant │ │ └── MediaProjectionConstant.kt │ │ └── ui │ │ ├── MediaProjectionAccessActivity.kt │ │ ├── MediaProjectionAccessBroadcastReceiver.kt │ │ ├── MediaProjectionAccessService.kt │ │ └── MediaProjectionAccessServiceBroadcastReceiver.kt │ └── res │ ├── drawable │ └── ic_baseline_fiber_manual_record_24.xml │ ├── layout │ └── activity_media_projection.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | /.idea/ 10 | gen/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaProjectionExample 2 | [![License](https://img.shields.io/hexpm/l/plug.svg)]() 3 | 4 | Maybe MVP pattern MediaProjection Example. 5 | 6 | - Develop Android studio 3.6.3 7 | 8 | ## Requirements 9 | 10 | - Target Sdk Version : 29 11 | - Min Sdk Version : 24 12 | 13 | 14 | ## Blog 15 | [MediaProjection 사용해보기](http://thdev.tech/Android-MediaProjection-Exmple/) 16 | 17 | 18 | ## Use Library 19 | - [PlayPauseButton](https://github.com/recruit-lifestyle/PlayPauseButton) 20 | - [MediaProjection](http://developer.android.com/reference/android/media/projection/package-summary.html) 21 | - [SurfaceView](http://developer.android.com/reference/android/view/SurfaceView.html) 22 | 23 | 24 | ## License 25 | 26 | ``` 27 | Copyright 2016, 2020 Taehwan 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | See the License for the specific language governing permissions and 39 | limitations under the License. 40 | ``` 41 | 42 | 43 | - PlayPauseButton 44 | 45 | ``` 46 | Copyright 2015 RECRUIT LIFESTYLE CO., LTD. 47 | 48 | Licensed under the Apache License, Version 2.0 (the "License"); 49 | you may not use this file except in compliance with the License. 50 | You may obtain a copy of the License at 51 | 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | 54 | Unless required by applicable law or agreed to in writing, software 55 | distributed under the License is distributed on an "AS IS" BASIS, 56 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 57 | See the License for the specific language governing permissions and 58 | limitations under the License. 59 | ``` -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion compileSdkVersionInfo 7 | buildToolsVersion buildToolsVersionInfo 8 | 9 | defaultConfig { 10 | applicationId "tech.thdev.app" 11 | minSdkVersion minSdkVerisonInfo 12 | targetSdkVersion targetSdkVersionInfo 13 | versionCode 4 14 | versionName "1.1.0" 15 | 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | 31 | viewBinding { 32 | enabled = true 33 | } 34 | } 35 | 36 | dependencies { 37 | implementation fileTree(dir: 'libs', include: ['*.jar']) 38 | 39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 40 | 41 | implementation "androidx.appcompat:appcompat:$appcompatVersion" 42 | implementation "androidx.activity:activity:$activityVersion" 43 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" 44 | implementation "com.google.android.material:material:$materialVersion" 45 | 46 | implementation "androidx.core:core-ktx:$coreKtx" 47 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleExtensions" 48 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleExtensions" 49 | 50 | implementation 'com.github.recruit-lifestyle:PlayPauseButton:1.0' 51 | 52 | implementation project(':mediaProjectionLibrary') 53 | 54 | // Testing-only dependencies 55 | testImplementation 'junit:junit:4.12' 56 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 57 | androidTestImplementation 'androidx.test:runner:1.2.0' 58 | androidTestImplementation 'androidx.test:rules:1.2.0' 59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 60 | 61 | // add this for intent mocking support 62 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' 63 | } 64 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/taehwankwon/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/thdev/mediaprojectionexample/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.example.myapplication", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/thdev/mediaprojectionexample/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.action.ViewActions.click 5 | import androidx.test.espresso.matcher.ViewMatchers.withId 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.rule.ActivityTestRule 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import tech.thdev.mediaprojectionexample.ui.main.MainActivity 12 | import java.util.concurrent.CountDownLatch 13 | import java.util.concurrent.TimeUnit 14 | 15 | /** 16 | * Created by Tae-hwan on 4/25/16. 17 | */ 18 | @RunWith(AndroidJUnit4::class) 19 | class MainActivityTest { 20 | // create a signal to let us know when our task is done. 21 | val signal = CountDownLatch(1) 22 | 23 | @Rule 24 | var activityRule: ActivityTestRule = ActivityTestRule(MainActivity::class.java) 25 | 26 | /** 27 | * Test example. 28 | * MediaProjection Don't show again checked test. 29 | * 30 | * @throws Exception 31 | */ 32 | @Test 33 | @Throws(Exception::class) 34 | fun testStartActivity() { 35 | onView(withId(R.id.btn_start_media_projection_activity)).perform(click()) 36 | onView(withId(R.id.fab)).perform(click()) 37 | 38 | /* The testing thread will wait here until the UI thread releases it 39 | * above with the countDown() or 5 seconds passes and it times out. 40 | */signal.await(5, TimeUnit.SECONDS) 41 | onView(withId(R.id.fab)).perform(click()) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/mediaprojectionexample/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample.ui.main 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.provider.Settings 10 | import androidx.activity.result.contract.ActivityResultContract 11 | import androidx.appcompat.app.AlertDialog 12 | import androidx.appcompat.app.AppCompatActivity 13 | import tech.thdev.mediaprojectionexample.R 14 | import tech.thdev.mediaprojectionexample.databinding.ActivityMainBinding 15 | import tech.thdev.mediaprojectionexample.databinding.ContentMainBinding 16 | import tech.thdev.mediaprojectionexample.ui.mediaprojection.MediaProjectionActivity 17 | import tech.thdev.mediaprojectionexample.ui.service.VideoViewService 18 | 19 | /** 20 | * MediaProjection Sample main 21 | * 22 | * Created by Tae-hwan on 4/8/16. 23 | */ 24 | class MainActivity : AppCompatActivity() { 25 | 26 | private val canOverlay = registerForActivityResult(OverlayActivityResultContract()) { 27 | runService() 28 | } 29 | 30 | private fun runService() { 31 | if (Settings.canDrawOverlays(this)) { 32 | startService() 33 | } else { 34 | showRejectDialog() 35 | } 36 | } 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | val binding = ActivityMainBinding.inflate(layoutInflater) 41 | val contentMainBinding = ContentMainBinding.inflate(layoutInflater, binding.root, true) 42 | setContentView(binding.root) 43 | 44 | setSupportActionBar(binding.toolbar) 45 | 46 | contentMainBinding.btnStartMediaProjectionService.setOnClickListener { 47 | runService() 48 | } 49 | 50 | contentMainBinding.btnStartMediaProjectionActivity.setOnClickListener { 51 | startActivity(MediaProjectionActivity.newInstance(this)) 52 | } 53 | } 54 | 55 | private fun showRejectDialog() { 56 | AlertDialog.Builder(this) 57 | .setTitle(R.string.reject_dialog_title) 58 | .setMessage(R.string.reject_dialog_message) 59 | .setPositiveButton("Ok") { _, _ -> 60 | canOverlay.launch("package:$packageName") 61 | } 62 | .setNegativeButton("Cancel", null) 63 | .show() 64 | } 65 | 66 | private fun startService() { 67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 68 | startForegroundService(VideoViewService.newService(this)) 69 | } else { 70 | startService(VideoViewService.newService(this)) 71 | } 72 | } 73 | } 74 | 75 | class OverlayActivityResultContract : ActivityResultContract() { 76 | 77 | override fun createIntent(context: Context, input: String?): Intent = 78 | Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(input)) 79 | 80 | override fun parseResult(resultCode: Int, intent: Intent?): Boolean = 81 | resultCode == Activity.RESULT_OK 82 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/mediaprojectionexample/ui/mediaprojection/MediaProjectionActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample.ui.mediaprojection 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.google.android.material.snackbar.Snackbar 9 | import tech.thdev.media_projection_library.MediaProjectionStatus 10 | import tech.thdev.media_projection_library.MediaProjectionStatusData 11 | import tech.thdev.media_projection_library.ui.MediaProjectionAccessService 12 | import tech.thdev.media_projection_library.ui.MediaProjectionAccessServiceBroadcastReceiver 13 | import tech.thdev.mediaprojectionexample.R 14 | import tech.thdev.mediaprojectionexample.databinding.ActivityMediaProjectionBinding 15 | import tech.thdev.mediaprojectionexample.databinding.ContentMediaProjectionBinding 16 | import tech.thdev.mediaprojectionexample.ui.surface.SurfaceViewHolder 17 | import tech.thdev.mediaprojectionexample.ui.util.DeviceUtil 18 | 19 | /** 20 | * Created by Tae-hwan on 4/8/16. 21 | * 22 | * MediaProjection example. 23 | */ 24 | class MediaProjectionActivity : AppCompatActivity() { 25 | 26 | companion object { 27 | 28 | fun newInstance(context: Context): Intent = 29 | Intent(context, MediaProjectionActivity::class.java) 30 | } 31 | 32 | private val surfaceViewHolder: SurfaceViewHolder by lazy { 33 | SurfaceViewHolder() 34 | } 35 | 36 | private var isStart = false 37 | 38 | private lateinit var binding: ActivityMediaProjectionBinding 39 | private lateinit var contentMainBinding: ContentMediaProjectionBinding 40 | 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | binding = ActivityMediaProjectionBinding.inflate(layoutInflater) 44 | contentMainBinding = ContentMediaProjectionBinding.inflate(layoutInflater, binding.root, true) 45 | setContentView(binding.root) 46 | 47 | setSupportActionBar(binding.toolbar) 48 | 49 | contentMainBinding.surfaceView.holder.addCallback(surfaceViewHolder) 50 | 51 | binding.fab.setOnClickListener { 52 | if (isStart) { 53 | stopMediaProjection() 54 | } else { 55 | mediaProjectionInit() 56 | } 57 | } 58 | } 59 | 60 | private fun onChangeStatus(statusData: MediaProjectionStatusData) { 61 | when (statusData.status) { 62 | MediaProjectionStatus.OnInitialized -> { 63 | isStart = false 64 | startMediaProjection() 65 | } 66 | MediaProjectionStatus.OnStarted -> { 67 | isStart = true 68 | binding.fab.setImageResource(android.R.drawable.ic_media_pause) 69 | } 70 | MediaProjectionStatus.OnStop -> { 71 | isStart = false 72 | binding.fab.setImageResource(android.R.drawable.ic_media_play) 73 | stopService() 74 | } 75 | MediaProjectionStatus.OnFail -> { 76 | isStart = false 77 | binding.fab.setImageResource(android.R.drawable.ic_media_play) 78 | Snackbar.make(binding.fab, R.string.media_projection_fail, Snackbar.LENGTH_SHORT).show() 79 | stopService() 80 | } 81 | MediaProjectionStatus.OnReject -> { 82 | isStart = false 83 | Snackbar.make(binding.fab, R.string.media_projection_reject, Snackbar.LENGTH_SHORT).show() 84 | stopService() 85 | } 86 | } 87 | } 88 | 89 | private fun mediaProjectionInit() { 90 | runService(MediaProjectionAccessService.newService(this)) 91 | MediaProjectionAccessServiceBroadcastReceiver.register(this, ::onChangeStatus) 92 | } 93 | 94 | private fun startMediaProjection() { 95 | val deviceSize = DeviceUtil.getDeviceSize(this) 96 | runService(MediaProjectionAccessService.newStartMediaProjection( 97 | context = this, 98 | surface = contentMainBinding.surfaceView.holder.surface, 99 | width = deviceSize.width, 100 | height = deviceSize.height 101 | )) 102 | } 103 | 104 | private fun stopMediaProjection() { 105 | runService(MediaProjectionAccessService.newStopMediaProjection(this)) 106 | } 107 | 108 | private fun stopService() { 109 | runService(MediaProjectionAccessService.newStopService(this)) 110 | MediaProjectionAccessServiceBroadcastReceiver.unregister(this) 111 | } 112 | 113 | private fun runService(service: Intent) { 114 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 115 | startForegroundService(service) 116 | } else { 117 | startService(service) 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/mediaprojectionexample/ui/service/VideoViewService.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample.ui.service 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Color 7 | import android.graphics.PixelFormat 8 | import android.os.Build 9 | import android.os.IBinder 10 | import android.view.Gravity 11 | import android.view.LayoutInflater 12 | import android.view.WindowManager 13 | import android.widget.Toast 14 | import tech.thdev.media_projection_library.MediaProjectionStatus 15 | import tech.thdev.media_projection_library.MediaProjectionStatusData 16 | import tech.thdev.media_projection_library.ui.MediaProjectionAccessService 17 | import tech.thdev.mediaprojectionexample.R 18 | import tech.thdev.mediaprojectionexample.databinding.WindowVideoViewBinding 19 | import tech.thdev.mediaprojectionexample.ui.surface.SurfaceViewHolder 20 | import tech.thdev.mediaprojectionexample.ui.util.DeviceUtil 21 | 22 | /** 23 | * Created by Tae-hwan on 4/8/16. 24 | */ 25 | class VideoViewService : MediaProjectionAccessService() { 26 | 27 | companion object { 28 | fun newService(context: Context): Intent = 29 | Intent(context, VideoViewService::class.java) 30 | } 31 | 32 | private lateinit var windowBinding: WindowVideoViewBinding 33 | 34 | private val windowManager: WindowManager by lazy { 35 | getSystemService(Context.WINDOW_SERVICE) as WindowManager 36 | } 37 | 38 | private lateinit var windowViewLayoutParams: WindowManager.LayoutParams 39 | 40 | private val surfaceViewHolder: SurfaceViewHolder by lazy { 41 | SurfaceViewHolder() 42 | } 43 | 44 | override fun onBind(intent: Intent?): IBinder? { 45 | return null 46 | } 47 | 48 | override fun onCreate() { 49 | super.onCreate() 50 | initWindowLayout(getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater) 51 | windowBinding.initCreate() 52 | } 53 | 54 | @SuppressLint("ClickableViewAccessibility") 55 | private fun WindowVideoViewBinding.initCreate() { 56 | surfaceView.holder.addCallback(surfaceViewHolder) 57 | root.setOnTouchListener(WindowTouchEvent(updateViewLayout = ::updateViewPosition)) 58 | btnStopService.setOnClickListener { stopSelf() } 59 | mainPlayPauseButton.run { 60 | setColor(Color.DKGRAY) 61 | setOnClickListener { 62 | // Do nothing. 63 | } 64 | setOnControlStatusChangeListener { _, state -> 65 | if (state) { 66 | createMediaProjection() 67 | } else { 68 | stopMediaProjection() 69 | } 70 | } 71 | } 72 | } 73 | 74 | private fun updateViewPosition(x: Int, y: Int) { 75 | windowViewLayoutParams.x += x 76 | windowViewLayoutParams.y += y 77 | windowManager.updateViewLayout(windowBinding.root, windowViewLayoutParams) 78 | } 79 | 80 | /** 81 | * Window View 를 초기화 한다. X, Y 좌표는 0, 0으로 지정한다. 82 | */ 83 | private fun initWindowLayout(layoutInflater: LayoutInflater) { 84 | windowBinding = WindowVideoViewBinding.inflate(layoutInflater, null, false).also { 85 | windowViewLayoutParams = WindowManager.LayoutParams( 86 | WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 87 | 30, 30, // X, Y 좌표 88 | WindowManager.LayoutParams.TYPE_TOAST 89 | .takeIf { Build.VERSION.SDK_INT < Build.VERSION_CODES.O } 90 | ?: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, 91 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, 92 | PixelFormat.TRANSLUCENT 93 | ) 94 | windowViewLayoutParams.gravity = Gravity.TOP or Gravity.START 95 | } 96 | windowManager.addView(windowBinding.root, windowViewLayoutParams) 97 | } 98 | 99 | /** 100 | * Play button design change 101 | */ 102 | private fun setPlayed(isPlayed: Boolean) { 103 | windowBinding.mainPlayPauseButton.run { 104 | if (this.isPlayed != isPlayed) { 105 | this.isPlayed = isPlayed 106 | startAnimation() 107 | } 108 | } 109 | } 110 | 111 | override fun onDestroy() { 112 | super.onDestroy() 113 | if (::windowBinding.isInitialized) { 114 | windowManager.removeView(windowBinding.root) 115 | } 116 | } 117 | 118 | override fun onChangeStatus(statusData: MediaProjectionStatusData) { 119 | super.onChangeStatus(statusData) 120 | 121 | when (statusData.status) { 122 | MediaProjectionStatus.OnInitialized -> { 123 | val deviceSize = DeviceUtil.getDeviceSize(this) 124 | startMediaProjection( 125 | surface = windowBinding.surfaceView.holder.surface, 126 | width = deviceSize.width, 127 | height = deviceSize.height 128 | ) 129 | } 130 | MediaProjectionStatus.OnStarted -> { 131 | setPlayed(true) 132 | Toast.makeText(this, R.string.media_projection_started, Toast.LENGTH_SHORT).show() 133 | } 134 | MediaProjectionStatus.OnStop -> { 135 | setPlayed(false) 136 | Toast.makeText(this, R.string.media_projection_stopped, Toast.LENGTH_SHORT).show() 137 | } 138 | MediaProjectionStatus.OnFail -> { 139 | setPlayed(false) 140 | Toast.makeText(this, R.string.media_projection_fail, Toast.LENGTH_SHORT).show() 141 | } 142 | MediaProjectionStatus.OnReject -> { 143 | setPlayed(false) 144 | Toast.makeText(this, R.string.media_projection_reject, Toast.LENGTH_SHORT).show() 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/mediaprojectionexample/ui/service/WindowTouchEvent.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample.ui.service 2 | 3 | import android.view.MotionEvent 4 | import android.view.View 5 | 6 | class WindowTouchEvent( 7 | private val updateViewLayout: (x: Int, y: Int) -> Unit 8 | ) : View.OnTouchListener { 9 | 10 | private var touchPrevX: Float = 0.0f 11 | private var touchPrevY: Float = 0.0f 12 | 13 | override fun onTouch(v: View?, event: MotionEvent?): Boolean { 14 | when (event?.action) { 15 | MotionEvent.ACTION_DOWN -> { 16 | actionDown(event) 17 | } 18 | MotionEvent.ACTION_MOVE -> { 19 | actionMove(event) 20 | } 21 | MotionEvent.ACTION_UP -> { 22 | actionUp(event) 23 | } 24 | else -> { 25 | 26 | } 27 | } 28 | return false 29 | } 30 | 31 | private fun actionDown(event: MotionEvent) { 32 | touchPrevX = event.rawX 33 | touchPrevY = event.rawY 34 | } 35 | 36 | private fun actionMove(event: MotionEvent) { 37 | val rawX = event.rawX 38 | val rawY = event.rawY 39 | 40 | val x = getDistance(rawX, touchPrevX) 41 | val y = getDistance(rawY, touchPrevY) 42 | 43 | touchPrevX = rawX 44 | touchPrevY = rawY 45 | 46 | updateViewLayout(x.toInt(), y.toInt()) 47 | } 48 | 49 | private fun getDistance(raw: Float, touchPrev: Float): Float = 50 | raw - touchPrev 51 | 52 | private fun actionUp(event: MotionEvent) { 53 | // Do nothing. 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/mediaprojectionexample/ui/surface/SurfaceViewHolder.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample.ui.surface 2 | 3 | import android.view.SurfaceHolder 4 | 5 | class SurfaceViewHolder : SurfaceHolder.Callback2 { 6 | 7 | override fun surfaceRedrawNeeded(holder: SurfaceHolder?) { 8 | 9 | } 10 | 11 | override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) { 12 | 13 | } 14 | 15 | override fun surfaceDestroyed(holder: SurfaceHolder?) { 16 | 17 | } 18 | 19 | override fun surfaceCreated(holder: SurfaceHolder?) { 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/mediaprojectionexample/ui/util/DeviceUtil.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.mediaprojectionexample.ui.util 2 | 3 | import android.app.Service 4 | import android.content.Context 5 | import android.util.DisplayMetrics 6 | import android.util.Size 7 | import android.view.WindowManager 8 | 9 | object DeviceUtil { 10 | fun getDeviceSize(context: Context): Size { 11 | val dm = DisplayMetrics() 12 | val display = 13 | (context.getSystemService(Service.WINDOW_SERVICE) as WindowManager).defaultDisplay 14 | display.getMetrics(dm) 15 | 16 | return Size(dm.widthPixels, dm.heightPixels) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_media_projection.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 23 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_media_projection.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/window_video_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 19 | 20 |