2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-30 05:37:43 +00:00

Compute total number of packets sent and received by type

This commit is contained in:
Albert Vaca Cintora 2023-05-27 00:49:38 +02:00
parent 8ea82ff053
commit 1ccf15010e
5 changed files with 269 additions and 1 deletions

View File

@ -538,4 +538,6 @@
<string name="send_clipboard">Send clipboard</string>
<string name="tap_to_execute">Tap to execute</string>
<string name="plugin_stats">Plugin stats</string>
</resources>

View File

@ -529,7 +529,9 @@ public class Device implements BaseLink.PacketReceiver {
}
@Override
public void onPacketReceived(NetworkPacket np) {
public void onPacketReceived(@NonNull NetworkPacket np) {
PacketStats.countReceived(getDeviceId(), np.getType());
if (NetworkPacket.PACKET_TYPE_PAIR.equals(np.getType())) {
@ -692,6 +694,7 @@ public class Device implements BaseLink.PacketReceiver {
} catch (IOException e) {
e.printStackTrace();
}
PacketStats.countSent(getDeviceId(), np.getType(), success);
if (success) break;
}

View File

@ -0,0 +1,163 @@
package org.kde.kdeconnect;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class PacketStats {
private static final long EVENT_KEEP_WINDOW_MILLIS = 24 * 60 * 60 * 1000; // Keep 24 hours of events
private static final long CLEANUP_INTERVAL_MILLIS = EVENT_KEEP_WINDOW_MILLIS/4; // Delete old (>24 hours) events every 6 hours
static class DeviceEvents {
public long createdAtMillis = System.currentTimeMillis();
public HashMap<String, ArrayList<Long>> receivedByType = new HashMap<>();
public HashMap<String, ArrayList<Long>> sentSuccessfulByType = new HashMap<>();
public HashMap<String, ArrayList<Long>> sentFailedByType = new HashMap<>();
static class Counts {
@NonNull String packetType;
int received = 0;
int sentSuccessful = 0;
int sentFailed = 0;
int total = 0;
Counts(@NonNull String packetType) {
this.packetType = packetType;
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
public @NonNull Collection<Counts> getCounts() {
HashMap<String, Counts> countsByType = new HashMap<>();
for (Map.Entry<String, ArrayList<Long>> entry : receivedByType.entrySet()) {
Counts counts = countsByType.computeIfAbsent(entry.getKey(), Counts::new);
counts.received += entry.getValue().size();
counts.total += entry.getValue().size();
}
for (Map.Entry<String, ArrayList<Long>> entry : sentSuccessfulByType.entrySet()) {
Counts counts = countsByType.computeIfAbsent(entry.getKey(), Counts::new);
counts.sentSuccessful += entry.getValue().size();
counts.total += entry.getValue().size();
}
for (Map.Entry<String, ArrayList<Long>> entry : sentFailedByType.entrySet()) {
Counts counts = countsByType.computeIfAbsent(entry.getKey(), Counts::new);
counts.sentFailed += entry.getValue().size();
counts.total += entry.getValue().size();
}
return countsByType.values();
}
}
private final static HashMap<String, DeviceEvents> eventsByDevice = new HashMap<>();
private static long nextCleanup = System.currentTimeMillis() + CLEANUP_INTERVAL_MILLIS;
@RequiresApi(api = Build.VERSION_CODES.N)
public static @NonNull String getStatsForDevice(@NonNull String deviceId) {
cleanupIfNeeded();
DeviceEvents deviceEvents = eventsByDevice.get(deviceId);
if (deviceEvents == null) {
return "";
}
StringBuilder ret = new StringBuilder();
long timeInMillis = System.currentTimeMillis() - deviceEvents.createdAtMillis;
if (timeInMillis > EVENT_KEEP_WINDOW_MILLIS) {
timeInMillis = EVENT_KEEP_WINDOW_MILLIS;
}
long hours = TimeUnit.MILLISECONDS.toHours(timeInMillis);
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeInMillis) % 60;
ret.append("From last ");
ret.append(hours);
ret.append("h ");
ret.append(minutes);
ret.append("m\n\n");
ArrayList<DeviceEvents.Counts> counts = new ArrayList<>(deviceEvents.getCounts());
Collections.sort(counts, (o1, o2) -> Integer.compare(o2.total, o1.total)); // Sort them by total number of events
for (DeviceEvents.Counts count : counts) {
String name = count.packetType;
if (name.startsWith("kdeconnect.")) {
name = name.substring("kdeconnect.".length());
}
ret.append(name);
ret.append("\n• ");
ret.append(count.received);
ret.append(" received\n• ");
ret.append(count.sentSuccessful + count.sentFailed);
ret.append(" sent (");
ret.append(count.sentFailed);
ret.append(" failed)\n");
}
return ret.toString();
}
public static void countReceived(@NonNull String deviceId, @NonNull String packetType) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
return; // computeIfAbsent not present in API < 24
}
eventsByDevice.computeIfAbsent(deviceId, key -> new DeviceEvents()).receivedByType.computeIfAbsent(packetType, key -> new ArrayList<>()).add(System.currentTimeMillis());
cleanupIfNeeded();
}
public static void countSent(@NonNull String deviceId, @NonNull String packetType, boolean success) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
return; // computeIfAbsent not present in API < 24
}
if (success) {
eventsByDevice.computeIfAbsent(deviceId, key -> new DeviceEvents()).sentSuccessfulByType.computeIfAbsent(packetType, key -> new ArrayList<>()).add(System.currentTimeMillis());
} else {
eventsByDevice.computeIfAbsent(deviceId, key ->new DeviceEvents()).sentFailedByType.computeIfAbsent(packetType, key -> new ArrayList<>()).add(System.currentTimeMillis());
}
cleanupIfNeeded();
}
private static void cleanupIfNeeded() {
final long cutoutTimestamp = System.currentTimeMillis() - EVENT_KEEP_WINDOW_MILLIS;
if (System.currentTimeMillis() > nextCleanup) {
Log.i("PacketStats", "Doing periodic cleanup");
for (DeviceEvents de : eventsByDevice.values()) {
removeOldEvents(de.receivedByType, cutoutTimestamp);
removeOldEvents(de.sentFailedByType, cutoutTimestamp);
removeOldEvents(de.sentSuccessfulByType, cutoutTimestamp);
}
nextCleanup = System.currentTimeMillis() + CLEANUP_INTERVAL_MILLIS;
}
}
static void removeOldEvents(HashMap<String, ArrayList<Long>> eventsByType, final long cutoutTimestamp) {
Iterator<Map.Entry<String, ArrayList<Long>>> iterator = eventsByType.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ArrayList<Long>> entry = iterator.next();
ArrayList<Long> events = entry.getValue();
int index = Collections.binarySearch(events, cutoutTimestamp);
if (index < 0) {
index = -(index + 1); // Convert the negative index to insertion point
}
if (index < events.size()) {
events.subList(0, index).clear();
} else {
iterator.remove(); // No element greater than the threshold
}
}
}
}

View File

@ -6,15 +6,20 @@
package org.kde.kdeconnect.UserInterface;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.PacketStats;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
@ -88,6 +93,24 @@ public class PluginSettingsActivity
return super.onOptionsItemSelected(item);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
return false; // PacketStats not working in API < 24
}
menu.add(R.string.plugin_stats).setOnMenuItemClickListener(item -> {
String stats = PacketStats.getStatsForDevice(deviceId);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(PluginSettingsActivity.this);
builder.setTitle(R.string.plugin_stats);
builder.setPositiveButton(R.string.ok, (dialog, which) -> dialog.dismiss());
builder.setMessage(stats);
builder.show();
return true;
});
return true;
}
@Override
public void onStartPluginSettingsFragment(Plugin plugin) {

View File

@ -0,0 +1,77 @@
package org.kde.kdeconnect;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.ArrayList;
import java.util.HashMap;
@RunWith(PowerMockRunner.class)
public class PacketStatsTest {
@Test
public void removeOldEvents_cutoutExists() {
final String key = "kdeconnect.ping";
HashMap<String, ArrayList<Long>> eventsByType = new HashMap<>();
ArrayList<Long> events = new ArrayList<>();
eventsByType.put(key, events);
events.add(10L);
events.add(20L);
events.add(30L);
final long cutout = 20L;
PacketStats.removeOldEvents(eventsByType, cutout);
ArrayList<Long> eventsAfter = eventsByType.get(key);
Assert.assertNotNull(eventsAfter);
Assert.assertEquals(2, eventsAfter.size());
Assert.assertEquals(eventsAfter.get(0).longValue(), 20L);
Assert.assertEquals(eventsAfter.get(1).longValue(), 30L);
}
@Test
public void removeOldEvents_cutoutDoesntExist() {
final String key = "kdeconnect.ping";
HashMap<String, ArrayList<Long>> eventsByType = new HashMap<>();
ArrayList<Long> events = new ArrayList<>();
eventsByType.put(key, events);
events.add(10L);
events.add(20L);
events.add(30L);
final long cutout = 25L;
PacketStats.removeOldEvents(eventsByType, cutout);
ArrayList<Long> eventsAfter = eventsByType.get(key);
Assert.assertNotNull(eventsAfter);
Assert.assertEquals(1, eventsAfter.size());
Assert.assertEquals(eventsAfter.get(0).longValue(), 30L);
}
@Test
public void removeOldEvents_OnlyOldEvents() {
final String key = "kdeconnect.ping";
HashMap<String, ArrayList<Long>> eventsByType = new HashMap<>();
ArrayList<Long> events = new ArrayList<>();
eventsByType.put(key, events);
events.add(10L);
events.add(20L);
final long cutout = 25L;
PacketStats.removeOldEvents(eventsByType, cutout);
ArrayList<Long> eventsAfter = eventsByType.get(key);
Assert.assertNull(eventsAfter);
}
@Test
public void removeOldEvents_OnlyNewEvents() {
final String key = "kdeconnect.ping";
HashMap<String, ArrayList<Long>> eventsByType = new HashMap<>();
ArrayList<Long> events = new ArrayList<>();
eventsByType.put(key, events);
events.add(10L);
final long cutout = 5L;
PacketStats.removeOldEvents(eventsByType, cutout);
ArrayList<Long> eventsAfter = eventsByType.get(key);
Assert.assertNotNull(eventsAfter);
Assert.assertEquals(1, eventsAfter.size());
Assert.assertEquals(eventsAfter.get(0).longValue(), 10L);
}
}