mirror of
https://github.com/Benjamin-Wiegand/Flywheel.git
synced 2026-08-07 14:22:23 -07:00
feat: move LogcatReader instance to service
log recordings can now continue when the debug activity is destroyed
This commit is contained in:
@@ -161,6 +161,11 @@
|
||||
android:exported="false"
|
||||
/>
|
||||
|
||||
<service
|
||||
android:name=".LogService"
|
||||
android:exported="false"
|
||||
/>
|
||||
|
||||
<service
|
||||
android:name=".NotificationService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -41,6 +42,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
@@ -48,14 +50,16 @@ import java.util.function.Supplier;
|
||||
import io.benwiegand.projection.geargrinder.exception.BluetoothConnectionException;
|
||||
import io.benwiegand.projection.geargrinder.logs.LogUiAdapter;
|
||||
import io.benwiegand.projection.geargrinder.logs.LogcatReader;
|
||||
import io.benwiegand.projection.geargrinder.service.GeargrinderServiceConnector;
|
||||
|
||||
public class DebugActivity extends AppCompatActivity {
|
||||
public class DebugActivity extends AppCompatActivity implements GeargrinderServiceConnector.ConnectionListener, LogcatReader.UiLogListener {
|
||||
private static final String TAG = DebugActivity.class.getSimpleName();
|
||||
|
||||
// useful for debugging
|
||||
private static final boolean AUTOSTART_TCP_SERVER = false;
|
||||
|
||||
private LogcatReader logcatReader;
|
||||
private GeargrinderServiceConnector connector;
|
||||
private final LogUiAdapter logUiAdapter = new LogUiAdapter();
|
||||
private boolean autoScroll = true;
|
||||
|
||||
@Override
|
||||
@@ -69,21 +73,12 @@ public class DebugActivity extends AppCompatActivity {
|
||||
return insets;
|
||||
});
|
||||
|
||||
findViewById(R.id.log_marker_button).setOnClickListener(v -> logcatReader.addMarker());
|
||||
findViewById(R.id.log_marker_button).setOnClickListener(v -> getLogcatReader().ifPresent(LogcatReader::addMarker));
|
||||
|
||||
findViewById(R.id.toggle_recording_button).setOnClickListener(v -> {
|
||||
findViewById(R.id.toggle_recording_button).setOnClickListener(v -> getLogcatReader().ifPresent(logcatReader -> {
|
||||
if (logcatReader.isRecording()) {
|
||||
Throwable error = logcatReader.stopRecording();
|
||||
if (error == null) {
|
||||
Toast.makeText(this, "recording stopped", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Log.e(TAG, "error during recording", error);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Log recording error")
|
||||
.setMessage("an error happened during the recording:\n\n" + error.getClass().getSimpleName() + ": " + error.getMessage())
|
||||
.setPositiveButton("close", null)
|
||||
.show();
|
||||
if (error != null) onRecordingError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,16 +92,15 @@ public class DebugActivity extends AppCompatActivity {
|
||||
File logFile = getFilesDir().toPath().resolve(name).toFile();
|
||||
try {
|
||||
logcatReader.startRecording(logFile);
|
||||
Toast.makeText(this, "recording started", Toast.LENGTH_SHORT).show();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "failed to start recording", e);
|
||||
onRecordingError(e);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("cancel", null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
|
||||
});
|
||||
}));
|
||||
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
@@ -116,8 +110,6 @@ public class DebugActivity extends AppCompatActivity {
|
||||
autoScrollSwitch.setOnCheckedChangeListener((v, checked) -> autoScroll = checked);
|
||||
|
||||
RecyclerView logRecyclerView = findViewById(R.id.log_recycler);
|
||||
LogUiAdapter logUiAdapter = new LogUiAdapter();
|
||||
|
||||
logRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
logRecyclerView.setAdapter(logUiAdapter);
|
||||
logRecyclerView.setItemAnimator(null); // does not work with fast-paced logs
|
||||
@@ -130,8 +122,10 @@ public class DebugActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
logcatReader = new LogcatReader(logUiAdapter);
|
||||
logcatReader.start();
|
||||
logUiAdapter.onLog(null, "connecting to log service...");
|
||||
|
||||
connector = new GeargrinderServiceConnector(TAG, this, this);
|
||||
connector.bindLogService(BIND_AUTO_CREATE | BIND_IMPORTANT);
|
||||
|
||||
if (AUTOSTART_TCP_SERVER)
|
||||
startService(new Intent(this, ConnectionService.class)
|
||||
@@ -141,7 +135,56 @@ public class DebugActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
logcatReader.destroy();
|
||||
getLogcatReader().ifPresent(logcatReader -> logcatReader.unregisterUiListener(logUiAdapter));
|
||||
connector.destroy();
|
||||
}
|
||||
|
||||
private Optional<LogcatReader> getLogcatReader() {
|
||||
return connector.getLogBinder()
|
||||
.map(LogService.ServiceBinder::getLogcatReader);
|
||||
}
|
||||
|
||||
public void updateRecordingStatus(boolean recording) {
|
||||
Button toggleRecordingButton = findViewById(R.id.toggle_recording_button);
|
||||
toggleRecordingButton.setText(recording ? "stop recording" : "start recording");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLogServiceConnected(LogService.ServiceBinder binder) {
|
||||
binder.getLogcatReader().registerUiListener(logUiAdapter);
|
||||
binder.getLogcatReader().registerUiListener(this);
|
||||
findViewById(R.id.log_marker_button).setEnabled(true);
|
||||
findViewById(R.id.toggle_recording_button).setEnabled(true);
|
||||
updateRecordingStatus(binder.getLogcatReader().isRecording());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordingStart(File file) {
|
||||
runOnUiThread(() -> {
|
||||
updateRecordingStatus(true);
|
||||
Toast.makeText(this, "recording started", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordingError(Throwable t) {
|
||||
runOnUiThread(() -> {
|
||||
Log.e(TAG, "error during recording", t);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Log recording error")
|
||||
.setMessage("an error happened during the recording:\n\n" + t.getClass().getSimpleName() + ": " + t.getMessage())
|
||||
.setPositiveButton("close", null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordingStop() {
|
||||
runOnUiThread(() -> {
|
||||
updateRecordingStatus(false);
|
||||
Toast.makeText(this, "recording stopped", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.benwiegand.projection.geargrinder;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import io.benwiegand.projection.geargrinder.logs.LogcatReader;
|
||||
|
||||
public class LogService extends Service implements LogcatReader.UiLogListener {
|
||||
|
||||
private final LogcatReader logcatReader = new LogcatReader();
|
||||
private final ServiceBinder binder = new ServiceBinder();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
logcatReader.registerUiListener(this);
|
||||
logcatReader.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
logcatReader.unregisterUiListener(this);
|
||||
logcatReader.destroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordingStart(File file) {
|
||||
startService(new Intent(this, LogService.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordingStop() {
|
||||
stopService(new Intent(this, LogService.class));
|
||||
}
|
||||
|
||||
public class ServiceBinder extends Binder {
|
||||
|
||||
public LogcatReader getLogcatReader() {
|
||||
return logcatReader;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -19,19 +23,18 @@ public class LogcatReader {
|
||||
private static final Pattern LOGCAT_REGEX = Pattern.compile("^(?<date>[0-9]+-[0-9]+) +(?<time>[0-9]+:[0-9]+:[0-9]+.[0-9]+) +(?<pid>[0-9]+) +(?<tid>[0-9]+) +(?<level>[FEWIVD]) +(?<tag>[^:]+): ?(?<msg>.*)$");
|
||||
|
||||
public interface UiLogListener {
|
||||
void onLog(String level, String text);
|
||||
default void onLog(String level, String text) {}
|
||||
default void onRecordingStart(File file) {}
|
||||
default void onRecordingStop() {}
|
||||
default void onRecordingError(Throwable t) {}
|
||||
}
|
||||
|
||||
private final Thread readThread = new Thread(this::readLoop);
|
||||
private boolean alive = true;
|
||||
|
||||
private final UiLogListener uiListener;
|
||||
private final Queue<UiLogListener> uiListeners = new ArrayDeque<>();
|
||||
private Recording activeRecording = null;
|
||||
|
||||
public LogcatReader(UiLogListener uiListener) {
|
||||
this.uiListener = uiListener;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
readThread.start();
|
||||
}
|
||||
@@ -41,6 +44,36 @@ public class LogcatReader {
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
public void registerUiListener(UiLogListener listener) {
|
||||
synchronized (uiListeners) {
|
||||
uiListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterUiListener(UiLogListener listener) {
|
||||
synchronized (uiListeners) {
|
||||
uiListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void callListeners(Consumer<UiLogListener> consumer) {
|
||||
synchronized (uiListeners) {
|
||||
LinkedList<UiLogListener> deadListeners = new LinkedList<>();
|
||||
for (UiLogListener listener : uiListeners) {
|
||||
try {
|
||||
consumer.accept(listener);
|
||||
} catch (Throwable t) {
|
||||
Log.wtf(TAG, "ui listener threw", t);
|
||||
deadListeners.add(listener); // avoid spamming the logs exponentially
|
||||
}
|
||||
}
|
||||
|
||||
if (deadListeners.isEmpty()) return;
|
||||
Log.w(TAG, "removing " + deadListeners.size() + " dead listeners");
|
||||
uiListeners.removeAll(deadListeners);
|
||||
}
|
||||
}
|
||||
|
||||
private void onLine(String rawText) {
|
||||
|
||||
Recording recording = activeRecording;
|
||||
@@ -65,9 +98,10 @@ public class LogcatReader {
|
||||
tag = String.valueOf(tagBuilder);
|
||||
}
|
||||
|
||||
uiListener.onLog(level, time + " " + tag + " " + level + ": " + msg);
|
||||
String text = time + " " + tag + " " + level + ": " + msg;
|
||||
callListeners(l -> l.onLog(level, text));
|
||||
} else {
|
||||
uiListener.onLog(null, rawText);
|
||||
callListeners(l -> l.onLog(null, rawText));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -117,9 +151,11 @@ public class LogcatReader {
|
||||
|
||||
private static class Recording {
|
||||
private final Writer writer;
|
||||
private final Consumer<Throwable> onError;
|
||||
private Throwable error = null;
|
||||
private Recording(Writer writer) {
|
||||
private Recording(Writer writer, Consumer<Throwable> onError) {
|
||||
this.writer = writer;
|
||||
this.onError = onError;
|
||||
}
|
||||
|
||||
private void onLine(String line) {
|
||||
@@ -131,6 +167,7 @@ public class LogcatReader {
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "failed to write line", t);
|
||||
error = t;
|
||||
onError.accept(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +176,14 @@ public class LogcatReader {
|
||||
|
||||
public void startRecording(File file) throws IOException {
|
||||
if (activeRecording != null) throw new IllegalStateException("recording already active");
|
||||
activeRecording = new Recording(new FileWriter(file));
|
||||
|
||||
Recording recording = new Recording(
|
||||
new FileWriter(file),
|
||||
t -> callListeners(l -> l.onRecordingError(t))
|
||||
);
|
||||
|
||||
callListeners(l -> l.onRecordingStart(file));
|
||||
activeRecording = recording;
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
@@ -149,7 +193,7 @@ public class LogcatReader {
|
||||
public Throwable stopRecording() {
|
||||
if (activeRecording == null) return null;
|
||||
Throwable error = activeRecording.error;
|
||||
if (error != null) {
|
||||
if (error == null) {
|
||||
try {
|
||||
activeRecording.writer.flush();
|
||||
activeRecording.writer.close();
|
||||
@@ -157,6 +201,7 @@ public class LogcatReader {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
callListeners(UiLogListener::onRecordingStop);
|
||||
activeRecording = null;
|
||||
return error;
|
||||
}
|
||||
|
||||
+14
@@ -13,6 +13,7 @@ import java.util.Optional;
|
||||
import io.benwiegand.projection.geargrinder.AccessibilityInputService;
|
||||
import io.benwiegand.projection.geargrinder.ConnectionService;
|
||||
import io.benwiegand.projection.geargrinder.IShizukuUserService;
|
||||
import io.benwiegand.projection.geargrinder.LogService;
|
||||
import io.benwiegand.projection.geargrinder.NotificationService;
|
||||
import io.benwiegand.projection.geargrinder.PackageService;
|
||||
import io.benwiegand.projection.geargrinder.PrivdService;
|
||||
@@ -31,6 +32,7 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
private static final ComponentName CONNECTION_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, ConnectionService.class.getName());
|
||||
private static final ComponentName SHIZUKU_USER_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, ShizukuUserService.class.getName());
|
||||
private static final ComponentName NOTIFICATION_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, NotificationService.class.getName());
|
||||
private static final ComponentName LOG_SERVICE_COMPONENT = new ComponentName(PACKAGE_NAME, LogService.class.getName());
|
||||
|
||||
private static final Shizuku.UserServiceArgs SHIZUKU_ARGS = new Shizuku.UserServiceArgs(SHIZUKU_USER_SERVICE_COMPONENT)
|
||||
.tag("geargrinder-shizuku-service")
|
||||
@@ -53,6 +55,7 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
default void onConnectionServiceConnected(ConnectionService.ServiceBinder binder) {}
|
||||
default void onShizukuUserServiceConnected(IShizukuUserService service) {}
|
||||
default void onNotificationServiceConnected(NotificationService.ServiceBinder binder) {}
|
||||
default void onLogServiceConnected(LogService.ServiceBinder binder) {}
|
||||
}
|
||||
|
||||
public GeargrinderServiceConnector(String tag, Context context, ConnectionListener listener) {
|
||||
@@ -147,6 +150,15 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
.map(b -> (NotificationService.ServiceBinder) b);
|
||||
}
|
||||
|
||||
public void bindLogService(int flags) {
|
||||
realBind(LOG_SERVICE_COMPONENT, flags);
|
||||
}
|
||||
|
||||
public Optional<LogService.ServiceBinder> getLogBinder() {
|
||||
return getBinder(LOG_SERVICE_COMPONENT)
|
||||
.map(b -> (LogService.ServiceBinder) b);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
@@ -166,6 +178,8 @@ public class GeargrinderServiceConnector extends MakeshiftServiceConnection {
|
||||
listener.onShizukuUserServiceConnected(IShizukuUserService.Stub.asInterface(service));
|
||||
} else if (name.equals(NOTIFICATION_SERVICE_COMPONENT)) {
|
||||
listener.onNotificationServiceConnected((NotificationService.ServiceBinder) service);
|
||||
} else if (name.equals(LOG_SERVICE_COMPONENT)) {
|
||||
listener.onLogServiceConnected((LogService.ServiceBinder) service);
|
||||
} else {
|
||||
Log.wtf(tag, "unhandled component: " + name);
|
||||
assert false;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:text="log marker"
|
||||
/>
|
||||
|
||||
@@ -32,7 +33,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:text="log recording"
|
||||
android:enabled="false"
|
||||
android:text="start recording"
|
||||
/>
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
|
||||
Reference in New Issue
Block a user