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

Use Jetpack Compose for the Device UI

This commit is contained in:
Albert Vaca Cintora
2023-05-31 13:40:29 +02:00
parent 03f50994ee
commit a6ec1744b7
26 changed files with 360 additions and 377 deletions

View File

@@ -159,10 +159,13 @@ dependencies {
implementation 'androidx.compose.material3:material3:1.1.0'
implementation 'androidx.compose.ui:ui-tooling-preview:1.4.3'
implementation 'androidx.activity:activity-compose:1.7.1'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'com.google.accompanist:accompanist-themeadapter-material3:0.31.0-alpha'
implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.1'
implementation 'androidx.compose.ui:ui-tooling-preview:1.4.3'
debugImplementation 'androidx.compose.ui:ui-tooling:1.4.3'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'

View File

@@ -1,9 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_drawer"

View File

@@ -1,51 +1,33 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment">
<!-- Shown when device is reachable but not yet paired -->
<include
android:id="@+id/pair_request"
layout="@layout/view_pair_request"
tools:visibility="gone"/>
<!-- Shown when the device is paired but not reachable -->
<include
android:id="@+id/pair_error"
layout="@layout/view_pair_error"
tools:visibility="gone"/>
<androidx.core.widget.NestedScrollView
android:id="@+id/device_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Shown when the device is paired and reachable -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/device_view_compose"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment">
<!-- Layout shown when device is reachable but not yet paired -->
<include
android:id="@+id/pair_request"
layout="@layout/view_pair_request"
tools:visibility="gone"/>
<!-- Layout shown when we can't pair with device or device is not reachable -->
<include
android:id="@+id/pair_error"
layout="@layout/view_pair_error"
tools:visibility="gone"/>
<!-- Layouts shown when device is paired and reachable -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/plugins_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:nestedScrollingEnabled="false"
tools:listitem="@layout/list_plugin_entry"
tools:layout_height="300dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/permissions_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
tools:context=".DeviceActivity"
tools:listitem="@layout/list_item_plugin_header"
tools:layout_height="300dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>
</FrameLayout>

View File

@@ -1,37 +1,33 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
tools:context="org.kde.kdeconnect.UserInterface.MainActivity"
android:fitsSystemWindows="true">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout"/>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:context="org.kde.kdeconnect.UserInterface.MainActivity"
android:fitsSystemWindows="true">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"/>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
</FrameLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -14,7 +14,7 @@
android:divider="@null"
android:dividerHeight="12dp"
android:orientation="vertical"
tools:listitem="@layout/list_plugin_entry"
tools:listitem="@layout/list_card_entry"
tools:context=".MainActivity" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -1,35 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/error_message_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/error_message_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:paddingEnd="8dip"
android:paddingStart="0dip"
android:src="@drawable/ic_error_outline_48dp"
app:tint="?attr/colorHighContrast"
tools:ignore="UnusedAttribute" />
<TextView
android:id="@+id/not_reachable_message"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="@string/unreachable_description"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
android:layout_height="match_parent"
android:padding="16dp"
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/error_message_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:paddingEnd="8dip"
android:paddingStart="0dip"
android:src="@drawable/ic_error_outline_48dp"
app:tint="?attr/colorHighContrast"
tools:ignore="UnusedAttribute" />
<TextView
android:id="@+id/not_reachable_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="@string/unreachable_description"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:visibility="visible" />
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -6,7 +6,7 @@
android:id="@+id/pairing_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_gravity="top"
android:orientation="vertical"
android:padding="@dimen/activity_vertical_margin"
android:visibility="gone"

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="plugins_columns">3</integer>
<integer name="mpris_now_playing_orientation">@integer/orientation_horizontal</integer>
<integer name="mpris_now_playing_album_weight">1</integer>
<integer name="mpris_now_playing_controls_weight">1</integer>

View File

@@ -14,11 +14,10 @@ import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.KeyEvent;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
@@ -53,8 +52,8 @@ public class BigscreenPlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.ic_presenter_24dp);
public @DrawableRes int getIcon() {
return R.drawable.ic_presenter_24dp;
}
@Override
@@ -68,7 +67,7 @@ public class BigscreenPlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return true;
}

View File

@@ -7,17 +7,16 @@
package org.kde.kdeconnect.Plugins.ClibpoardPlugin;
import android.Manifest;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
@@ -130,14 +129,14 @@ public class ClipboardPlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return Build.VERSION.SDK_INT > Build.VERSION_CODES.P &&
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_LOGS) == PackageManager.PERMISSION_DENIED;
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.ic_baseline_content_paste_24);
public @DrawableRes int getIcon() {
return R.drawable.ic_baseline_content_paste_24;
}
@Override

View File

@@ -9,10 +9,9 @@ package org.kde.kdeconnect.Plugins.MousePadPlugin;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
@@ -48,8 +47,8 @@ public class MousePadPlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.touchpad_plugin_action_24dp);
public @DrawableRes int getIcon() {
return R.drawable.touchpad_plugin_action_24dp;
}
@Override
@@ -63,7 +62,7 @@ public class MousePadPlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return true;
}

View File

@@ -10,11 +10,10 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
@@ -250,8 +249,8 @@ public class MprisPlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.mpris_plugin_action_24dp);
public @DrawableRes int getIcon() {
return R.drawable.mpris_plugin_action_24dp;
}
@Override
@@ -482,7 +481,7 @@ public class MprisPlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return true;
}

View File

@@ -7,11 +7,10 @@
package org.kde.kdeconnect.Plugins.PhotoPlugin;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.kde.kdeconnect.Helpers.FilesHelper;
import org.kde.kdeconnect.NetworkPacket;
@@ -62,8 +61,8 @@ public class PhotoPlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.ic_camera);
public @DrawableRes int getIcon() {
return R.drawable.ic_camera;
}
void sendCancel() {

View File

@@ -10,10 +10,10 @@ import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.CallSuper;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
@@ -109,11 +109,10 @@ public abstract class Plugin {
}
/**
* Return an icon associated to this plugin. This function can
* access this.context to load the image from resources.
* Return an icon associated to this plugin. Only needed if hasMainActivity() returns true and displayInContextMenu() returns false
*/
public @Nullable Drawable getIcon() {
return null;
public @DrawableRes int getIcon() {
return -1;
}
/**
@@ -156,16 +155,10 @@ public abstract class Plugin {
/**
* Return true if the plugin should display something in the Device main view
*/
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return false;
}
/**
* Implement here what your plugin should do when clicked
*/
public void startMainActivity(Activity parentActivity) {
}
/**
* Return true if the entry for this app should appear in the context menu instead of the main view
*/
@@ -173,6 +166,12 @@ public abstract class Plugin {
return false;
}
/**
* Implement here what your plugin should do when clicked
*/
public void startMainActivity(Activity parentActivity) {
}
/**
* Returns false when we should avoid loading this Plugin for {@link #device}.
* <p>

View File

@@ -9,9 +9,9 @@ package org.kde.kdeconnect.Plugins;
import static org.apache.commons.collections4.SetUtils.unmodifiableSet;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -38,7 +38,7 @@ public class PluginFactory {
public static class PluginInfo {
PluginInfo(@NonNull String displayName, @NonNull String description, @Nullable Drawable icon,
PluginInfo(@NonNull String displayName, @NonNull String description, @DrawableRes int icon,
boolean enabledByDefault, boolean hasSettings, boolean supportsDeviceSpecificSettings,
boolean listenToUnpaired, @NonNull String[] supportedPacketTypes, @NonNull String[] outgoingPacketTypes,
@NonNull Class<? extends Plugin> instantiableClass) {
@@ -62,7 +62,7 @@ public class PluginFactory {
return description;
}
public @Nullable Drawable getIcon() {
public @DrawableRes int getIcon() {
return icon;
}
@@ -94,7 +94,7 @@ public class PluginFactory {
private final @NonNull String displayName;
private final @NonNull String description;
private final @Nullable Drawable icon;
private final @DrawableRes int icon;
private final boolean enabledByDefault;
private final boolean hasSettings;
private final boolean supportsDeviceSpecificSettings;

View File

@@ -12,11 +12,10 @@ import static org.kde.kdeconnect.Plugins.MousePadPlugin.KeyListenerView.SpecialK
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.KeyEvent;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.Device;
@@ -51,8 +50,8 @@ public class PresenterPlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.ic_presenter_24dp);
public @DrawableRes int getIcon() {
return R.drawable.ic_presenter_24dp;
}
@Override
@@ -61,7 +60,7 @@ public class PresenterPlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return true;
}

View File

@@ -8,7 +8,6 @@ package org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin;
import android.app.Activity;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.Settings;
@@ -20,8 +19,8 @@ import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import androidx.fragment.app.DialogFragment;
@@ -153,8 +152,8 @@ public class RemoteKeyboardPlugin extends Plugin implements SharedPreferences.On
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.ic_action_keyboard_24dp);
public @DrawableRes int getIcon() {
return R.drawable.ic_action_keyboard_24dp;
}
@Override

View File

@@ -11,12 +11,11 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import org.json.JSONArray;
@@ -78,8 +77,8 @@ public class RunCommandPlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.run_command_plugin_icon_24dp);
public @DrawableRes int getIcon() {
return R.drawable.run_command_plugin_icon_24dp;
}
@Override
@@ -173,7 +172,7 @@ public class RunCommandPlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return true;
}

View File

@@ -11,7 +11,6 @@ import android.app.Activity;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -20,6 +19,7 @@ import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
@@ -84,8 +84,8 @@ public class SharePlugin extends Plugin {
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.share_plugin_action_24dp);
public @DrawableRes int getIcon() {
return R.drawable.share_plugin_action_24dp;
}
@Override
@@ -94,7 +94,7 @@ public class SharePlugin extends Plugin {
}
@Override
public boolean hasMainActivity(Context context) {
public boolean displayAsButton(Context context) {
return true;
}

View File

@@ -14,24 +14,36 @@ import android.view.Menu
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.annotation.UiThread
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device
import org.kde.kdeconnect.Device.PairingCallback
import org.kde.kdeconnect.Device.PluginsChangedListener
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.UserInterface.List.PluginAdapter
import org.kde.kdeconnect.UserInterface.List.PluginItem
import org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterPlugin
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin
import org.kde.kdeconnect.UserInterface.compose.AppTheme
import org.kde.kdeconnect_tp.R
import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding
import org.kde.kdeconnect_tp.databinding.ViewPairErrorBinding
import org.kde.kdeconnect_tp.databinding.ViewPairRequestBinding
import java.util.concurrent.ConcurrentHashMap
/**
* Main view. Displays the current device and its plugins
@@ -44,14 +56,8 @@ class DeviceFragment : Fragment() {
private var device: Device? = null
private val mActivity: MainActivity? by lazy { activity as MainActivity? }
//TODO use LinkedHashMap and delete irrelevant records when plugins changed
private val pluginListItems: ArrayList<PluginItem> = ArrayList()
private val permissionListItems: ArrayList<PluginItem> = ArrayList()
/**
* Top-level ViewBinding for this fragment.
*
* Host for [.pluginListItems].
*/
private var deviceBinding: ActivityDeviceBinding? = null
private fun requireDeviceBinding() = deviceBinding ?: throw IllegalStateException("deviceBinding is not set")
@@ -100,6 +106,10 @@ class DeviceFragment : Fragment() {
device = KdeConnect.getInstance().getDevice(deviceId)
requireErrorBinding().errorMessageContainer.setOnRefreshListener {
this.refreshDevicesAction()
}
requirePairingBinding().pairButton.setOnClickListener {
with(requirePairingBinding()) {
pairButton.visibility = View.GONE
@@ -128,10 +138,6 @@ class DeviceFragment : Fragment() {
}
setHasOptionsMenu(true)
requireDeviceBinding().pluginsList.layoutManager =
GridLayoutManager(requireContext(), resources.getInteger(R.integer.plugins_columns))
requireDeviceBinding().permissionsList.layoutManager = LinearLayoutManager(requireContext())
device?.apply {
mActivity?.supportActionBar?.title = name
addPairingCallback(pairingCallback)
@@ -146,7 +152,15 @@ class DeviceFragment : Fragment() {
return deviceBinding.root
}
private val pluginsChangedListener = PluginsChangedListener { refreshUI() }
private fun refreshDevicesAction() {
BackgroundService.ForceRefreshConnections(requireContext())
requireErrorBinding().errorMessageContainer.isRefreshing = true
requireErrorBinding().errorMessageContainer.postDelayed({
errorBinding?.errorMessageContainer?.isRefreshing = false // check for null since the view might be destroyed by now
}, 1500)
}
private val pluginsChangedListener = PluginsChangedListener { mActivity?.runOnUiThread { refreshUI() } }
override fun onDestroyView() {
device?.apply {
removePluginsChangedListener(pluginsChangedListener)
@@ -208,8 +222,8 @@ class DeviceFragment : Fragment() {
}
if (device.isPaired) {
menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener {
// Remove listener so buttons don't show for an instant before changing the view
device.apply {
// Remove listener so buttons don't show for an instant before changing the view
removePairingCallback(pairingCallback)
removePluginsChangedListener(pluginsChangedListener)
unpair()
@@ -245,106 +259,54 @@ class DeviceFragment : Fragment() {
}
}
@UiThread
private fun refreshUI() {
val device = device ?: return
//Once in-app, there is no point in keep displaying the notification if any
device.hidePairingNotification()
mActivity?.runOnUiThread(object : Runnable {
override fun run() {
if (device.isPairRequestedByPeer) {
with (requirePairingBinding()) {
pairMessage.setText(R.string.pair_requested)
pairVerification.visibility = View.VISIBLE
pairVerification.text = SslHelper.getVerificationKey(SslHelper.certificate, device.certificate)
pairingButtons.visibility = View.VISIBLE
pairProgress.visibility = View.GONE
pairButton.visibility = View.GONE
pairRequestButtons.visibility = View.VISIBLE
}
with (requireDeviceBinding()) {
permissionsList.visibility = View.GONE
pluginsList.visibility = View.GONE
}
} else {
val paired = device.isPaired
val reachable = device.isReachable
requirePairingBinding().pairingButtons.visibility = if (paired) View.GONE else View.VISIBLE
if (paired && !reachable) {
requireErrorBinding().errorMessageContainer.visibility = View.VISIBLE
requireErrorBinding().notReachableMessage.visibility = View.VISIBLE
requireDeviceBinding().permissionsList.visibility = View.GONE
requireDeviceBinding().pluginsList.visibility = View.GONE
} else if (paired) {
requireErrorBinding().errorMessageContainer.visibility = View.GONE
requireErrorBinding().notReachableMessage.visibility = View.GONE
requireDeviceBinding().permissionsList.visibility = View.VISIBLE
requireDeviceBinding().pluginsList.visibility = View.VISIBLE
} else {
requireDeviceBinding().permissionsList.visibility = View.GONE
requireDeviceBinding().pluginsList.visibility = View.GONE
}
try {
if (paired && reachable) {
//Plugins button list
val plugins: Collection<Plugin> = device.loadedPlugins.values
//TODO look for LinkedHashMap mention above
pluginListItems.clear()
permissionListItems.clear()
//Fill enabled plugins ArrayList
for (p in plugins) {
if (p.hasMainActivity(context) && !p.displayInContextMenu()) {
pluginListItems.add(
PluginItem(requireContext(), p, { p.startMainActivity(mActivity) })
)
}
}
//Fill permissionListItems with permissions plugins
createPermissionsList(
device.pluginsWithoutPermissions,
R.string.plugins_need_permission
) { p: Plugin ->
p.permissionExplanationDialog.show(childFragmentManager, null)
}
createPermissionsList(
device.pluginsWithoutOptionalPermissions,
R.string.plugins_need_optional_permission
) { p: Plugin ->
p.optionalPermissionExplanationDialog.show(childFragmentManager, null)
}
requireDeviceBinding().permissionsList.adapter =
PluginAdapter(permissionListItems, R.layout.list_item_plugin_header)
requireDeviceBinding().pluginsList.adapter =
PluginAdapter(pluginListItems, R.layout.list_plugin_entry)
requireDeviceBinding().permissionsList.adapter?.notifyDataSetChanged()
requireDeviceBinding().pluginsList.adapter?.notifyDataSetChanged()
displayBatteryInfoIfPossible()
}
mActivity?.invalidateOptionsMenu()
} catch (e: IllegalStateException) {
//Ignore: The activity was closed while we were trying to update it
} catch (e: ConcurrentModificationException) {
Log.e(TAG, "ConcurrentModificationException")
this.run() //Try again
}
}
if (device.isPairRequestedByPeer) {
with (requirePairingBinding()) {
pairMessage.setText(R.string.pair_requested)
pairVerification.visibility = View.VISIBLE
pairVerification.text = SslHelper.getVerificationKey(SslHelper.certificate, device.certificate)
pairingButtons.visibility = View.VISIBLE
pairProgress.visibility = View.GONE
pairButton.visibility = View.GONE
pairRequestButtons.visibility = View.VISIBLE
}
})
requireDeviceBinding().deviceView.visibility = View.GONE
} else {
if (device.isPaired) {
requirePairingBinding().pairingButtons.visibility = View.GONE
if (device.isReachable) {
requireErrorBinding().errorMessageContainer.visibility = View.GONE
requireDeviceBinding().deviceView.visibility = View.VISIBLE
requireDeviceBinding().deviceViewCompose.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { AppTheme { PluginList(device) } }
}
displayBatteryInfoIfPossible()
} else {
requireErrorBinding().errorMessageContainer.visibility = View.VISIBLE
requireDeviceBinding().deviceView.visibility = View.GONE
}
} else {
requireErrorBinding().errorMessageContainer.visibility = View.GONE
requireDeviceBinding().deviceView.visibility = View.GONE
requirePairingBinding().pairingButtons.visibility = View.VISIBLE
}
mActivity?.invalidateOptionsMenu()
}
}
private val pairingCallback: PairingCallback = object : PairingCallback {
override fun incomingRequest() {
refreshUI()
mActivity?.runOnUiThread { refreshUI() }
}
override fun pairingSuccessful() {
refreshUI()
mActivity?.runOnUiThread { refreshUI() }
}
override fun pairingFailed(error: String) {
@@ -375,28 +337,6 @@ class DeviceFragment : Fragment() {
}
}
private fun createPermissionsList(
plugins: ConcurrentHashMap<String, Plugin>,
@StringRes headerText: Int,
action: (Plugin) -> Unit,
) {
if (plugins.isEmpty()) return
val device = device ?: return
permissionListItems.add(
PluginItem(
context = requireContext(),
header = requireContext().getString(headerText),
textStyleRes = com.google.android.material.R.style.TextAppearance_Material3_BodyMedium,
)
)
for (plugin in plugins.values) {
if (device.isPluginEnabled(plugin.pluginKey)) {
permissionListItems.add(
PluginItem(requireContext(), plugin, action, com.google.android.material.R.style.TextAppearance_Material3_LabelLarge)
)
}
}
}
/**
* This method tries to display battery info for the remote device. Includes
@@ -429,4 +369,120 @@ class DeviceFragment : Fragment() {
super.onDetach()
mActivity?.supportActionBar?.subtitle = null
}
@Composable
@Preview
fun PreviewCompose() {
val plugins = listOf(MprisPlugin(), RunCommandPlugin(), PresenterPlugin())
plugins.forEach { it.setContext(LocalContext.current, null) }
PluginButtons(plugins.iterator(), 2)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PluginButton(plugin : Plugin, modifier: Modifier) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier.height(height = 120.dp),
onClick = { plugin.startMainActivity(mActivity) }
) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(horizontal=16.dp, vertical=10.dp)
) {
Icon(
painter = painterResource(plugin.icon),
modifier = Modifier.padding(top = 12.dp),
contentDescription = null
)
Text(
text = plugin.actionName,
maxLines = 2,
fontSize = 18.sp,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
fun PluginButtons(plugins: Iterator<Plugin>, numColumns: Int)
{
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
while (plugins.hasNext()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
repeat(numColumns) {
if (plugins.hasNext()) {
PluginButton(
plugin = plugins.next(),
modifier = Modifier.weight(1f)
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}
@Composable
fun PluginsWithoutPermissions(title : String, plugins: Collection<Plugin>, action : (plugin: Plugin) -> Unit)
{
Text(
text = title,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
)
plugins.forEach { plugin ->
Text(
text = plugin.displayName,
modifier = Modifier
.fillMaxWidth()
.clickable { action(plugin) }
.padding(start = 28.dp, end = 16.dp, top = 12.dp, bottom = 12.dp)
)
}
}
@Composable
fun PluginList(device : Device) {
val context = requireContext()
val pluginsWithButtons = device.loadedPlugins.values.filter { it.displayAsButton(context) }.iterator()
val pluginsNeedPermissions = device.pluginsWithoutPermissions.values.filter { device.isPluginEnabled(it.pluginKey) }
val pluginsNeedOptionalPermissions = device.pluginsWithoutOptionalPermissions.values.filter { device.isPluginEnabled(it.pluginKey) }
Surface {
Column(modifier = Modifier.padding(top = 16.dp)) {
val numColumns = resources.getInteger(R.integer.plugins_columns)
PluginButtons(pluginsWithButtons, numColumns)
Spacer(modifier = Modifier.padding(vertical=6.dp))
if (pluginsNeedPermissions.isNotEmpty()) {
PluginsWithoutPermissions(
title = getString(R.string.plugins_need_permission),
plugins = pluginsNeedPermissions,
action = { it.permissionExplanationDialog.show(childFragmentManager,null) }
)
Spacer(modifier = Modifier.padding(vertical=2.dp))
}
if (pluginsNeedOptionalPermissions.isNotEmpty()) {
PluginsWithoutPermissions(
title = getString(R.string.plugins_need_optional_permission),
plugins = pluginsNeedOptionalPermissions,
action = { it.optionalPermissionExplanationDialog.show(childFragmentManager,null) }
)
}
}
}
}
}

View File

@@ -12,7 +12,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import org.kde.kdeconnect_tp.databinding.ListPluginEntryBinding;
import org.kde.kdeconnect_tp.databinding.ListCardEntryBinding;
public class EntryItemWithIcon implements ListAdapter.Item {
protected final String title;
@@ -26,7 +26,7 @@ public class EntryItemWithIcon implements ListAdapter.Item {
@NonNull
@Override
public View inflateView(@NonNull LayoutInflater layoutInflater) {
final ListPluginEntryBinding binding = ListPluginEntryBinding.inflate(layoutInflater);
final ListCardEntryBinding binding = ListCardEntryBinding.inflate(layoutInflater);
binding.listItemEntryTitle.setText(title);
binding.listItemEntryIcon.setImageDrawable(icon);

View File

@@ -1,53 +0,0 @@
package org.kde.kdeconnect.UserInterface.List
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.kde.kdeconnect_tp.R
/**
* Adapter for showing enabled plugins and permission requests
* can be used with following layouts:
* list_plugin_entry - card view with text and icon
* list_item_plugin_header - plain TextView
* Any other TextView layout
*/
class PluginAdapter(
private val pluginList: ArrayList<PluginItem>,
private val layoutRes: Int,
) : RecyclerView.Adapter<PluginAdapter.PluginViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(viewGroup: ViewGroup, type: Int) : PluginViewHolder {
context = viewGroup.context
return PluginViewHolder(
LayoutInflater.from(context).inflate(layoutRes, viewGroup, false)
)
}
override fun getItemCount() = pluginList.size
override fun onBindViewHolder(holder: PluginViewHolder, position: Int) {
pluginList[position].let { plugin ->
holder.pluginTitle.text = plugin.header
holder.pluginIcon?.setImageDrawable(plugin.icon)
// Remove context when we require API 23+
plugin.textStyleRes?.let { holder.pluginTitle.setTextAppearance(context, it) }
plugin.action?.let { action -> holder.itemView.setOnClickListener { action.invoke() } }
}
}
class PluginViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val pluginTitle: TextView = view.findViewById(R.id.list_item_entry_title) ?: view as TextView
val pluginIcon: ImageView? = view.findViewById(R.id.list_item_entry_icon)
}
}

View File

@@ -1,29 +0,0 @@
package org.kde.kdeconnect.UserInterface.List
import android.content.Context
import android.graphics.drawable.Drawable
import org.kde.kdeconnect.Plugins.Plugin
class PluginItem(
val context: Context,
val header: String,
val textStyleRes: Int? = null,
) {
var action: (() -> Unit)? = null
var icon: Drawable? = null
constructor(
context: Context,
plugin: Plugin,
action: (Plugin) -> Unit,
textStyleRes: Int? = null,
) : this(
context = context,
header = plugin.actionName,
textStyleRes = textStyleRes,
) {
this.action = { action(plugin) }
this.icon = plugin.icon
}
}

View File

@@ -44,7 +44,7 @@ private const val STATE_SELECTED_DEVICE = "selected_device" //Saved persistently
class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val mNavigationView: NavigationView by lazy { binding.navigationDrawer }
private val mDrawerLayout: DrawerLayout? by lazy { binding.drawerLayout }
private var mDrawerLayout: DrawerLayout? = null
private lateinit var mNavViewDeviceName: TextView
@@ -76,7 +76,9 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
super.onCreate(savedInstanceState)
DeviceHelper.initializeDeviceId(this)
setContentView(binding.root)
val root = binding.root
setContentView(root)
mDrawerLayout = if (root is DrawerLayout) root else null
val mDrawerHeader = mNavigationView.getHeaderView(0)
mNavViewDeviceName = mDrawerHeader.findViewById(R.id.device_name)

View File

@@ -0,0 +1,28 @@
package org.kde.kdeconnect.UserInterface.compose
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
@Composable
fun AppTheme(
content: @Composable () -> Unit) {
val darkTheme = isSystemInDarkTheme()
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}