WIP: was storing first time but issues while stopping video
This commit is contained in:
parent
9f663db12a
commit
4b2eb8def9
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
|
@ -0,0 +1 @@
|
||||||
|
Wittrail-Android
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<value>
|
||||||
|
<entry key="app">
|
||||||
|
<State />
|
||||||
|
</entry>
|
||||||
|
</value>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.9.0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -48,17 +48,22 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val cameraxVersion = "1.3.1"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("androidx.camera:camera-camera2:$cameraxVersion")
|
||||||
implementation("androidx.core:core-ktx:1.10.1")
|
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
implementation("androidx.camera:camera-video:$cameraxVersion")
|
||||||
implementation("androidx.activity:activity-compose:1.7.0")
|
implementation("androidx.camera:camera-view:$cameraxVersion")
|
||||||
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||||
|
implementation("androidx.activity:activity-compose:1.8.2")
|
||||||
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.material3:material3")
|
implementation("androidx.compose.material3:material3")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-service:2.7.0")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
|
|
@ -2,6 +2,16 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
|
||||||
|
<uses-feature android:name="android.hardware.camera.any" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_CAMERA" tools:ignore="ProtectedPermissions" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_MICROPHONE" tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
@ -12,6 +22,12 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.WittrailAndroid"
|
android:theme="@style/Theme.WittrailAndroid"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<service
|
||||||
|
android:name="RecordingService"
|
||||||
|
android:foregroundServiceType="camera|microphone"
|
||||||
|
android:exported="false"
|
||||||
|
tools:ignore="ForegroundServicePermission">
|
||||||
|
</service>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
@ -1,46 +1,138 @@
|
||||||
package com.urkob.wittrail_android
|
package com.urkob.wittrail_android
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import android.util.Log
|
||||||
import androidx.activity.compose.setContent
|
import android.widget.Button
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import android.widget.Toast
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.material3.Surface
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.material3.Text
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.urkob.wittrail_android.ui.theme.WittrailAndroidTheme
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val REQUEST_CAMERA_PERMISSION = 101
|
||||||
|
|
||||||
|
private val REQUIRED_PERMISSIONS =
|
||||||
|
mutableListOf (
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
).apply {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var useFrontCamera: Boolean = false
|
||||||
|
private lateinit var startFrontCameraButton: Button
|
||||||
|
private lateinit var startBackCameraButton: Button
|
||||||
|
private lateinit var stopCameraButton: Button
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContentView(R.layout.activity_main)
|
||||||
WittrailAndroidTheme {
|
|
||||||
// A surface container using the 'background' color from the theme
|
startFrontCameraButton = findViewById(R.id.startFrontCameraButton)
|
||||||
Surface(
|
startBackCameraButton = findViewById(R.id.startBackCameraButton)
|
||||||
modifier = Modifier.fillMaxSize(),
|
stopCameraButton = findViewById(R.id.stopCameraButton)
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
if (!allPermissionsGranted()) {
|
||||||
Greeting("Android")
|
requestPermissions()
|
||||||
}
|
} else {
|
||||||
}
|
Log.e("main", "INIT START CAMERA")
|
||||||
|
initStartCamera()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
ContextCompat.checkSelfPermission(
|
||||||
Text(
|
baseContext, it) == PackageManager.PERMISSION_GRANTED
|
||||||
text = "Hello $name!",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun GreetingPreview() {
|
|
||||||
WittrailAndroidTheme {
|
|
||||||
Greeting("Android")
|
|
||||||
}
|
}
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun requestPermissions() {
|
||||||
|
activityResultLauncher.launch(REQUIRED_PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private val activityResultLauncher =
|
||||||
|
registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions())
|
||||||
|
{ permissions ->
|
||||||
|
// Handle Permission granted/rejected
|
||||||
|
var permissionGranted = true
|
||||||
|
permissions.entries.forEach {
|
||||||
|
if (it.key in REQUIRED_PERMISSIONS && !it.value)
|
||||||
|
permissionGranted = false
|
||||||
|
}
|
||||||
|
if (!permissionGranted) {
|
||||||
|
Toast.makeText(baseContext,
|
||||||
|
"Permission request denied",
|
||||||
|
Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Log.e("main", "INIT START CAMERA")
|
||||||
|
initStartCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasCameraPermission() =
|
||||||
|
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
private fun requestCameraPermission() {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this, arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun initStartCamera(){
|
||||||
|
startFrontCameraButton.setOnClickListener {
|
||||||
|
useFrontCamera = true
|
||||||
|
if (hasCameraPermission()) {
|
||||||
|
startRecordingService(useFrontCamera)
|
||||||
|
} else {
|
||||||
|
requestCameraPermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startBackCameraButton.setOnClickListener {
|
||||||
|
useFrontCamera = false
|
||||||
|
if (hasCameraPermission()) {
|
||||||
|
startRecordingService(useFrontCamera)
|
||||||
|
} else {
|
||||||
|
requestCameraPermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCameraButton.setOnClickListener {
|
||||||
|
stopRecordingService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun startRecordingService(useFrontCamera: Boolean) {
|
||||||
|
Log.e("main", "startRecordingService")
|
||||||
|
val serviceIntent = Intent(this, RecordingService::class.java).apply {
|
||||||
|
action = RecordingService.ACTION_START_RECORDING
|
||||||
|
putExtra("useFrontCamera", useFrontCamera)
|
||||||
|
}
|
||||||
|
startForegroundService(serviceIntent)
|
||||||
|
// startService(serviceIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopRecordingService() {
|
||||||
|
Log.e("main", "stopRecordingService")
|
||||||
|
val serviceIntent = Intent(this, RecordingService::class.java).apply {
|
||||||
|
action = RecordingService.ACTION_STOP_RECORDING
|
||||||
|
}
|
||||||
|
stopForeground(STOP_FOREGROUND_DETACH)
|
||||||
|
stopService(serviceIntent)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
package com.urkob.wittrail_android
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.video.MediaStoreOutputOptions
|
||||||
|
import androidx.camera.video.Quality
|
||||||
|
import androidx.camera.video.QualitySelector
|
||||||
|
import androidx.camera.video.Recorder
|
||||||
|
import androidx.camera.video.Recording
|
||||||
|
import androidx.camera.video.VideoCapture
|
||||||
|
import androidx.camera.video.VideoRecordEvent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleService
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingService : LifecycleService() {
|
||||||
|
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
||||||
|
private val tag = "RecordingService"
|
||||||
|
private val fileNameFormat = "yyyy-MM-dd-HH-mm-ss-SSS"
|
||||||
|
private var useFrontCamera = false
|
||||||
|
private var videoCapture: VideoCapture<Recorder>? = null
|
||||||
|
private var recording: Recording? = null
|
||||||
|
private lateinit var cameraExecutor: ExecutorService
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
createNotificationChannel()
|
||||||
|
startForeground(NOTIFICATION_ID, createNotification())
|
||||||
|
|
||||||
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
||||||
|
|
||||||
|
Log.e(tag, "onCreate")
|
||||||
|
cameraProviderFuture.addListener({
|
||||||
|
// Initialize videoCapture here
|
||||||
|
val recorder = Recorder.Builder()
|
||||||
|
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
videoCapture = VideoCapture.withOutput(recorder)
|
||||||
|
|
||||||
|
// Now we can start the camera
|
||||||
|
startCamera()
|
||||||
|
}, ContextCompat.getMainExecutor(this))
|
||||||
|
|
||||||
|
Log.e(tag, "END onCreate")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val serviceChannel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Foreground Service Channel",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(serviceChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
notificationIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("Recording Service")
|
||||||
|
.setContentText("Recording is running in the background")
|
||||||
|
.setSmallIcon(R.drawable.ic_launcher_foreground) // Replace with your own drawable resource
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setTicker("Ticker text")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
useFrontCamera = intent?.getBooleanExtra("useFrontCamera", false) ?: false
|
||||||
|
when (intent?.action) {
|
||||||
|
ACTION_START_RECORDING -> {
|
||||||
|
Log.e(tag, ACTION_START_RECORDING)
|
||||||
|
captureVideo()
|
||||||
|
}
|
||||||
|
ACTION_STOP_RECORDING -> {
|
||||||
|
Log.e(tag, ACTION_STOP_RECORDING)
|
||||||
|
|
||||||
|
stopRecording()
|
||||||
|
// return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return START_STICKY
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// override fun onDestroy() {
|
||||||
|
// Log.e(tag, "ON DESTROY IS CALLED SHOUlD STOP CAMERA")
|
||||||
|
// super.onDestroy()
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun startCamera() {
|
||||||
|
Log.e(tag, "startCamera")
|
||||||
|
// Used to bind the lifecycle of cameras to the lifecycle owner
|
||||||
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
||||||
|
val cameraSelector = if (useFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
try {
|
||||||
|
// Unbind use cases before rebinding
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
|
||||||
|
// Bind use cases to camera
|
||||||
|
cameraProvider.bindToLifecycle(this, cameraSelector, videoCapture)
|
||||||
|
|
||||||
|
} catch(exc: Exception) {
|
||||||
|
Log.e(tag, "Use case binding failed", exc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements VideoCapture use case, including start and stop capturing.
|
||||||
|
private fun captureVideo() {
|
||||||
|
Log.e(tag, "START CAPTURE VIDEO")
|
||||||
|
val videoCapture = this.videoCapture ?: return
|
||||||
|
|
||||||
|
val curRecording = recording
|
||||||
|
if (curRecording != null) {
|
||||||
|
Log.e(tag, "curRecording != null")
|
||||||
|
// Stop the current recording session.
|
||||||
|
curRecording.stop()
|
||||||
|
recording = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and start a new recording session
|
||||||
|
val name = SimpleDateFormat(fileNameFormat, Locale.US)
|
||||||
|
.format(System.currentTimeMillis())
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/wittrail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.e(tag, "contentValues")
|
||||||
|
val mediaStoreOutputOptions = MediaStoreOutputOptions
|
||||||
|
.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
|
||||||
|
.setContentValues(contentValues)
|
||||||
|
.build()
|
||||||
|
Log.e(tag, "mediaStoreOutputOptions")
|
||||||
|
recording = videoCapture.output
|
||||||
|
.prepareRecording(this, mediaStoreOutputOptions)
|
||||||
|
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
|
||||||
|
when(recordEvent) {
|
||||||
|
is VideoRecordEvent.Start -> {
|
||||||
|
// TODO: should only display stop button
|
||||||
|
Log.e(tag, "START RECORDING")
|
||||||
|
}
|
||||||
|
is VideoRecordEvent.Finalize -> {
|
||||||
|
Log.e(tag, "ON FINALIZE")
|
||||||
|
if (!recordEvent.hasError()) {
|
||||||
|
Log.d(tag, "Saved URI: " + recordEvent.outputResults.outputUri)
|
||||||
|
val msg = "Video capture succeeded: " +
|
||||||
|
"${recordEvent.outputResults.outputUri}"
|
||||||
|
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
Log.d(tag, msg)
|
||||||
|
} else {
|
||||||
|
Log.e(tag, "Video capture ends with error: " +
|
||||||
|
"${recordEvent.error} $recordEvent")
|
||||||
|
Log.d(tag, "Saved URI: " + recordEvent.outputResults.outputUri)
|
||||||
|
recording?.close()
|
||||||
|
recording = null
|
||||||
|
}
|
||||||
|
Log.e(tag, "RECORDING STOPPED")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
const val ACTION_START_RECORDING = "com.urkob.wittrail_android.action.START_RECORDING"
|
||||||
|
const val ACTION_STOP_RECORDING = "com.urkob.wittrail_android.action.STOP_RECORDING"
|
||||||
|
private const val CHANNEL_ID = "ForegroundServiceChannel"
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
}
|
||||||
|
private fun stopRecording() {
|
||||||
|
Log.e(tag, "stopRecording")
|
||||||
|
|
||||||
|
val curRecording = recording
|
||||||
|
if (curRecording != null) {
|
||||||
|
Log.e(tag, "stopRecording curRecording != null Stop the current recording session")
|
||||||
|
curRecording.stop()
|
||||||
|
recording = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,236 @@
|
||||||
|
package com.urkob.wittrail_android
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.video.MediaStoreOutputOptions
|
||||||
|
import androidx.camera.video.Recorder
|
||||||
|
import androidx.camera.video.Recording
|
||||||
|
import androidx.camera.video.VideoCapture
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingServices : Service() {
|
||||||
|
private lateinit var cameraExecutor: ExecutorService
|
||||||
|
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
||||||
|
private var recording: Recording? = null
|
||||||
|
private lateinit var videoCapture: VideoCapture<Recorder>
|
||||||
|
|
||||||
|
private val notificationChannelId = "RECORDING_CHANNEL"
|
||||||
|
private val notificationId = 1
|
||||||
|
private var videoFile: File? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindCamera() {
|
||||||
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
|
// You need to build the use cases. For example:
|
||||||
|
val previewUseCase = Preview.Builder().build()
|
||||||
|
val videoCaptureUseCase = VideoCapture.withOutput(Recorder.Builder().build())
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase, videoCaptureUseCase)
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
Log.e(TAG, "Use case binding failed", exc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startRecording() {
|
||||||
|
val mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val outputOptions = VideoCapture.OutputFileOptions.Builder(videoFile).build()
|
||||||
|
// Start recording
|
||||||
|
videoCapture.startRecording(outputOptions, cameraExecutor, object : VideoCapture.OnVideoSavedCallback {
|
||||||
|
fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
|
||||||
|
val savedUri = outputFileResults.savedUri ?: Uri.fromFile(videoFile)
|
||||||
|
Log.d(TAG, "Video File Saved at $savedUri")
|
||||||
|
// Here you can handle the saved video file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
||||||
|
Log.e(TAG, "Video capture error: $message", cause)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val captureListener = object : VideoCapture.OnVideoSavedCallback {
|
||||||
|
fun onVideoSaved(file: File) {
|
||||||
|
Log.d(TAG, "Video File: $file")
|
||||||
|
// Here you can handle the saved video file
|
||||||
|
}
|
||||||
|
fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
||||||
|
Log.e(TAG, "Video capture error: $message", cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
cameraExecutor.shutdown()
|
||||||
|
recording?.stop() // Stop the recording
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a file
|
||||||
|
private fun createFile(baseFolder: File, format: String, extension: String) =
|
||||||
|
File(baseFolder, SimpleDateFormat(format, Locale.US)
|
||||||
|
.format(System.currentTimeMillis()) + extension)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RecordingService"
|
||||||
|
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
|
||||||
|
private const val VIDEO_EXTENSION = ".mp4"
|
||||||
|
private val outputDirectory: File by lazy {
|
||||||
|
getOutputDirectory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get the output directory
|
||||||
|
private fun getOutputDirectory(): File {
|
||||||
|
val mediaDir = externalMediaDirs.firstOrNull()?.let {
|
||||||
|
File(it, resources.getString(R.string.app_name)).apply { mkdirs() }
|
||||||
|
}
|
||||||
|
return if (mediaDir != null && mediaDir.exists())
|
||||||
|
mediaDir else filesDir
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
createNotificationChannel()
|
||||||
|
startForeground(notificationId, createNotification())
|
||||||
|
|
||||||
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
|
val previewUseCase = Preview.Builder().build()
|
||||||
|
val videoCaptureUseCase = VideoCapture.withOutput(Recorder.Builder().build())
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase, videoCaptureUseCase)
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
Log.e(TAG, "Use case binding failed", exc)
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
||||||
|
cameraProviderFuture.addListener(Runnable {
|
||||||
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
bindCamera()
|
||||||
|
}, ContextCompat.getMainExecutor(this))
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCamera() {
|
||||||
|
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
val preview = Preview.Builder()
|
||||||
|
.build()
|
||||||
|
.also {
|
||||||
|
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
|
val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
this, cameraSelector, preview)
|
||||||
|
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
this, cameraSelector, preview, imageCapture, videoCapture)
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
Log.e(TAG, "Use case binding failed", exc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun stopRecording() {
|
||||||
|
try {
|
||||||
|
Log.d("RecordingService", "Recording stopped. File saved at: ${videoFile?.absolutePath}")
|
||||||
|
// Now scan the file
|
||||||
|
videoFile?.let { file ->
|
||||||
|
Log.d("RecordingService", "Recording stopped. File saved at: ${file.absolutePath}")
|
||||||
|
scanFile(file) // Use 'let' to ensure videoFile is not null when calling scanFile
|
||||||
|
}
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(this, "Recording stop failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun scanFile(file: File) {
|
||||||
|
MediaScannerConnection.scanFile(
|
||||||
|
this@`RecordingService.txt`,
|
||||||
|
arrayOf(file.absolutePath),
|
||||||
|
null
|
||||||
|
) { path: String?, uri: Uri? ->
|
||||||
|
Log.d("RecordingService", "Scanned $path:")
|
||||||
|
Log.d("RecordingService", "-> Uri = $uri")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||||
|
// val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, notificationIntent,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return NotificationCompat.Builder(this, notificationChannelId)
|
||||||
|
.setContentTitle("Recording Service")
|
||||||
|
.setContentText("Recording in progress...")
|
||||||
|
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val serviceChannel = NotificationChannel(
|
||||||
|
notificationChannelId,
|
||||||
|
"Foreground Service Channel",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(serviceChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/startFrontCameraButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Start Front Camera" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/startBackCameraButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Start Back Camera"
|
||||||
|
android:layout_marginTop="16dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/stopCameraButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Stop Recording"
|
||||||
|
android:layout_marginTop="16dp" />
|
||||||
|
</LinearLayout>
|
|
@ -1,5 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.WittrailAndroid" parent="android:Theme.Material.Light.NoActionBar" />
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.WittrailAndroid" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue