Comments added, code cleared

This commit is contained in:
2021-05-11 16:44:05 +03:00
parent e5c0287f51
commit 80391efc44
14 changed files with 239 additions and 234 deletions

View File

@@ -4,31 +4,36 @@ import android.content.Context
import android.graphics.*
import android.text.TextPaint
import android.util.AttributeSet
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
/**
* Класс наследован от SurfaceView
* В нем отрисовывается проход фразы Hello World за 20 кадров
* в превью можно переключать кадры, меняя _frameNumber от 0 до 19
* В режиме редактирования для удобной отладки строка заполняется красным фоном
*
*/
class HelloSurface : SurfaceView, SurfaceHolder.Callback {
private var _helloWorldString: String = ""
private var _textSize: Float = 100f
private var _frameNumber: Int = 7
private var _helloWorldString: String = "" //Строка для отображения
private var _frameNumber: Int = 7 // Инициализация номера фрейма, видно на превью
private var _frameRate: Int = 30 //Частота кадров
private lateinit var textPaint: TextPaint
private lateinit var backgroundPaint: Paint
private lateinit var backgroundPaint: Paint //Фон надписи в режиме редактирвоания
private var textWidth: Float = 0f
private var textHeight: Float = 0f
private var textPositionVertical = 0f
private var frameDelta = 0f
private var mainJob: Job? = null
private var persistentSurface: Surface? = null
private var persistentSurface: Surface? = null //Surface для рендера и сохранения видео
/**
* The text to draw
* Текст для отрисовки
*/
var helloWorldString: String
get() = _helloWorldString
@@ -38,22 +43,24 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
}
/**
* font Size
* Частота кадров
*/
var textSize: Float
get() = _textSize
var frameRate: Int
get() = _frameRate
set(value) {
_textSize = value
invalidateTextPaintAndMeasurements()
_frameRate = value
}
var frameNumber: Int
/**
* Номер фрейма, используется только в режиме редактирования для превью
*/
var frameNumber: Int
get() = _frameNumber
set(value) {
_frameNumber = value % 20
invalidateTextPaintAndMeasurements()
}
init {
holder.addCallback(this)
}
@@ -75,20 +82,14 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
}
private fun attrsInit(attrs: AttributeSet?, defStyle: Int) {
// Load attributes
val a = context.obtainStyledAttributes(
attrs, R.styleable.TestView, defStyle, 0
)
_helloWorldString = a.getString(R.styleable.TestView_helloString) ?: "Test String"
_textSize = a.getDimension(
R.styleable.TestView_textSize,
textSize
)
_frameNumber = a.getInteger(
R.styleable.TestView_frameNumber,
frameNumber
)
_frameRate = a.getInteger(R.styleable.TestView_frameRate, 30)
_frameNumber = a.getInteger(R.styleable.TestView_frameNumber, frameNumber)
a.recycle()
textPaint = TextPaint().apply {
@@ -103,17 +104,20 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
invalidateTextPaintAndMeasurements()
}
fun setExternalSurface(surface: Surface) {
persistentSurface = surface
}
/**
* Перегруженный onDraw остался только для превью
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (isInEditMode)
canvas?.let {
drawHello(canvas, _frameNumber)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
@@ -121,38 +125,46 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
}
private fun invalidateTextPaintAndMeasurements() {
val textBounds = Rect()
textPaint.let {
it.textSize = textSize
}
textPaint.getTextBounds(helloWorldString, 0, helloWorldString.length, textBounds)
textPaint.getTextBounds(_helloWorldString, 0, _helloWorldString.length, textBounds)
textHeight = textBounds.height()+0f
textWidth = textBounds.width()+0f
}
/**
* Выделение красным фоном надписи в превью
*/
private fun drawBounds(canvas: Canvas, sx: Int, sy: Int) {
val textBounds = Rect()
textPaint.getTextBounds(helloWorldString, 0, helloWorldString.length, textBounds)
textBounds.right = textPaint.measureText(helloWorldString).toInt()
textPaint.getTextBounds(_helloWorldString, 0, _helloWorldString.length, textBounds)
textBounds.right = textPaint.measureText(_helloWorldString).toInt()
textBounds.offsetTo(sx, sy-textHeight.toInt()+textPaint.descent().toInt())
canvas.drawRect(textBounds, backgroundPaint)
}
/**
* todo comment
*/
fun drawHello(canvas: Canvas, frameNumber: Int) {
/**
* Основной метод прорисовки надписи
* Так как этот метод используется для различных размеров Canvas,
* я решил сделать размер шрифта равным 1/20 от высоты Canvas
*
*/
private fun drawHello(canvas: Canvas, frameN: Int) {
val textSize = canvas.height/20f
textPaint.textSize = textSize
textWidth = textPaint.measureText(helloWorldString)
textWidth = textPaint.measureText(_helloWorldString)
textPositionVertical = canvas.height/2f+textSize/2f
frameDelta = (canvas.width+textWidth)/20f
val textPositionVertical = canvas.height/2f+textSize/2f
val frameDelta = (canvas.width+textWidth)/20f
var xPos = frameNumber*frameDelta-textWidth
val xPos = frameN*frameDelta-textWidth
if (isInEditMode) drawBounds(canvas, xPos.toInt(), textPositionVertical.toInt())
if (isInEditMode) {
invalidateTextPaintAndMeasurements()
drawBounds(canvas, xPos.toInt(), textPositionVertical.toInt())
}
canvas.drawText( helloWorldString, xPos, textPositionVertical, textPaint)
canvas.drawText( _helloWorldString, xPos, textPositionVertical, textPaint)
}
private fun clearCanvas(canvas: Canvas) {
@@ -165,20 +177,31 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
renderLoop()
}
}
@Volatile private var maxFramesRender = 120
@Volatile private var framesRendered = -1
/**
* Метод, запускающий отсчет фреймов для записи
*/
fun startRecording(maxFrames: Int) {
maxFramesRender = maxFrames
framesRendered++
}
var onLastFrame : () -> Unit = @Synchronized {}
/**
* Метод, который будет вызван из корутины после рендера последнего фрейма для записи
* Он будет переназначен во время инициализации енкодера
*/
var onLastFrame : () -> Unit = {}
fun setOnLastFrameRecordedListener(listener: () -> Unit) {
onLastFrame = listener
}
/**
* Отрисовка превью и здесь же рендер фреймов
*/
private suspend fun renderLoop() {
while(GlobalScope.isActive) {
val timeInMillis = measureTimeMillis {
@@ -193,13 +216,15 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
preview()
_frameNumber++
}
if (_frameNumber >= 20) _frameNumber = 0
delay(33-timeInMillis)
}
//Расчет паузы перед отрисовкой следующего фрейма
delay(1000/_frameRate-timeInMillis)
}
}
fun render() {
//Рендер фрейма для записи видео
private fun render() {
persistentSurface?.let {surface ->
val pCanvas = surface.lockCanvas(null)
pCanvas?.let {
@@ -211,8 +236,8 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
}
}
}
fun preview() {
//Такой же рендер, но видимый на View и работающий постоянно
private fun preview() {
val canvas = holder.lockCanvas()
canvas?.let {
clearCanvas(it)
@@ -224,7 +249,7 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
//Остановка корутины при уничтожении surface
override fun surfaceDestroyed(holder: SurfaceHolder) {
runBlocking {
mainJob?.cancelAndJoin()

View File

@@ -1,141 +0,0 @@
package su.rst10h.loopedworld
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.util.AttributeSet
import android.util.Log
import android.view.SurfaceView
import android.view.View
/**
* TODO: document your custom view class.
*/
class TestView : View {
private var _helloWorldString: String = ""
private var _textSize: Float = 100f // TODO: use a default from R.dimen...
private var _frameNumber: Int = 12
private lateinit var textPaint: TextPaint
private var textWidth: Float = 0f
private var textHeight: Float = 0f
private var textPositionVertical = 0f
private var frameDelta = 0f
/**
* The text to draw
*/
var helloWorldString: String
get() = _helloWorldString
set(value) {
_helloWorldString = value
invalidateTextPaintAndMeasurements()
}
/**
* font Size
*/
var textSize: Float
get() = _textSize
set(value) {
_textSize = value
invalidateTextPaintAndMeasurements()
}
var frameNumber: Int
get() = _frameNumber
set(value) {
_frameNumber = value
invalidateTextPaintAndMeasurements()
}
constructor(context: Context) : super(context) {
init(null, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(attrs, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
) {
init(attrs, defStyle)
}
private fun init(attrs: AttributeSet?, defStyle: Int) {
// Load attributes
val a = context.obtainStyledAttributes(
attrs, R.styleable.TestView, defStyle, 0
)
_helloWorldString = a.getString(R.styleable.TestView_helloString) ?: "Test String Hello"
_textSize = a.getDimension(
R.styleable.TestView_textSize,
textSize
)
_frameNumber = a.getInteger(
R.styleable.TestView_frameNumber,
frameNumber
)
a.recycle()
textPaint = TextPaint().apply {
flags = Paint.ANTI_ALIAS_FLAG
textAlign = Paint.Align.LEFT
typeface = Typeface.DEFAULT_BOLD
}
invalidateTextPaintAndMeasurements()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
invalidateTextPaintAndMeasurements()
}
private fun invalidateTextPaintAndMeasurements() {
textPaint.let {
it.textSize = textSize
textWidth = it.measureText(helloWorldString)
textHeight = it.fontMetrics.bottom
}
invalidate()
}
/**
* todo commen
*/
private fun drawHelloWorld(canvas: Canvas, frameNumber: Int) {
textPositionVertical = canvas.height/2f+textHeight
frameDelta = (width+textWidth)/20f
var xPos = frameNumber*frameDelta-textWidth
// if (isInEditMode) {
// xPos = width/2f-textWidth/2f
// }
helloWorldString.let {
canvas.drawText( it, xPos, textPositionVertical, textPaint)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// TODO: consider storing these as member variables to reduce
val paddingLeft = paddingLeft
val paddingTop = paddingTop
val paddingRight = paddingRight
val paddingBottom = paddingBottom
val contentWidth = width - paddingLeft - paddingRight
val contentHeight = height - paddingTop - paddingBottom
drawHelloWorld(canvas, _frameNumber)
}
}

View File

@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<su.rst10h.loopedworld.TestView
<su.rst10h.loopedworld.HelloSurface
style="@style/Widget.Theme.Inspiry.MyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -11,6 +11,7 @@
android:paddingBottom="40dp"
app:frameNumber="8"
app:helloString="Hello World"
app:textSize="55sp" />
app:frameRate = "30"
/>
</FrameLayout>

View File

@@ -1,7 +1,7 @@
<resources>
<declare-styleable name="TestView">
<attr name="helloString" format="string" />
<attr name="textSize" format="dimension" />
<attr name="frameRate" format="integer" />
<attr name="frameNumber" format="integer" />
</declare-styleable>
</resources>

View File

@@ -1,13 +1,11 @@
package su.rst10h.inspiry
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*

View File

@@ -12,12 +12,23 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Inspiry">
<activity android:name=".ui.MainActivity">
<activity android:name=".ui.MainActivity"
android:configChanges="orientation"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="su.rst10h.inspiry.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>

View File

@@ -1,4 +1,11 @@
package su.rst10h.inspiry
import android.media.MediaFormat
/**
* Константы, доступные в любой части приложения.
*/
const val MEDIA_FOLDER = "test"
const val MEDIA_FILE_NAME = "TestVideo.mp4"
const val MEDIA_CODEC = MediaFormat.MIMETYPE_VIDEO_AVC
const val MEDIA_FRAME_RATE = 30

View File

@@ -5,6 +5,11 @@ import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
/**
* Проверка разрешений.
* Оставил на всякий случай
*/
class PermissionsChecker {
companion object {
private var REQUEST_EXTERNAL_STORAGE = 1

View File

@@ -4,12 +4,13 @@ import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.MediaMuxer
import android.util.Log
import android.view.Surface
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import su.rst10h.inspiry.MEDIA_CODEC
import su.rst10h.inspiry.MEDIA_FILE_NAME
import su.rst10h.inspiry.MEDIA_FOLDER
import su.rst10h.inspiry.MEDIA_FRAME_RATE
import su.rst10h.loopedworld.HelloSurface
import java.io.File
@@ -17,29 +18,43 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
private val bufferInfo = MediaCodec.BufferInfo()
private val mediaEncoder: MediaCodec
private lateinit var inputSurface: Surface
private val inputSurface: Surface
/**
* onStopRecord нужен, чтобы ViewModel узнала о завершении записи
*/
var onStopRecord = {}
/**
* Инициализация энкодера
* Кодек и частота берется из констант
*/
init {
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1080, 1920)
val format = MediaFormat.createVideoFormat(MEDIA_CODEC, 1080, 1920)
format.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
)
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
mediaEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
format.setInteger(MediaFormat.KEY_FRAME_RATE, MEDIA_FRAME_RATE)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
mediaEncoder = MediaCodec.createEncoderByType(MEDIA_CODEC)
mediaEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
inputSurface = mediaEncoder.createInputSurface()
}
/**
* Запись на диск осуществляем с помощью MediaMuxer
*/
private lateinit var mediaMuxer: MediaMuxer
private var muxerTrackIndex = -1
@Volatile var isRunning = false
/**
* Запуск рендера
*/
fun startRender() {
muxerTrackIndex = -1
@@ -53,12 +68,17 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
encode()
}
/**
* Это нужно для остановки энкодера после записи 120 фреймов
*/
helloSurface.setOnLastFrameRecordedListener {
Log.d("Encoder", "last frame")
isRunning = false
onStopRecord()
}
/**
* Запуск отрисовки 120 фреймов в inputSurface энкодера
*/
helloSurface.setExternalSurface(inputSurface)
helloSurface.startRecording(120)
}
@@ -69,37 +89,41 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
when(encoderStatus) {
MediaCodec.INFO_TRY_AGAIN_LATER -> { }
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
Log.d("encoder", "start muxer $muxerTrackIndex")
val newFormat: MediaFormat = mediaEncoder.outputFormat
muxerTrackIndex = mediaMuxer.addTrack(newFormat)
mediaMuxer.start()
}
-3 -> {
Log.d("encoder", "bad status $encoderStatus")
}
else -> {
/**
* если статус >=0, значит что то есть в буфере
*/
val data = mediaEncoder.getOutputBuffer(encoderStatus)
data?.let {
data.position(bufferInfo.offset)
data.limit(bufferInfo.offset + bufferInfo.size)
it.position(bufferInfo.offset)
it.limit(bufferInfo.offset + bufferInfo.size)
mediaMuxer.writeSampleData(muxerTrackIndex, data, bufferInfo)
}
mediaEncoder.releaseOutputBuffer(encoderStatus, false)
}
}
}
Log.d("encoder","stop encoding")
mediaMuxer.stop()
mediaMuxer.release()
mediaEncoder.stop()
}
private fun prepareFilePath(appPath: File): String {
val fullPath = appPath.absolutePath+"/$MEDIA_FOLDER/"
/**
* Метод возвращает полный путь с именем файла для записи видео
* Здесь создается каталог test, если не существует
*/
private fun prepareFilePath(path: File): String {
val fullPath = path.absolutePath+"/$MEDIA_FOLDER/"
if (!File(fullPath).exists()) {
if (!File(fullPath).mkdir()) {
Log.d("main", "directory test not created")
throw Exception("The test directory does not exist and has not been created:\n$fullPath$\n")
}
}

View File

@@ -1,32 +1,51 @@
package su.rst10h.inspiry.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Button
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import su.rst10h.inspiry.MEDIA_FILE_NAME
import su.rst10h.inspiry.MEDIA_FOLDER
import su.rst10h.inspiry.R
import androidx.core.content.FileProvider
import su.rst10h.inspiry.*
import su.rst10h.inspiry.data.ActivityState
import su.rst10h.loopedworld.HelloSurface
import java.io.File
class MainActivity : AppCompatActivity() {
private val viewModel : MainViewModel by viewModels()
private lateinit var helloSurface: HelloSurface
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Это нужно на некоторых версиях MIUI для принудительного отключения ночной темы:
//На обычных Android устройствах можно обойтись без нее
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
setContentView(R.layout.activity_main)
val viewModel : MainViewModel by viewModels()
/**
* Так как мы пишем в папку приложения, разрешения можно было не запрашивать
* но я не помню, прокатит ли это на Android 7 и ниже, просто на всякий случай:
*/
PermissionsChecker.verifyStoragePermissions(this)
/**
* Binding не использовал, он тут роли не сыграет.
*/
val btn = findViewById<Button>(R.id.button)
val surfaceView = findViewById<HelloSurface>(R.id.testView)
helloSurface = findViewById<HelloSurface>(R.id.testView)
helloSurface.frameRate = MEDIA_FRAME_RATE //Частоту обновления берем из констант
/**
* Наблюдатель за состоянием и действия при изменении состояния:
*/
viewModel.state.observe(this, { state ->
when (state) {
ActivityState.WAIT -> {
@@ -43,25 +62,61 @@ class MainActivity : AppCompatActivity() {
}
ActivityState.OPEN_MEDIA -> {
openMedia()
viewModel.reset_state()
viewModel.resetState()
}
}
})
getExternalFilesDir(null)?.let {
viewModel.initRender(surfaceView, it)
/**
* Инициализация энкодера через ViewModel
* Конечно, не лучшее решение передавать View в Repository
* Но там он нужен для решения текущей задачи
* Тем более HelloSurface специально создан для этих целей
*
*/
getBasePath()?.let {
viewModel.initRender(helloSurface, it)
}
/**
* Одна кнопка на все действия
*/
btn.setOnClickListener {
viewModel.action()
}
}
/**
* Метод открывает заранее созданный файл
* Можно было сделать чуть проще, но на устройствах с Android 11 тогда могут быть проблемы
* В гугле такие затейники!
*/
private fun openMedia() {
val f = File(getBasePath(), "/$MEDIA_FOLDER/$MEDIA_FILE_NAME")
val fileUri = FileProvider.getUriForFile( this, "${application.packageName}.fileprovider", f)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(
getExternalFilesDir(null)?.absolutePath+
"/$MEDIA_FOLDER/$MEDIA_FILE_NAME"),
"video/mp4")
intent.setDataAndType(fileUri, "video/*")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
/**
* Повторная инициализация енкодера
*/
override fun onStart() {
super.onStart()
getBasePath()?.let {
viewModel.initRender(helloSurface, it)
}
}
/**
* @return app folder
*/
private fun getBasePath(): File? {
return this.getExternalFilesDir(null)
}
}

View File

@@ -8,7 +8,13 @@ import su.rst10h.inspiry.repository.VideoEncoder
import su.rst10h.loopedworld.HelloSurface
import java.io.File
/**
* View Model для главной и единственной Activity
*/
class MainViewModel: ViewModel() {
/**
* MutableLiveData содержит состояние приложения
*/
val state by lazy {
MutableLiveData<ActivityState>().apply {
this.value = WAIT
@@ -17,14 +23,23 @@ class MainViewModel: ViewModel() {
private lateinit var videoEncoder: VideoEncoder
/**
* Инициализация энкодера
*/
fun initRender(surface: HelloSurface, appPath: File) {
videoEncoder = VideoEncoder(appPath, surface)
}
fun reset_state() {
/**
* Сброс состояния
*/
fun resetState() {
state.postValue(WAIT)
}
/**
* Действия при нажатии кнопки и смена состояний
*/
fun action() {
when (state.value) {
WAIT -> {
@@ -37,8 +52,6 @@ class MainViewModel: ViewModel() {
RENDERED -> {
state.postValue(OPEN_MEDIA)
}
}
}

View File

@@ -16,6 +16,7 @@
android:layout_marginBottom="100dp"
android:elevation="8dp"
app:frameNumber="0"
app:frameRate="30"
app:helloString="Hello World"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -23,7 +24,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
app:textSize="40sp" />
/>
<Button
android:id="@+id/button"

View File

@@ -1,6 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Inspiry" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path path="test/" name="files" />
<external-path path="." name="external"/>
</paths>