Skip to content

Commit

Permalink
Merge pull request #26 from jpage4500/feature/10-25
Browse files Browse the repository at this point in the history
- new feature to save device logs to file; option to hide toolbar buttons
  • Loading branch information
jpage4500 authored Oct 31, 2024
2 parents 9ce450b + c594638 commit 5c85bac
Show file tree
Hide file tree
Showing 24 changed files with 781 additions and 80 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Java desktop app to manage multiple Android devices via adb
<br>
<img src="resources/screenshot-logs.jpg" width="600" alt="logs">
<br>
<img src="resources/screenshot-savelogs.jpg" width="600" alt="logs">
<br>
</details>

## Prerequisites
Expand Down
Binary file added resources/screenshot-savelogs.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion src/main/java/com/jpage4500/devicemanager/data/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public enum PowerStatus {

// counter of running tasks like mirroring a device; used to show a 'busy' icon
@ExcludeFromSerialization
public AtomicInteger busyCounter = new AtomicInteger(0);
private final AtomicInteger busyCounter = new AtomicInteger(0);

// last time device was seen (online or offline)
public Long lastUpdateMs;
Expand Down Expand Up @@ -111,6 +111,27 @@ public void setCustomProperty(String key, String value) {
customPropertyMap.put(key, value);
}

public boolean isBusy() {
return busyCounter.get() > 0;
}

/**
* set device to BUSY state
*
* @return true if device is BUSY
*/
public boolean setBusy(boolean isBusy) {
int newValue;
if (isBusy) newValue = busyCounter.incrementAndGet();
else newValue = busyCounter.decrementAndGet();
// safety-check
if (newValue < 0) {
newValue = 0;
busyCounter.set(0);
}
return newValue > 0;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.jpage4500.devicemanager.data;

import com.jpage4500.devicemanager.utils.GsonHelper;
import com.jpage4500.devicemanager.utils.TextUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/jpage4500/devicemanager/data/SaveLogEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jpage4500.devicemanager.data;

import java.io.File;

public class SaveLogEntry {
public Device device;
public long numLines;
public long size;
public File saveFile;

}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class DeviceManager {
private final ScheduledExecutorService scheduledExecutorService;
private ScheduledFuture<?> deviceRefreshRuture;

private final AtomicBoolean isLogging = new AtomicBoolean(false);
private final Map<String, AtomicBoolean> loggingStateMap = new HashMap<>();

private JadbConnection connection;

Expand Down Expand Up @@ -257,7 +257,7 @@ private void fetchDeviceDetails(Device device, boolean fullRefresh, DeviceListen
commandExecutorService.submit(() -> {
Timer timer = new Timer();
// show device as 'busy'
device.busyCounter.incrementAndGet();
device.setBusy(true);
listener.handleDeviceUpdated(device);

// NOTE: if device just restarted, the initial fullRefresh will fail so try again next time
Expand Down Expand Up @@ -309,10 +309,10 @@ private void fetchDeviceDetails(Device device, boolean fullRefresh, DeviceListen
} else {
if (log.isTraceEnabled()) log.trace("fetchDeviceDetails: REFRESH:{}: {}", timer, GsonHelper.toJson(device));
}
int busyCount = device.busyCounter.decrementAndGet();
if (busyCount == 0) listener.handleDeviceUpdated(device);
boolean isBusy = device.setBusy(false);
if (!isBusy) listener.handleDeviceUpdated(device);

// if devicce isn't fully booted yet, schedule another refresh
// if device isn't fully booted yet, schedule another refresh
if (!device.isBooted) {
scheduledExecutorService.schedule(() -> {
log.trace("fetchDeviceDetails: try again for {}", device.getDisplayName());
Expand Down Expand Up @@ -964,11 +964,21 @@ public interface DeviceLogListener {
void handleProcessMap(Map<String, String> processMap);
}

private AtomicBoolean getLoggingState(String serial, boolean createIfNotFound) {
AtomicBoolean loggingState = loggingStateMap.get(serial);
if (loggingState == null && createIfNotFound) {
loggingState = new AtomicBoolean(true);
loggingStateMap.put(serial, loggingState);
}
return loggingState;
}

public void startLogging(Device device, Long startTime, DeviceLogListener listener) {
stopLogging(device);
commandExecutorService.submit(() -> {
log.debug("startLogging: {}", startTime);
isLogging.set(true);
log.debug("startLogging: {}, startTime:{}", device.serial, startTime);
AtomicBoolean loggingState = getLoggingState(device.serial, true);
loggingState.set(true);
InputStream inputStream = null;
try {
String[] args = new String[]{"-v", "threadtime"};
Expand Down Expand Up @@ -998,7 +1008,9 @@ else if (startTime != null && (logEntry.timestamp == null || startTime > logEntr
lastUpdateMs = System.currentTimeMillis();

// check if logging is still running
if (!isLogging.get()) break;
if (!loggingState.get()) {
loggingStateMap.remove(device.serial);
}
}
}
} catch (Exception e) {
Expand All @@ -1020,20 +1032,20 @@ else if (startTime != null && (logEntry.timestamp == null || startTime > logEntr
listener.handleProcessMap(pidMap);
}
// check if logging is still running and schedule next lookup
if (isLogging.get()) {
if (isLogging(device.serial)) {
scheduleNextProcessCheck(device, listener);
}
});
}

private void scheduleNextProcessCheck(Device device, DeviceLogListener listener) {
// make sure logging is still running
if (!isLogging.get()) return;
if (!isLogging(device.serial)) return;

// run in 30 seconds
scheduledExecutorService.schedule(() -> {
// make sure logging is still running
if (!isLogging.get()) return;
if (!isLogging(device.serial)) return;

Map<String, String> pidMap = getProcessMap(device);
listener.handleProcessMap(pidMap);
Expand Down Expand Up @@ -1061,14 +1073,20 @@ private Map<String, String> getProcessMap(Device device) {
}

public void stopLogging(Device device) {
if (isLogging.get()) {
log.debug("stopLogging: ");
isLogging.set(false);
AtomicBoolean loggingState = getLoggingState(device.serial, false);
if (loggingState != null && loggingState.get()) {
log.debug("stopLogging: {}", device.serial);
loggingState.set(false);
}
}

public boolean isLogging(Device device) {
return isLogging.get();
return isLogging(device.serial);
}

private boolean isLogging(String serial) {
AtomicBoolean loggingState = getLoggingState(serial, false);
return loggingState != null && loggingState.get();
}

public void handleExit() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.jpage4500.devicemanager.table;

import com.jpage4500.devicemanager.data.SaveLogEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.List;

public class SaveLogsTableModel extends AbstractTableModel {
private static final Logger log = LoggerFactory.getLogger(SaveLogsTableModel.class);

private final List<SaveLogEntry> entryList;

public enum Columns {
NAME("Name"),
SERIAL("Serial"),
SIZE("Size"),
;
String desc;

Columns(String desc) {
this.desc = desc;
}

@Override
public String toString() {
return desc;
}
}

public SaveLogsTableModel() {
entryList = new ArrayList<>();
}

public void setEntryList(List<SaveLogEntry> entryList) {
this.entryList.clear();
this.entryList.addAll(entryList);

fireTableDataChanged();
}

public List<SaveLogEntry> getEntryList() {
return entryList;
}

public void notifyEntryUpdated(SaveLogEntry entry) {
int index = entryList.indexOf(entry);
if (index >= 0) {
fireTableRowsUpdated(index, index);
}
}

public int getColumnCount() {
return Columns.values().length;
}

@Override
public Class<?> getColumnClass(int columnIndex) {
return SaveLogEntry.class;
}

public String getColumnName(int i) {
Columns[] columns = Columns.values();
Columns colType = columns[i];
return colType.toString();
}

public int getRowCount() {
return entryList.size();
}

public Object getValueAt(int row, int col) {
if (row >= entryList.size()) return null;
else if (col >= getColumnCount()) return null;

return entryList.get(row);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public Component getTableCellRendererComponent(JTable table, Object object, bool
align = SwingConstants.RIGHT;
break;
case NAME:
if (device.busyCounter.get() > 0) {
if (device.isBusy()) {
icon = statusBusyIcon;
} else if (device.isOnline) {
if (!device.isBooted) icon = statusNotReadyIcon;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.jpage4500.devicemanager.table.utils;

import com.jpage4500.devicemanager.data.SaveLogEntry;
import com.jpage4500.devicemanager.table.SaveLogsTableModel;
import com.jpage4500.devicemanager.utils.Colors;
import com.jpage4500.devicemanager.utils.FileUtils;
import com.jpage4500.devicemanager.utils.UiUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.image.BufferedImage;

public class SaveLogsCellRenderer extends JLabel implements TableCellRenderer {
private static final Logger log = LoggerFactory.getLogger(SaveLogsCellRenderer.class);

private final Icon statusOfflineIcon;
private final Icon statusOnlineIcon;
private final Icon statusBusyIcon;
private final Icon statusNotReadyIcon;

public SaveLogsCellRenderer() {
setOpaque(true);

UiUtils.setEmptyBorder(this, 10, 10);

BufferedImage image = UiUtils.getImage("device_status.png", UiUtils.IMG_SIZE_ICON, UiUtils.IMG_SIZE_ICON);

BufferedImage offlineImage = UiUtils.replaceColor(image, Color.GRAY);
statusOfflineIcon = new ImageIcon(offlineImage);

BufferedImage onlineImage = UiUtils.replaceColor(image, Colors.COLOR_ONLINE);
statusOnlineIcon = new ImageIcon(onlineImage);

BufferedImage busyImage = UiUtils.replaceColor(image, Colors.COLOR_BUSY);
statusBusyIcon = new ImageIcon(busyImage);

BufferedImage notReadyImage = UiUtils.replaceColor(image, Colors.COLOR_NOT_READY);
statusNotReadyIcon = new ImageIcon(notReadyImage);
}

public Component getTableCellRendererComponent(JTable table, Object object, boolean isSelected, boolean hasFocus, int row, int column) {
SaveLogEntry entry = (SaveLogEntry) object;
// convert table column to model column
column = table.convertColumnIndexToModel(column);

SaveLogsTableModel.Columns col = SaveLogsTableModel.Columns.values()[column];
Icon icon = null;
String text = null;
int align = SwingConstants.LEFT;
switch (col) {
case NAME:
text = entry.device.nickname;
if (entry.device.isBusy()) {
icon = statusBusyIcon;
} else if (entry.device.isOnline) {
if (!entry.device.isBooted) icon = statusNotReadyIcon;
else icon = statusOnlineIcon;
} else {
icon = statusOfflineIcon;
}
break;
case SERIAL:
text = entry.device.serial;
break;
case SIZE:
// right-align size column
align = SwingConstants.RIGHT;
if (entry.size > 0) {
text = FileUtils.bytesToDisplayString(entry.size);
} else {
text = "-";
}
break;
}

boolean isTableFocused = table.hasFocus();
Color textColor = isSelected && isTableFocused ? Color.WHITE : Color.BLACK;
Color backgroundColor = isSelected ? table.getSelectionBackground() : table.getBackground();

setForeground(textColor);
setBackground(backgroundColor);

setIcon(icon);
setText(text);
setHorizontalAlignment(align);

return this;
}
}
Loading

0 comments on commit 5c85bac

Please sign in to comment.