Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions app/src/main/java/com/camerainterceptor/ui/LogEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.camerainterceptor.ui;

import java.util.Objects;

/**
* Model class representing a single log entry.
* Immutable data class for use with RecyclerView DiffUtil.
*/
public class LogEntry {

public enum Level {
ERROR("ERROR"),
WARN("WARN"),
INFO("INFO"),
DEBUG("DEBUG"),
VERBOSE("VERBOSE"),
UNKNOWN("");

private final String tag;

Level(String tag) {
this.tag = tag;
}

public String getTag() {
return tag;
}

public static Level fromString(String levelStr) {
if (levelStr == null) return UNKNOWN;
String upper = levelStr.toUpperCase().trim();
for (Level level : values()) {
if (level.tag.equals(upper) || upper.startsWith(level.tag)) {
return level;
}
}
// Also check single letters
if (upper.equals("E")) return ERROR;
if (upper.equals("W")) return WARN;
if (upper.equals("I")) return INFO;
if (upper.equals("D")) return DEBUG;
if (upper.equals("V")) return VERBOSE;
return UNKNOWN;
}
}

private final long id;
private final String timestamp;
private final Level level;
private final String tag;
private final String message;
private final String rawLine;

public LogEntry(long id, String timestamp, Level level, String tag, String message, String rawLine) {
this.id = id;
this.timestamp = timestamp;
this.level = level;
this.tag = tag;
this.message = message;
this.rawLine = rawLine;
}

public long getId() {
return id;
}

public String getTimestamp() {
return timestamp;
}

public Level getLevel() {
return level;
}

public String getTag() {
return tag;
}

public String getMessage() {
return message;
}

public String getRawLine() {
return rawLine;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LogEntry logEntry = (LogEntry) o;
return id == logEntry.id &&
Objects.equals(timestamp, logEntry.timestamp) &&
level == logEntry.level &&
Objects.equals(tag, logEntry.tag) &&
Objects.equals(message, logEntry.message);
}

@Override
public int hashCode() {
return Objects.hash(id, timestamp, level, tag, message);
}

/**
* Parse a log line into a LogEntry object.
* Expected format: "TIMESTAMP [LEVEL] TAG: MESSAGE" or "TIMESTAMP LEVEL: MESSAGE"
* Falls back to raw line if parsing fails.
*/
public static LogEntry parse(long id, String line) {
if (line == null || line.isEmpty()) {
return new LogEntry(id, "", Level.UNKNOWN, "", "", line != null ? line : "");
}

String timestamp = "";
Level level = Level.UNKNOWN;
String tag = "";
String message = line;

try {
// Try to extract timestamp (format: YYYY-MM-DD HH:MM:SS or similar)
int firstSpace = line.indexOf(' ');
if (firstSpace > 0) {
// Check if starts with date-like pattern
String potentialDate = line.substring(0, Math.min(19, line.length()));
if (potentialDate.matches("^\\d{4}-\\d{2}-\\d{2}.*") ||
potentialDate.matches("^\\d{2}:\\d{2}:\\d{2}.*")) {
// Find end of timestamp (usually after second space for date+time)
int secondSpace = line.indexOf(' ', firstSpace + 1);
if (secondSpace > 0 && secondSpace < 25) {
timestamp = line.substring(0, secondSpace).trim();
message = line.substring(secondSpace + 1).trim();
} else {
timestamp = line.substring(0, firstSpace).trim();
message = line.substring(firstSpace + 1).trim();
}
}
}

// Try to extract level [LEVEL] or LEVEL:
String remaining = message;

// Check for [LEVEL] format
if (remaining.startsWith("[")) {
int closeBracket = remaining.indexOf(']');
if (closeBracket > 0 && closeBracket < 10) {
String levelStr = remaining.substring(1, closeBracket);
level = Level.fromString(levelStr);
remaining = remaining.substring(closeBracket + 1).trim();
}
} else {
// Check for LEVEL: format or LEVEL/ format (logcat style)
String[] levelPrefixes = {"ERROR:", "WARN:", "INFO:", "DEBUG:", "VERBOSE:",
"E:", "W:", "I:", "D:", "V:",
"ERROR/", "WARN/", "INFO/", "DEBUG/", "VERBOSE/",
"E/", "W/", "I/", "D/", "V/"};
for (String prefix : levelPrefixes) {
if (remaining.toUpperCase().startsWith(prefix)) {
level = Level.fromString(prefix.replace(":", "").replace("/", ""));
remaining = remaining.substring(prefix.length()).trim();
break;
}
}
}

// Try to extract tag (format: TAG: message)
int colonIndex = remaining.indexOf(':');
if (colonIndex > 0 && colonIndex < 50) {
String potentialTag = remaining.substring(0, colonIndex).trim();
// Tags usually don't contain spaces
if (!potentialTag.contains(" ")) {
tag = potentialTag;
remaining = remaining.substring(colonIndex + 1).trim();
}
}

message = remaining;

} catch (Exception e) {
// If parsing fails, keep the raw line
message = line;
}

return new LogEntry(id, timestamp, level, tag, message, line);
}
}
133 changes: 133 additions & 0 deletions app/src/main/java/com/camerainterceptor/ui/LogEntryAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.camerainterceptor.ui;

import android.graphics.drawable.GradientDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;

import com.camerainterceptor.R;

/**
* RecyclerView Adapter for log entries with syntax highlighting by log level.
* Uses ListAdapter with DiffUtil for efficient updates.
*/
public class LogEntryAdapter extends ListAdapter<LogEntry, LogEntryAdapter.LogViewHolder> {

private static final DiffUtil.ItemCallback<LogEntry> DIFF_CALLBACK = new DiffUtil.ItemCallback<LogEntry>() {
@Override
public boolean areItemsTheSame(@NonNull LogEntry oldItem, @NonNull LogEntry newItem) {
return oldItem.getId() == newItem.getId();
}

@Override
public boolean areContentsTheSame(@NonNull LogEntry oldItem, @NonNull LogEntry newItem) {
return oldItem.equals(newItem);
}
};

public LogEntryAdapter() {
super(DIFF_CALLBACK);
}

@NonNull
@Override
public LogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_log_entry, parent, false);
return new LogViewHolder(view);
}

@Override
public void onBindViewHolder(@NonNull LogViewHolder holder, int position) {
holder.bind(getItem(position));
}

static class LogViewHolder extends RecyclerView.ViewHolder {
private final View levelIndicator;
private final TextView textTimestamp;
private final TextView textLevel;
private final TextView textMessage;
private final TextView textTag;

LogViewHolder(@NonNull View itemView) {
super(itemView);
levelIndicator = itemView.findViewById(R.id.level_indicator);
textTimestamp = itemView.findViewById(R.id.text_timestamp);
textLevel = itemView.findViewById(R.id.text_level);
textMessage = itemView.findViewById(R.id.text_message);
textTag = itemView.findViewById(R.id.text_tag);
}

void bind(LogEntry entry) {
int levelColor = getLevelColor(entry.getLevel());

// Set level indicator color
levelIndicator.setBackgroundColor(levelColor);

// Set timestamp
if (entry.getTimestamp() != null && !entry.getTimestamp().isEmpty()) {
textTimestamp.setVisibility(View.VISIBLE);
textTimestamp.setText(entry.getTimestamp());
} else {
textTimestamp.setVisibility(View.GONE);
}

// Set level badge
textLevel.setText(entry.getLevel().getTag());
GradientDrawable background = (GradientDrawable) ContextCompat.getDrawable(
itemView.getContext(), R.drawable.bg_badge);
if (background != null) {
background = (GradientDrawable) background.mutate();
background.setColor(levelColor);
textLevel.setBackground(background);
}

// Set message
String message = entry.getMessage();
if (message == null || message.isEmpty()) {
message = entry.getRawLine();
}
textMessage.setText(message);

// Set tag
if (entry.getTag() != null && !entry.getTag().isEmpty()) {
textTag.setVisibility(View.VISIBLE);
textTag.setText(entry.getTag());
} else {
textTag.setVisibility(View.GONE);
}
}

private int getLevelColor(LogEntry.Level level) {
int colorRes;
switch (level) {
case ERROR:
colorRes = R.color.log_error;
break;
case WARN:
colorRes = R.color.log_warn;
break;
case INFO:
colorRes = R.color.log_info;
break;
case DEBUG:
colorRes = R.color.log_debug;
break;
case VERBOSE:
colorRes = R.color.log_verbose;
break;
default:
colorRes = R.color.log_debug;
break;
}
return ContextCompat.getColor(itemView.getContext(), colorRes);
}
}
}
Loading
Loading