Merge pull request #59 from Aga-C/add-recording-widget

Added widget for quick recording starting (#22)
This commit is contained in:
Tibor Kaputa 2021-10-18 19:37:59 +02:00 committed by GitHub
commit 76d785be89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 356 additions and 8 deletions

View File

@ -28,6 +28,31 @@
android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
<activity
android:name=".activities.WidgetRecordDisplayConfigureActivity"
android:screenOrientation="portrait"
android:theme="@style/MyWidgetConfigTheme">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".helpers.MyWidgetRecordDisplayProvider"
android:icon="@drawable/ic_microphone_vector">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_record_display" />
</receiver>
<activity
android:name=".activities.BackgroundRecordActivity"
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".activities.SplashActivity"
android:theme="@style/SplashTheme" />

View File

@ -0,0 +1,28 @@
package com.simplemobiletools.voicerecorder.activities
import android.content.Intent
import com.simplemobiletools.voicerecorder.services.RecorderService
class BackgroundRecordActivity : SimpleActivity() {
companion object {
const val RECORD_INTENT_ACTION = "RECORD_ACTION"
}
override fun onResume() {
super.onResume()
if (intent.action == RECORD_INTENT_ACTION) {
Intent(this@BackgroundRecordActivity, RecorderService::class.java).apply {
try {
if (RecorderService.isRunning) {
stopService(this)
} else {
startService(this)
}
} catch (ignored: Exception) {
}
}
}
moveTaskToBack(true)
finish()
}
}

View File

@ -0,0 +1,101 @@
package com.simplemobiletools.voicerecorder.activities
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.widget.SeekBar
import com.simplemobiletools.commons.dialogs.ColorPickerDialog
import com.simplemobiletools.commons.extensions.adjustAlpha
import com.simplemobiletools.commons.extensions.applyColorFilter
import com.simplemobiletools.commons.extensions.setFillWithStroke
import com.simplemobiletools.commons.helpers.DEFAULT_WIDGET_BG_COLOR
import com.simplemobiletools.commons.helpers.IS_CUSTOMIZING_COLORS
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider
import kotlinx.android.synthetic.main.widget_record_display_config.*
class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
private var mWidgetAlpha = 0f
private var mWidgetId = 0
private var mWidgetColor = 0
private var mWidgetColorWithoutTransparency = 0
public override fun onCreate(savedInstanceState: Bundle?) {
useDynamicTheme = false
super.onCreate(savedInstanceState)
setResult(Activity.RESULT_CANCELED)
setContentView(R.layout.widget_record_display_config)
initVariables()
val isCustomizingColors = intent.extras?.getBoolean(IS_CUSTOMIZING_COLORS) ?: false
mWidgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID
if (mWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !isCustomizingColors) {
finish()
}
config_save.setOnClickListener { saveConfig() }
config_widget_color.setOnClickListener { pickBackgroundColor() }
}
private fun initVariables() {
mWidgetColor = resources.getColor(R.color.color_primary)
mWidgetAlpha = if (mWidgetColor == DEFAULT_WIDGET_BG_COLOR) {
1f
} else {
Color.alpha(mWidgetColor) / 255.toFloat()
}
mWidgetColorWithoutTransparency = Color.rgb(Color.red(mWidgetColor), Color.green(mWidgetColor), Color.blue(mWidgetColor))
config_widget_seekbar.setOnSeekBarChangeListener(seekbarChangeListener)
config_widget_seekbar.progress = (mWidgetAlpha * 100).toInt()
updateColors()
}
private fun saveConfig() {
config.widgetBgColor = mWidgetColor
requestWidgetUpdate()
Intent().apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)
setResult(Activity.RESULT_OK, this)
}
finish()
}
private fun pickBackgroundColor() {
ColorPickerDialog(this, mWidgetColorWithoutTransparency) { wasPositivePressed, color ->
if (wasPositivePressed) {
mWidgetColorWithoutTransparency = color
updateColors()
}
}
}
private fun requestWidgetUpdate() {
Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, this, MyWidgetRecordDisplayProvider::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(mWidgetId))
sendBroadcast(this)
}
}
private fun updateColors() {
mWidgetColor = mWidgetColorWithoutTransparency.adjustAlpha(mWidgetAlpha)
config_widget_color.setFillWithStroke(mWidgetColor, Color.BLACK)
config_image.background.mutate().applyColorFilter(mWidgetColor)
}
private val seekbarChangeListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
mWidgetAlpha = progress.toFloat() / 100.toFloat()
updateColors()
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
}
}

View File

@ -1,6 +1,36 @@
package com.simplemobiletools.voicerecorder.extensions
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import com.simplemobiletools.voicerecorder.helpers.Config
import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider
import com.simplemobiletools.voicerecorder.helpers.TOGGLE_WIDGET_UI
val Context.config: Config get() = Config.newInstance(applicationContext)
fun Context.drawableToBitmap(drawable: Drawable): Bitmap {
val size = (60 * resources.displayMetrics.density).toInt()
val mutableBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(mutableBitmap)
drawable.setBounds(0, 0, size, size)
drawable.draw(canvas)
return mutableBitmap
}
fun Context.updateWidgets(isRecording: Boolean) {
val widgetIDs = AppWidgetManager.getInstance(applicationContext)
?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetRecordDisplayProvider::class.java)) ?: return
if (widgetIDs.isNotEmpty()) {
Intent(applicationContext, MyWidgetRecordDisplayProvider::class.java).apply {
action = TOGGLE_WIDGET_UI
putExtra(IS_RECORDING, isRecording)
sendBroadcast(this)
}
}
}

View File

@ -26,6 +26,8 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
override fun onResume() {
setupColors()
if (!RecorderService.isRunning) status = RECORDING_STOPPED
refreshView()
}
override fun onDestroy() {
@ -127,14 +129,7 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun gotDurationEvent(event: Events.RecordingDuration) {
updateRecordingDuration(event.duration)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun gotStatusEvent(event: Events.RecordingStatus) {
status = event.status
private fun refreshView() {
toggle_recording_button.setImageDrawable(getToggleButtonIcon())
toggle_pause_button.beVisibleIf(status != RECORDING_STOPPED && isNougatPlus())
if (status == RECORDING_PAUSED) {
@ -149,6 +144,17 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun gotDurationEvent(event: Events.RecordingDuration) {
updateRecordingDuration(event.duration)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun gotStatusEvent(event: Events.RecordingStatus) {
status = event.status
refreshView()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun gotAmplitudeEvent(event: Events.RecordingAmplitude) {
val amplitude = event.amplitude

View File

@ -25,6 +25,9 @@ const val RECORDING_RUNNING = 0
const val RECORDING_STOPPED = 1
const val RECORDING_PAUSED = 2
const val IS_RECORDING = "is_recording"
const val TOGGLE_WIDGET_UI = "toggle_widget_ui"
// shared preferences
const val HIDE_NOTIFICATION = "hide_notification"
const val SAVE_RECORDINGS = "save_recordings"

View File

@ -0,0 +1,62 @@
package com.simplemobiletools.voicerecorder.helpers
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.widget.RemoteViews
import com.simplemobiletools.commons.extensions.getColoredDrawableWithColor
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.BackgroundRecordActivity
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.drawableToBitmap
class MyWidgetRecordDisplayProvider : AppWidgetProvider() {
private val OPEN_APP_INTENT_ID = 1
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
changeWidgetIcon(appWidgetManager, context, Color.WHITE)
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == TOGGLE_WIDGET_UI && intent.extras?.containsKey(IS_RECORDING) == true) {
val appWidgetManager = AppWidgetManager.getInstance(context) ?: return
val color = if (intent.extras!!.getBoolean(IS_RECORDING)) context.config.widgetBgColor else Color.WHITE
changeWidgetIcon(appWidgetManager, context, color)
} else {
super.onReceive(context, intent)
}
}
private fun changeWidgetIcon(appWidgetManager: AppWidgetManager, context: Context, color: Int) {
val alpha = Color.alpha(context.config.widgetBgColor)
val bmp = getColoredIcon(context, color, alpha)
appWidgetManager.getAppWidgetIds(getComponentName(context)).forEach {
RemoteViews(context.packageName, R.layout.widget_record_display).apply {
setupAppOpenIntent(context, this)
setImageViewBitmap(R.id.record_display_btn, bmp)
appWidgetManager.updateAppWidget(it, this)
}
}
}
private fun getComponentName(context: Context) = ComponentName(context, MyWidgetRecordDisplayProvider::class.java)
private fun setupAppOpenIntent(context: Context, views: RemoteViews) {
Intent(context, BackgroundRecordActivity::class.java).apply {
action = BackgroundRecordActivity.RECORD_INTENT_ACTION
val pendingIntent = PendingIntent.getActivity(context, OPEN_APP_INTENT_ID, this, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.record_display_btn, pendingIntent)
}
}
private fun getColoredIcon(context: Context, color: Int, alpha: Int): Bitmap {
val drawable = context.resources.getColoredDrawableWithColor(R.drawable.ic_microphone_vector, color, alpha)
return context.drawableToBitmap(drawable)
}
}

View File

@ -22,6 +22,7 @@ import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SplashActivity
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.updateWidgets
import com.simplemobiletools.voicerecorder.helpers.*
import com.simplemobiletools.voicerecorder.models.Events
import org.greenrobot.eventbus.EventBus
@ -29,6 +30,10 @@ import java.io.File
import java.util.*
class RecorderService : Service() {
companion object {
var isRunning = false
}
private val AMPLITUDE_UPDATE_MS = 75L
private var currFilePath = ""
@ -56,10 +61,18 @@ class RecorderService : Service() {
override fun onDestroy() {
super.onDestroy()
stopRecording()
isRunning = false
updateWidgets(false)
}
// mp4 output format with aac encoding should produce good enough m4a files according to https://stackoverflow.com/a/33054794/1967672
private fun startRecording() {
isRunning = true
updateWidgets(true)
if (status == RECORDING_RUNNING) {
return
}
val baseFolder = if (isQPlus()) {
cacheDir
} else {

View File

@ -0,0 +1,4 @@
<vector android:height="48dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="m12,14.0197c1.6912,0 3.0463,-1.3652 3.0463,-3.0565l0.0102,-6.1129c-0,-1.6912 -1.3652,-3.0565 -3.0565,-3.0565 -1.6912,0 -3.0565,1.3652 -3.0565,3.0565l0,6.1129c0,1.6912 1.3652,3.0565 3.0565,3.0565zM17.3998,10.9632c0,3.0565 -2.5878,5.196 -5.3998,5.196 -2.812,0 -5.4058,-2.0073 -5.4058,-5.0638 0,0 -0.0004,-1.0378 -0.844,-1.0329 -0.8435,0.0049 -0.882,0.9007 -0.882,0.9007 0,3.4742 2.7712,6.3473 6.1129,6.8465l0,3.2728c0,0 -0.1072,1.1107 1.0248,1.1236 1.111,0.0127 1.0128,-1.1236 1.0128,-1.1236l0,-3.2728c3.3417,-0.489 6.1129,-3.3621 6.1129,-6.8465 0,0 0.004,-0.9064 -0.8345,-0.8928 -0.8564,0.0139 -0.8975,0.8928 -0.8975,0.8928z"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/record_display_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp" />

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_margin="@dimen/activity_margin"
android:paddingBottom="@dimen/activity_margin">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/config_widget_color"
android:layout_marginBottom="@dimen/activity_margin"
android:gravity="center">
<ImageView
android:id="@+id/config_image"
android:layout_width="@dimen/main_button_size"
android:layout_height="@dimen/main_button_size"
android:background="@drawable/ic_microphone_vector" />
</RelativeLayout>
<ImageView
android:id="@+id/config_widget_color"
android:layout_width="@dimen/widget_colorpicker_size"
android:layout_height="@dimen/widget_colorpicker_size"
android:layout_above="@+id/config_save" />
<RelativeLayout
android:id="@+id/config_widget_seekbar_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignTop="@+id/config_widget_color"
android:layout_alignBottom="@+id/config_widget_color"
android:layout_toRightOf="@+id/config_widget_color"
android:background="@android:color/white">
<SeekBar
android:id="@+id/config_widget_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:paddingLeft="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin" />
</RelativeLayout>
<Button
android:id="@+id/config_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:background="@color/gradient_grey_start"
android:fontFamily="sans-serif-light"
android:paddingLeft="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin"
android:text="@string/ok"
android:textColor="@color/color_primary"
android:textSize="@dimen/big_text_size" />
</RelativeLayout>

View File

@ -2,4 +2,5 @@
<resources>
<dimen name="toggle_recording_button_size">64dp</dimen>
<dimen name="player_button_margin">48dp</dimen>
<dimen name="main_button_size">150dp</dimen>
</resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.simplemobiletools.voicerecorder.activities.WidgetRecordDisplayConfigureActivity"
android:initialLayout="@layout/widget_record_display"
android:minWidth="40dp"
android:minHeight="40dp"
android:previewImage="@drawable/ic_microphone_widget_icon"
android:updatePeriodMillis="86400000" />