2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 06:05:12 +00:00

Update Presenter activity to use Compose

The Presenter activity including its layout file and related Java class were replaced with Compose UI.
Additionally, the androidx.compose.material3:material3 was updated from version 1.1.0 to version 1.1.1.
This commit is contained in:
Dmitry Yudin
2023-08-08 06:12:15 +00:00
committed by Albert Vaca Cintora
parent ad83f76ad1
commit c5c780b5ce
8 changed files with 255 additions and 304 deletions

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterActivity">
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
<LinearLayout
android:id="@+id/mpris_control_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="12dp"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
style="@android:style/TextAppearance.Medium"
android:id="@+id/textView"
android:layout_weight="0"
android:layout_marginBottom="6dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/presenter_lock_tip" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="6dp"
android:orientation="horizontal"
android:layout_weight="1">
<com.google.android.material.button.MaterialButton
android:id="@+id/previous_button"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:contentDescription="@string/mpris_rew"
android:layout_weight="0.25"
android:layout_marginEnd="3dp"
app:icon="@drawable/ic_previous_black"
style="@style/KdeConnectButton.IconButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/next_button"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_marginStart="3dp"
android:layout_weight="0.25"
android:contentDescription="@string/mpris_ff"
app:icon="@drawable/ic_next_black"
style="@style/KdeConnectButton.IconButton" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/pointer_button"
android:layout_width="match_parent"
android:layout_marginBottom="6dp"
android:layout_height="wrap_content"
android:layout_weight="0.30"
android:visibility="gone"
android:text="@string/presenter_pointer"
style="@style/KdeConnectButton.IconButton.Secondary"
tools:visibility="visible" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/fullscreen"
android:title="@string/presenter_fullscreen" />
<item
android:id="@+id/exit_presentation"
android:title="@string/presenter_exit" />
</menu>

View File

@@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -74,10 +72,10 @@ class ComposeSendActivity : AppCompatActivity() {
}
val plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin::class.java)
if (plugin == null) {
finish();
return;
finish()
return
}
plugin.sendKeyboardPacket(np);
plugin.sendKeyboardPacket(np)
}
private fun sendComposed() {
@@ -89,7 +87,6 @@ class ComposeSendActivity : AppCompatActivity() {
userInput.value = String()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ComposeSendScreen() {
Mdc3Theme {
@@ -97,7 +94,6 @@ class ComposeSendActivity : AppCompatActivity() {
topBar = {
KdeTopAppBar(
title = stringResource(R.string.compose_send_title),
navIcon = Icons.Default.ArrowBack,
navIconOnClick = { onBackPressedDispatcher.onBackPressed() },
actions = {
KdeTextButton(

View File

@@ -1,185 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.PresenterPlugin;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.PowerManager;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.media.VolumeProviderCompat;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityPresenterBinding;
import java.util.Objects;
public class PresenterActivity extends AppCompatActivity implements SensorEventListener {
private ActivityPresenterBinding binding;
private MediaSessionCompat mMediaSession;
private PresenterPlugin plugin;
private SensorManager sensorManager;
static final float SENSITIVITY = 0.03f; //TODO: Make configurable?
public void gyroscopeEvent(SensorEvent event) {
float xPos = -event.values[2] * SENSITIVITY;
float yPos = -event.values[0] * SENSITIVITY;
plugin.sendPointer(xPos, yPos);
}
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
gyroscopeEvent(event);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//Ignored
}
void enablePointer() {
if (sensorManager != null) {
return; //Already enabled
}
sensorManager = ContextCompat.getSystemService(this, SensorManager.class);
binding.pointerButton.setVisibility(View.VISIBLE);
binding.pointerButton.setOnTouchListener((v, event) -> {
if(event.getAction() == MotionEvent.ACTION_DOWN){
sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);
v.performClick(); // The linter complains if this is not called
}
else if (event.getAction() == MotionEvent.ACTION_UP) {
sensorManager.unregisterListener(this);
plugin.stopPointer();
}
return true;
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityPresenterBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbarLayout.toolbar);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
String deviceId = getIntent().getStringExtra("deviceId");
this.plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, PresenterPlugin.class);
binding.nextButton.setOnClickListener(v -> plugin.sendNext());
binding.previousButton.setOnClickListener(v -> plugin.sendPrevious());
if (plugin.isPointerSupported()) {
enablePointer();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_presenter, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.fullscreen) {
plugin.sendFullscreen();
return true;
} else if (id == R.id.exit_presentation) {
plugin.sendEsc();
return true;
} else {
return super.onContextItemSelected(item);
}
}
@Override
protected void onStart() {
super.onStart();
if (mMediaSession != null) {
mMediaSession.setActive(true);
return;
}
createMediaSession();
}
@Override
protected void onStop() {
super.onStop();
if (sensorManager != null) {
// Make sure we don't leave the listener on
sensorManager.unregisterListener(this);
}
if (mMediaSession != null) {
PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
boolean screenOn = pm.isInteractive();
if (screenOn) {
mMediaSession.release();
} // else we are in the lockscreen, keep the mediasession
}
}
private void createMediaSession() {
mMediaSession = new MediaSessionCompat(this, "kdeconnect");
// Deprecated flags not required in Build.VERSION_CODES.O and later
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 0)
.build());
mMediaSession.setPlaybackToRemote(getVolumeProvider());
mMediaSession.setActive(true);
}
private VolumeProviderCompat getVolumeProvider() {
final int VOLUME_UP = 1;
final int VOLUME_DOWN = -1;
return new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, 1, 0) {
@Override
public void onAdjustVolume(int direction) {
if (direction == VOLUME_UP) {
plugin.sendNext();
}
else if (direction == VOLUME_DOWN) {
plugin.sendPrevious();
}
}
};
}
@Override
public boolean onSupportNavigateUp() {
super.onBackPressed();
return true;
}
}

View File

@@ -0,0 +1,195 @@
/*
* SPDX-FileCopyrightText: 2023 Dmitry Yudin <dgyudin@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.PresenterPlugin
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.view.MotionEvent
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.media.VolumeProviderCompat
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.UserInterface.compose.KdeButton
import org.kde.kdeconnect.UserInterface.compose.KdeTopAppBar
import org.kde.kdeconnect_tp.R
private const val VOLUME_UP = 1
private const val VOLUME_DOWN = -1
class PresenterActivity : AppCompatActivity(), SensorEventListener {
private val offScreenControlsSupported = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
private val mediaSession by lazy {
if (offScreenControlsSupported) MediaSessionCompat(this, "kdeconnect") else null
}
private val powerManager by lazy { getSystemService(POWER_SERVICE) as PowerManager }
private val plugin: PresenterPlugin by lazy {
KdeConnect.getInstance().getDevicePlugin(intent.getStringExtra("deviceId"), PresenterPlugin::class.java)
}
//TODO: make configurable
private val sensitivity = 0.03f
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_GYROSCOPE) {
val xPos = -event.values[2] * sensitivity
val yPos = -event.values[0] * sensitivity
plugin.sendPointer(xPos, yPos)
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
//ignored
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { PresenterScreen() }
createMediaSession()
}
override fun onResume() {
super.onResume()
mediaSession?.setActive(false)
}
override fun onPause() {
super.onPause()
//fixme watch for isInteractive in background
mediaSession?.setActive(!powerManager.isInteractive)
}
override fun onDestroy() {
mediaSession?.release()
super.onDestroy()
}
private fun createMediaSession() {
mediaSession?.setPlaybackState(
PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_PLAYING, 0, 0f).build()
)
mediaSession?.setPlaybackToRemote(volumeProvider)
}
private val volumeProvider = object : VolumeProviderCompat(VOLUME_CONTROL_RELATIVE, 1, 0) {
override fun onAdjustVolume(direction: Int) {
if (direction == VOLUME_UP) {
plugin.sendNext()
} else if (direction == VOLUME_DOWN) {
plugin.sendPrevious()
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Preview
@Composable
private fun PresenterScreen() {
val sensorManager = LocalContext.current.getSystemService(SENSOR_SERVICE) as? SensorManager
Mdc3Theme {
Scaffold(topBar = { PresenterAppBar() }) {
Column(
modifier = Modifier.fillMaxSize().padding(it).padding(16.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
if (offScreenControlsSupported) Text(
text = stringResource(R.string.presenter_lock_tip),
modifier = Modifier.padding(bottom = 8.dp).padding(horizontal = 16.dp),
style = MaterialTheme.typography.bodyLarge,
)
Row(
modifier = Modifier.fillMaxSize().weight(3f),
horizontalArrangement = Arrangement.spacedBy(20.dp),
) {
KdeButton(
onClick = { plugin.sendPrevious() },
modifier = Modifier.fillMaxSize().weight(1f),
icon = Icons.Default.ArrowBack,
)
KdeButton(
onClick = { plugin.sendNext() },
modifier = Modifier.fillMaxSize().weight(1f),
icon = Icons.Default.ArrowForward,
)
}
if (sensorManager != null) KdeButton(
onClick = {},
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = Modifier.fillMaxSize().weight(1f).pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
sensorManager.registerListener(
this@PresenterActivity,
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
SensorManager.SENSOR_DELAY_GAME
)
}
MotionEvent.ACTION_UP -> {
sensorManager.unregisterListener(this@PresenterActivity)
plugin.stopPointer()
false
}
else -> false
}
},
text = stringResource(R.string.presenter_pointer),
)
}
}
}
}
@Preview
@Composable
private fun PresenterAppBar() {
var dropdownShownState by remember { mutableStateOf(false) }
KdeTopAppBar(navIconOnClick = { onBackPressedDispatcher.onBackPressed() }, actions = {
IconButton(onClick = { dropdownShownState = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.extra_options))
}
DropdownMenu(expanded = dropdownShownState, onDismissRequest = { dropdownShownState = false }) {
DropdownMenuItem(
onClick = { plugin.sendFullscreen() },
text = { Text(stringResource(R.string.presenter_fullscreen)) },
)
DropdownMenuItem(
onClick = { plugin.sendEsc() },
text = { Text(stringResource(R.string.presenter_exit)) },
)
}
})
}
}

View File

@@ -9,12 +9,16 @@ package org.kde.kdeconnect.UserInterface.compose
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
@@ -39,4 +43,46 @@ fun KdeTextButton(
Text(text = text)
}
)
}
@Composable
fun KdeButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
colors: ButtonColors = ButtonDefaults.buttonColors(),
text: String? = null,
icon: ImageVector? = null,
) {
//TODO uncomment when button is widely used
// val interactionSource = remember { MutableInteractionSource() }
// val pressedState = interactionSource.collectIsPressedAsState()
// val cornerSize by animateDpAsState(
// targetValue = if (pressedState.value) 24.dp else 48.dp,
// label = "Corner size change on press"
// )
Button(
onClick = onClick,
modifier = modifier,
// shape = RoundedCornerShape(cornerSize),
shape = RoundedCornerShape(24.dp),
colors = colors,
// interactionSource = interactionSource,
content = {
icon?.let { Icon(imageVector = it, contentDescription = text) }
text?.let { Text(it, maxLines = 1, overflow = Ellipsis) }
}
)
}
@Preview
@Composable
fun IconButtonPreview() {
KdeButton(
{},
Modifier.width(120.dp),
ButtonDefaults.buttonColors(Color.Gray, Color.DarkGray),
"Button Text",
Icons.Default.Build,
)
}

View File

@@ -7,7 +7,6 @@
package org.kde.kdeconnect.UserInterface.compose
import android.annotation.SuppressLint
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@@ -17,7 +16,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import org.kde.kdeconnect_tp.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KdeTextField(modifier: Modifier = Modifier, input: MutableState<String>, label: String) {
var value by rememberSaveable { input }

View File

@@ -7,20 +7,20 @@
package org.kde.kdeconnect.UserInterface.compose
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import org.kde.kdeconnect_tp.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KdeTopAppBar(
title: String,
navIcon: ImageVector,
navIconOnClick: () -> Unit,
title: String = stringResource(R.string.kde_connect),
navIcon: ImageVector = Icons.Default.ArrowBack,
navIconOnClick: () -> Unit, // = { onBackPressedDispatcher.onBackPressed() }
actions: @Composable (RowScope.() -> Unit) = {},
) {
TopAppBar(