2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00

Display battery status at the bottom of each device on its DeviceFragment

kdeconnect-android!105
This commit is contained in:
Philip Cohn-Cort 2021-09-13 00:09:43 +00:00
parent 0f4aaa286d
commit 5940e957a8
8 changed files with 326 additions and 118 deletions

View File

@ -1,126 +1,68 @@
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment"> tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment">
<LinearLayout <!-- Layout shown when device is reachable but not yet paired -->
android:id="@+id/pairing_buttons" <include
android:layout_width="match_parent" android:id="@+id/pair_request"
android:layout_height="wrap_content" layout="@layout/view_pair_request"
android:layout_gravity="center" tools:visibility="gone"/>
android:orientation="vertical"
android:padding="@dimen/activity_vertical_margin"
android:visibility="gone">
<ProgressBar
android:id="@+id/pair_progress"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"/>
<TextView
android:id="@+id/pair_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dip"
android:text="@string/device_not_paired"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<TextView
android:id="@+id/pair_verification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="5dp"
android:drawableLeft="@drawable/ic_key"
android:drawableStart="@drawable/ic_key"
android:layout_marginBottom="8dip"
android:visibility="gone"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium"/>
<Button
android:id="@+id/pair_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_round"
android:text="@string/request_pairing"
android:textColor="@android:color/white"/>
<LinearLayout
android:id="@+id/pair_request_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone">
<Button
android:id="@+id/accept_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="4dip"
android:layout_weight="1"
android:background="@drawable/button_round"
android:text="@string/pairing_accept"
android:textColor="@android:color/white"/>
<Button
android:id="@+id/reject_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="4dip"
android:layout_weight="1"
android:background="@drawable/button_round"
android:text="@string/pairing_reject"
android:textColor="@android:color/white"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
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">
<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:paddingLeft="0dip"
android:paddingRight="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"
android:visibility="gone" />
</LinearLayout>
<!-- 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"/>
<!-- Layout shown when device is paired and reachable -->
<ListView <ListView
android:id="@+id/buttons_list" android:id="@+id/buttons_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:fillViewport="true" android:fillViewport="true"
tools:context=".DeviceActivity" tools:context=".DeviceActivity"
tools:listitem="@layout/list_item_with_icon_entry" tools:listitem="@layout/list_item_with_icon_entry"
android:layout_weight=".8"
android:divider="@null" android:divider="@null"
android:dividerHeight="0dp" /> android:dividerHeight="0dp" />
<!-- Extra information about the current device -->
<RelativeLayout
android:id="@+id/view_status_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:padding="8dp"
android:visibility="gone"
tools:visibility="visible"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/view_status_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:padding="4dp"
android:text="@string/view_status_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
/>
<CheckedTextView
android:id="@+id/view_battery_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/view_status_title"
android:checkMark="@null"
android:clickable="false"
android:padding="4dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/battery_status_unknown"
tools:text="100%"
/>
</RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@ -3,6 +3,7 @@
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
@ -40,7 +41,9 @@
android:fadingEdge="horizontal" android:fadingEdge="horizontal"
android:singleLine="true" android:singleLine="true"
android:text="" android:text=""
android:textAppearance="?android:attr/textAppearanceMedium"/> android:textAppearance="?android:attr/textAppearanceMedium"
tools:maxLength="20"
tools:text="@tools:sample/lorem/random"/>
<TextView <TextView
android:id="@+id/list_item_entry_summary" android:id="@+id/list_item_entry_summary"
@ -52,8 +55,10 @@
android:text="" android:text=""
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#CC2222" android:textColor="#CC2222"
android:visibility="gone"/> android:visibility="gone"
tools:text="Other (optional) info"
tools:visibility="visible"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:paddingLeft="0dip"
android:paddingRight="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"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/pairing_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:padding="@dimen/activity_vertical_margin"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
android:id="@+id/pair_progress"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<TextView
android:id="@+id/pair_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dip"
android:text="@string/device_not_paired"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/pair_verification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="5dp"
android:drawableLeft="@drawable/ic_key"
android:drawableStart="@drawable/ic_key"
android:layout_marginBottom="8dip"
android:visibility="gone"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium" />
<Button
android:id="@+id/pair_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_round"
android:text="@string/request_pairing"
android:textColor="@android:color/white" />
<LinearLayout
android:id="@+id/pair_request_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone">
<Button
android:id="@+id/accept_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="4dip"
android:layout_weight="1"
android:background="@drawable/button_round"
android:text="@string/pairing_accept"
android:textColor="@android:color/white" />
<Button
android:id="@+id/reject_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="4dip"
android:layout_weight="1"
android:background="@drawable/button_round"
android:text="@string/pairing_reject"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>

View File

@ -135,6 +135,13 @@
<string name="mouse_receiver_plugin_description">Receive remote mouse movement</string> <string name="mouse_receiver_plugin_description">Receive remote mouse movement</string>
<string name="mouse_receiver_plugin_name">Mouse receiver</string> <string name="mouse_receiver_plugin_name">Mouse receiver</string>
<string name="mouse_receiver_no_permissions">You need to enable Accessibility Service</string> <string name="mouse_receiver_no_permissions">You need to enable Accessibility Service</string>
<string name="view_status_title">Status</string>
<string name="battery_status_format">Battery: %d%%</string>
<string name="battery_status_low_format">Battery: %d%% Low Battery</string>
<string name="battery_status_charging_format">Battery: %d%% charging</string>
<string name="battery_status_unknown">Battery information not available</string>
<string name="category_connected_devices">Connected devices</string> <string name="category_connected_devices">Connected devices</string>
<string name="category_not_paired_devices">Available devices</string> <string name="category_not_paired_devices">Available devices</string>
<string name="category_remembered_devices">Remembered devices</string> <string name="category_remembered_devices">Remembered devices</string>

View File

@ -11,6 +11,8 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.os.BatteryManager; import android.os.BatteryManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
@ -27,8 +29,15 @@ public class BatteryPlugin extends Plugin {
private static final int THRESHOLD_EVENT_NONE = 0; private static final int THRESHOLD_EVENT_NONE = 0;
private static final int THRESHOLD_EVENT_BATTERY_LOW = 1; private static final int THRESHOLD_EVENT_BATTERY_LOW = 1;
public static boolean isLowBattery(@NonNull DeviceBatteryInfo info) {
return info.getThresholdEvent() == THRESHOLD_EVENT_BATTERY_LOW;
}
private final NetworkPacket batteryInfo = new NetworkPacket(PACKET_TYPE_BATTERY); private final NetworkPacket batteryInfo = new NetworkPacket(PACKET_TYPE_BATTERY);
@Nullable
private DeviceBatteryInfo remoteBatteryInfo;
@Override @Override
public String getDisplayName() { public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_battery); return context.getResources().getString(R.string.pref_plugin_battery);
@ -73,6 +82,11 @@ public class BatteryPlugin extends Plugin {
intentFilter.addAction(Intent.ACTION_BATTERY_LOW); intentFilter.addAction(Intent.ACTION_BATTERY_LOW);
Intent currentState = context.registerReceiver(receiver, intentFilter); Intent currentState = context.registerReceiver(receiver, intentFilter);
receiver.onReceive(context, currentState); receiver.onReceive(context, currentState);
// Request new battery info from the linked device
NetworkPacket np = new NetworkPacket(PACKET_TYPE_BATTERY_REQUEST);
np.set("request", true);
device.sendPacket(np);
return true; return true;
} }
@ -89,17 +103,36 @@ public class BatteryPlugin extends Plugin {
device.sendPacket(batteryInfo); device.sendPacket(batteryInfo);
} }
if (PACKET_TYPE_BATTERY.equals(np.getType())) {
remoteBatteryInfo = new DeviceBatteryInfo(np);
device.onPluginsChanged();
}
return true; return true;
} }
/**
* The latest battery information about the linked device. Will be null if the linked device
* has not sent us any such information yet.
* <p>
* See {@link DeviceBatteryInfo} for info on which fields we expect to find.
* </p>
*
* @return the most recent packet received from the remote device. May be null
*/
@Nullable
public DeviceBatteryInfo getRemoteBatteryInfo() {
return remoteBatteryInfo;
}
@Override @Override
public String[] getSupportedPacketTypes() { public String[] getSupportedPacketTypes() {
return new String[]{PACKET_TYPE_BATTERY_REQUEST}; return new String[]{PACKET_TYPE_BATTERY_REQUEST, PACKET_TYPE_BATTERY};
} }
@Override @Override
public String[] getOutgoingPacketTypes() { public String[] getOutgoingPacketTypes() {
return new String[]{PACKET_TYPE_BATTERY}; return new String[]{PACKET_TYPE_BATTERY_REQUEST, PACKET_TYPE_BATTERY};
} }
} }

View File

@ -0,0 +1,30 @@
package org.kde.kdeconnect.Plugins.BatteryPlugin
import org.kde.kdeconnect.NetworkPacket
/**
* Specialised data representation of the packets received by [BatteryPlugin].
*
* Constants for [thresholdEvent] may be found in [BatteryPlugin].
*
* @param currentCharge the amount of charge in the device's battery
* @param isCharging whether the device is charging
* @param thresholdEvent status classifier (used to indicate low battery, etc.)
* @see BatteryPlugin.isLowBattery
*/
data class DeviceBatteryInfo(
val currentCharge: Int,
val isCharging: Boolean,
val thresholdEvent: Int,
) {
/**
* For use with packets of type [BatteryPlugin.PACKET_TYPE_BATTERY].
*/
constructor(np: NetworkPacket) :
this(
np.getInt("currentCharge"),
np.getBoolean("isCharging"),
np.getInt("thresholdEvent", 0)
)
}

View File

@ -19,6 +19,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -29,6 +30,9 @@ import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.TelephonyHelper; import org.kde.kdeconnect.Helpers.TelephonyHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin;
import org.kde.kdeconnect.Plugins.BatteryPlugin.DeviceBatteryInfo;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.SMSPlugin.SMSPlugin; import org.kde.kdeconnect.Plugins.SMSPlugin.SMSPlugin;
import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem; import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem;
@ -38,6 +42,8 @@ import org.kde.kdeconnect.UserInterface.List.PluginListHeaderItem;
import org.kde.kdeconnect.UserInterface.List.SetDefaultAppPluginListItem; import org.kde.kdeconnect.UserInterface.List.SetDefaultAppPluginListItem;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding; import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding;
import org.kde.kdeconnect_tp.databinding.ViewPairErrorBinding;
import org.kde.kdeconnect_tp.databinding.ViewPairRequestBinding;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -63,7 +69,24 @@ public class DeviceFragment extends Fragment {
private ArrayList<ListAdapter.Item> pluginListItems; private ArrayList<ListAdapter.Item> pluginListItems;
private ActivityDeviceBinding binding; /**
* Top-level ViewBinding for this fragment.
*
* Host for {@link #pluginListItems}.
*/
private ActivityDeviceBinding deviceBinding;
/**
* Not-yet-paired ViewBinding.
*
* Used to start and retry pairing.
*/
private ViewPairRequestBinding binding;
/**
* Cannot-communicate ViewBinding.
*
* Used when the remote device is unreachable.
*/
private ViewPairErrorBinding errorBinding;
public DeviceFragment() { public DeviceFragment() {
} }
@ -99,7 +122,12 @@ public class DeviceFragment extends Fragment {
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
binding = ActivityDeviceBinding.inflate(inflater, container, false); deviceBinding = ActivityDeviceBinding.inflate(inflater, container, false);
// Inner binding for the layout shown when we're not paired yet...
binding = deviceBinding.pairRequest;
// ...and for when pairing doesn't (or can't) work
errorBinding = deviceBinding.pairError;
binding.pairButton.setOnClickListener(v -> { binding.pairButton.setOnClickListener(v -> {
binding.pairButton.setVisibility(View.GONE); binding.pairButton.setVisibility(View.GONE);
@ -148,7 +176,7 @@ public class DeviceFragment extends Fragment {
}); });
return binding.getRoot(); return deviceBinding.getRoot();
} }
String getDeviceId() { return mDeviceId; } String getDeviceId() { return mDeviceId; }
@ -272,8 +300,9 @@ public class DeviceFragment extends Fragment {
boolean reachable = device.isReachable(); boolean reachable = device.isReachable();
binding.pairingButtons.setVisibility(paired ? View.GONE : View.VISIBLE); binding.pairingButtons.setVisibility(paired ? View.GONE : View.VISIBLE);
binding.errorMessageContainer.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); errorBinding.errorMessageContainer.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE);
binding.notReachableMessage.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); errorBinding.notReachableMessage.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE);
deviceBinding.viewStatusContainer.setVisibility((paired && reachable) ? View.VISIBLE : View.GONE);
try { try {
pluginListItems = new ArrayList<>(); pluginListItems = new ArrayList<>();
@ -300,10 +329,12 @@ public class DeviceFragment extends Fragment {
dialog.show(getChildFragmentManager(), null); dialog.show(getChildFragmentManager(), null);
} }
}); });
DeviceFragment.this.displayBatteryInfoIfPossible();
} }
ListAdapter adapter = new ListAdapter(mActivity, pluginListItems); ListAdapter adapter = new ListAdapter(mActivity, pluginListItems);
binding.buttonsList.setAdapter(adapter); deviceBinding.buttonsList.setAdapter(adapter);
mActivity.invalidateOptionsMenu(); mActivity.invalidateOptionsMenu();
@ -373,4 +404,50 @@ public class DeviceFragment extends Fragment {
pluginListItems.add(new FailedPluginListItem(plugin, action)); pluginListItems.add(new FailedPluginListItem(plugin, action));
} }
} }
/**
* This method tries to display battery info for the remote device. Includes
* <ul>
* <li>Current charge as a percentage</li>
* <li>Whether the remote device is low on power</li>
* <li>Whether the remote device is currently charging</li>
* </ul>
* <p>
* This will show a simple message on the view instead if we don't have
* accurate info right now.
* </p>
*/
private void displayBatteryInfoIfPossible() {
boolean canDisplayBatteryInfo = false;
BatteryPlugin batteryPlugin = (BatteryPlugin) device.getLoadedPlugins().get(
Plugin.getPluginKey(BatteryPlugin.class)
);
if (batteryPlugin != null) {
DeviceBatteryInfo info = batteryPlugin.getRemoteBatteryInfo();
if (info != null) {
canDisplayBatteryInfo = true;
Context ctx = deviceBinding.viewBatteryStatus.getContext();
boolean isCharging = info.isCharging();
@StringRes
int resId;
if (isCharging) {
resId = R.string.battery_status_charging_format;
} else if (BatteryPlugin.isLowBattery(info)) {
resId = R.string.battery_status_low_format;
} else {
resId = R.string.battery_status_format;
}
deviceBinding.viewBatteryStatus.setChecked(isCharging);
deviceBinding.viewBatteryStatus.setText(ctx.getString(resId, info.getCurrentCharge()));
}
}
if (!canDisplayBatteryInfo) {
deviceBinding.viewBatteryStatus.setText(R.string.battery_status_unknown);
}
}
} }