Skip to content

Commit

Permalink
Merge pull request #431 from joaomgcd/taskerplugin
Browse files Browse the repository at this point in the history
Taskerplugin
  • Loading branch information
mchowning committed Oct 24, 2022
2 parents f3654d6 + fcd33ba commit fba1b2a
Show file tree
Hide file tree
Showing 25 changed files with 814 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
-----

* New Features:
* Added Tasker integration with "Play Filter" and "Control Playback" actions.
([#415](https://github.com/Automattic/pocket-casts-android/pull/431)).
* Fixed background color for screens using the compose theme
([#432](https://github.com/Automattic/pocket-casts-android/pull/432)).
* Bug Fixes:
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dependencies {
implementation project(':modules:features:filters')
implementation project(':modules:features:navigation')
implementation project(':modules:features:account')
implementation project(':modules:features:taskerplugin')
implementation project(':modules:features:endofyear')
}

Expand Down
21 changes: 21 additions & 0 deletions modules/features/taskerplugin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apply from: "../../modules.gradle"

android {
namespace 'au.com.shiftyjelly.pocketcasts.taskerplugin'
buildFeatures {
viewBinding true
dataBinding = true
compose true
}
}

dependencies {
implementation project(':modules:services:localization')
implementation project(':modules:services:ui')
implementation project(':modules:services:compose')
implementation project(':modules:services:repositories')
implementation project(':modules:services:model')
implementation project(':modules:services:views')
implementation project(':modules:services:images')
implementation 'com.joaomgcd:taskerpluginlibrary:0.4.3'
}
25 changes: 25 additions & 0 deletions modules/features/taskerplugin/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http:https://schemas.android.com/apk/res/android">

<application>

<activity
android:name=".playplaylist.config.ActivityConfigPlayPlaylist"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="Pocket Casts Play Filter">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<activity
android:name=".controlplayback.config.ActivityConfigControlPlayback"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="Pocket Casts Control Playback">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.hilt.appTheme

abstract class ActivityConfigBase<TViewModel : ViewModelBase<*, *>> : ComponentActivity() {
protected abstract val viewModel: TViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onCreate({ finish() }, { intent }, { code, data -> setResult(code, data) })
setContent {
AppThemeWithBackground(themeType = appTheme.activeTheme) {
Content()
}
}
}
@Composable
protected abstract fun Content()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.compose.theme
import au.com.shiftyjelly.pocketcasts.localization.R
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme

class TaskerInputFieldState<T>(val content: Content<T>) {
data class Content<T> constructor(
val value: String?,
@StringRes val labelResId: Int,
val onTextChange: (String) -> Unit,
val taskerVariables: List<String>,
val possibleItems: List<T>? = null,
val itemToString: (T?) -> String = { it?.toString() ?: "" },
val itemContent: @Composable (T) -> Unit = { Text(text = itemToString(it)) }
)
}

private enum class TaskerInputFieldSelectMode { Variable, ItemList }

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun <T> ComposableTaskerInputField(content: TaskerInputFieldState.Content<T>) {
var selectionMode by remember { mutableStateOf(null as TaskerInputFieldSelectMode?) }
val keyboardController = LocalSoftwareKeyboardController.current

/**
* @param selection if null, just hide dropdown and don't signal text change
*/
fun finishSelecting(selection: String? = null) {
selectionMode = null
keyboardController?.hide()
selection?.let { content.onTextChange(it) }
}
Box {

Row {
val possibleItems = content.possibleItems

val hasSuggestedItems = !possibleItems.isNullOrEmpty()
val hasTaskerVariables = content.taskerVariables.isNotEmpty()
OutlinedTextField(
modifier = Modifier.weight(1f),
value = content.value ?: "",
label = { Text(text = stringResource(id = content.labelResId)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { finishSelecting() }),
onValueChange = {
content.onTextChange(it)
},
trailingIcon = if (!hasSuggestedItems && !hasTaskerVariables) null else {
{
Row {
if (hasTaskerVariables) {
IconButton(onClick = { selectionMode = TaskerInputFieldSelectMode.Variable }) {
Icon(
painter = painterResource(au.com.shiftyjelly.pocketcasts.taskerplugin.R.drawable.label_outline),
contentDescription = stringResource(R.string.tasker_variables),
tint = MaterialTheme.theme.colors.primaryIcon01,
modifier = Modifier.padding(end = 16.dp, start = 16.dp)
)
}
}
if (hasSuggestedItems) {
IconButton(onClick = { selectionMode = TaskerInputFieldSelectMode.ItemList }) {
Icon(
painter = painterResource(au.com.shiftyjelly.pocketcasts.images.R.drawable.ic_search),
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.theme.colors.primaryIcon01,
modifier = Modifier.padding(end = 16.dp, start = 16.dp)
)
}
}
}
}
}
)
val dropdownMaxHeight = screenSize.height / 6 * 2 //at most dropdown can be 2/6 of the screen size so it doesn't draw over its parent
if (hasTaskerVariables) {
DropdownMenu(
modifier = Modifier.requiredSizeIn(maxHeight = dropdownMaxHeight),
expanded = selectionMode == TaskerInputFieldSelectMode.Variable,
onDismissRequest = { finishSelecting() },
properties = PopupProperties(focusable = false)
) {
content.taskerVariables.forEach {
DropdownMenuItem(onClick = {
finishSelecting(it)
}) {
Text(it)
}
}
}
}
if (possibleItems != null && possibleItems.isNotEmpty()) {
DropdownMenu(
modifier = Modifier.requiredSizeIn(maxHeight = dropdownMaxHeight),
expanded = selectionMode == TaskerInputFieldSelectMode.ItemList,
onDismissRequest = { finishSelecting() },
properties = PopupProperties(focusable = false)
) {
possibleItems.forEach {
DropdownMenuItem(onClick = {
finishSelecting(content.itemToString(it))
}) {
content.itemContent(it)
}
}
}
}
}
}
}

@Preview(showBackground = true)
@Composable
private fun ComposableTaskerInputFieldPreview() {
AppTheme(Theme.ThemeType.CLASSIC_LIGHT) {
ComposableTaskerInputField(
TaskerInputFieldState.Content(
"some value", R.string.archive, {},
listOf("%test"),
listOf("Hi", "Hello")
)
)
}
}

@Composable
fun ComposableTaskerInputFieldList(
fieldContents: List<TaskerInputFieldState.Content<*>>,
onFinish: () -> Unit
) {
Box(modifier = Modifier.fillMaxHeight()) {
LazyColumn {
fieldContents.forEach { content ->
item {
ComposableTaskerInputField(content)
}
}
}
Button(onClick = onFinish, modifier = Modifier.align(Alignment.BottomEnd)) {
Text(stringResource(R.string.ok))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp

val String?.nullIfEmpty get() = if (isNullOrEmpty()) null else this
fun <T> tryOrNull(handleError: ((Throwable) -> T?)? = null, block: () -> T?): T? = try {
block()
} catch (t: Throwable) {
handleError?.invoke(t)
}

val screenSize
@Composable
get() :DpSize {
val configuration = LocalConfiguration.current

val screenHeight = configuration.screenHeightDp.dp
val screenWidth = configuration.screenWidthDp.dp
return DpSize(width = screenWidth, height = screenHeight)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

import android.app.Application
import android.content.Intent
import androidx.lifecycle.AndroidViewModel
import au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback.InputControlPlayback
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import kotlinx.coroutines.flow.MutableStateFlow

abstract class ViewModelBase<TInput : Any, THelper : TaskerPluginConfigHelperNoOutput<TInput, out TaskerPluginRunnerActionNoOutput<TInput>>>(application: Application) : AndroidViewModel(application), TaskerPluginConfig<TInput> {
override val context get() = getApplication<Application>()
abstract val helperClass: Class<THelper>
private val taskerHelper by lazy { helperClass.getConstructor(TaskerPluginConfig::class.java).newInstance(this) }
protected var input: TInput? = null

private var taskerInput
get() = TaskerInput(input ?: taskerHelper.inputClass.newInstance())
set(value) {
input = value.regular
}

override val inputForTasker: TaskerInput<TInput>
get() = taskerInput

fun getDescription(command: InputControlPlayback.PlaybackCommand) = command.getDescription(context)
override fun assignFromInput(input: TaskerInput<TInput>) {
taskerInput = input
}

fun onCreate(finishFunc: (() -> Unit), getIntentFunc: (() -> Intent?), setResultFunc: ((Int, Intent) -> Unit)) {
this.finishFunc = finishFunc
this.getIntentFunc = getIntentFunc
this.setResultFunc = setResultFunc
taskerHelper.onCreate()
}

private var finishFunc: (() -> Unit)? = null
override fun finish() = finishFunc?.invoke() ?: Unit

private var getIntentFunc: (() -> Intent?)? = null
override fun getIntent() = getIntentFunc?.invoke()

private var setResultFunc: ((Int, Intent) -> Unit)? = null
override fun setResult(resultCode: Int, data: Intent) = setResultFunc?.invoke(resultCode, data) ?: Unit

fun finishForTasker() = taskerHelper.finishForTasker()

val taskerVariables by lazy { taskerHelper.relevantVariables.distinct().sortedBy { it } }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base.hilt

import android.content.Context
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PlaylistManager
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent

@InstallIn(SingletonComponent::class)
@EntryPoint
interface ThemeEntryPoint {
fun getTheme(): Theme
}
@InstallIn(SingletonComponent::class)
@EntryPoint
interface PlaybackManagerEntryPoint {
fun getPlaybackManager(): PlaybackManager
}

@InstallIn(SingletonComponent::class)
@EntryPoint
interface PlaylistManagerEntryPoint {
fun getPlaylistManager(): PlaylistManager
}

@InstallIn(SingletonComponent::class)
@EntryPoint
interface EpisodeManagerEntryPoint {
fun getEpisodeManager(): EpisodeManager
}

val Context.appTheme get() = EntryPointAccessors.fromApplication(applicationContext, ThemeEntryPoint::class.java).getTheme()
val Context.playbackManager get() = EntryPointAccessors.fromApplication(applicationContext, PlaybackManagerEntryPoint::class.java).getPlaybackManager()
val Context.playlistManager get() = EntryPointAccessors.fromApplication(applicationContext, PlaylistManagerEntryPoint::class.java).getPlaylistManager()
val Context.episodeManager get() = EntryPointAccessors.fromApplication(applicationContext, EpisodeManagerEntryPoint::class.java).getEpisodeManager()
Loading

0 comments on commit fba1b2a

Please sign in to comment.