refactor: rewrite logsender-api and logsender-sample in Java to avoid Kotlin dependencies

This commit is contained in:
Akash Yadav
2023-04-12 09:33:11 +05:30
parent e52e62136e
commit 49f78d876c
13 changed files with 392 additions and 301 deletions
@@ -99,10 +99,10 @@ import com.itsaky.androidide.viewmodel.EditorViewModel
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
import com.itsaky.androidide.xml.widgets.WidgetTableRegistry
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
import java.io.File
import java.util.Objects
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
/**
* Base class for EditorActivity which handles most of the view related things.
@@ -34,8 +34,7 @@ class LogReceiverService : Service() {
private val binder = LogReceiverImpl()
companion object {
@JvmStatic
internal val LOOKUP_KEY = Lookup.Key<LogReceiverService>()
@JvmStatic internal val LOOKUP_KEY = Lookup.Key<LogReceiverService>()
}
override fun onCreate() {
@@ -60,6 +59,6 @@ class LogReceiverService : Service() {
}
}
fun lookupLogService() : LogReceiverService? {
fun lookupLogService(): LogReceiverService? {
return Lookup.DEFAULT.lookup(LogReceiverService.LOOKUP_KEY)
}
}
-2
View File
@@ -17,7 +17,6 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
@@ -28,7 +27,6 @@ android {
dependencies {
implementation(projects.logger)
implementation(libs.androidx.ktx)
implementation(libs.google.material)
implementation(libs.androidx.appcompat)
@@ -0,0 +1,158 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender;
import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.logsender.utils.LogReader;
import com.itsaky.androidide.logsender.utils.Logger;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Entry point for LogSender.
*
* @author Akash Yadav
*/
public class LogSender extends ILogSender.Stub implements ServiceConnection {
@NonNull
private final AtomicBoolean installed = new AtomicBoolean(false);
@Nullable
private LogReader reader;
@Nullable
private ILogReceiver receiver;
@Nullable
private String packageName;
public static final LogSender INSTANCE = new LogSender();
/**
* Action for the LogSender service.
*/
public static final String SERVICE_ACTION = "com.itsaky.androidide.LOG_SERVICE_ACTION";
/**
* Constant used to indicate that the package name of the application cannot be determined.
*/
public static final String PACKAGE_UNKNOWN = "<unknown-package-name>";
/**
* AndroidIDE's package name.
*/
private static final String PACKAGE_ANDROIDIDE = "com.itsaky.androidide";
private LogSender() {
}
public static void install(Application app) {
INSTANCE.doInstall(app);
}
private void doInstall(Application app) {
if (installed.get()) {
Logger.warn("LogSender is already installed");
return;
}
this.packageName = app.getPackageName();
if (PACKAGE_ANDROIDIDE.equals(packageName)) {
return;
}
final var intent = new Intent(SERVICE_ACTION);
intent.setPackage(PACKAGE_ANDROIDIDE);
if (app.bindService(intent, this, 0)) {
Logger.info("Binding to log receiver");
installed.set(true);
} else {
Logger.error("Failed to bind to log receiver");
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
this.receiver = ILogReceiver.Stub.asInterface(service);
if (this.receiver == null) {
throw new IllegalStateException("Failed to get log receiver instance");
}
try {
this.receiver.connect(this);
Logger.info("Connecting to log receiver");
} catch (RemoteException e) {
Logger.error("Failed to connect to log receiver", e);
throw new RuntimeException(e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Logger.info("Disconnected from log receiver");
if (this.reader != null) {
this.reader.interrupt();
}
this.receiver = null;
this.reader = null;
this.installed.set(false);
}
@Override
public void startReader() {
if (reader != null && reader.isAlive()) {
Logger.warn("LogReader has already been started");
return;
}
reader = new LogReader(this::doLog);
reader.start();
}
@Override
public int getPid() {
return Process.myPid();
}
@Override
public String getPackageName() {
if (this.packageName == null) {
return PACKAGE_UNKNOWN;
}
return this.packageName;
}
private void doLog(String line) {
try {
if (line != null && receiver != null) {
receiver.onLog(line);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@@ -1,105 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.Process
import com.itsaky.androidide.logsender.utils.LogReader
import com.itsaky.androidide.logsender.utils.error
import com.itsaky.androidide.logsender.utils.info
import com.itsaky.androidide.logsender.utils.warn
import java.util.concurrent.atomic.AtomicBoolean
/**
* Entry point for LogSender.
*
* @author Akash Yadav
*/
object LogSender : ILogSender.Stub(), ServiceConnection {
private var installed = AtomicBoolean(false)
private var reader: LogReader? = null
private var receiver: ILogReceiver? = null
private var packageName: String? = null
/** Action for the LogSender service. */
const val SERVICE_ACTION = "com.itsaky.androidide.LOG_SERVICE_ACTION"
/** Constant used to indicate that the package name of the application cannot be determined. */
const val PACKAGE_UNKNOWN = "<unknown-package-name>"
/** AndroidIDE's package name. */
private const val PACKAGE_ANDROIDIDE = "com.itsaky.androidide"
fun install(app: Application) {
check(!installed.getAndSet(true)) { "LogSender has already been installed" }
this.packageName = app.packageName
if (PACKAGE_ANDROIDIDE == this.packageName) {
return
}
Intent(SERVICE_ACTION).setPackage(PACKAGE_ANDROIDIDE).let {
if (app.bindService(it, this, 0)) {
info("Binding to AndroidIDE's LogReceiver service")
} else {
error("Failed to bind to log receiver service")
}
}
installed.set(true)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
this.receiver = ILogReceiver.Stub.asInterface(service)
checkNotNull(this.receiver) { "Failed to get receiver instance" }
this.receiver!!.connect(this)
info("Connected to AndroidIDE's LogRecevier")
}
override fun onServiceDisconnected(name: ComponentName?) {
info("Disconnected from AndroidIDE's LogRecevier")
this.installed.set(false)
this.reader?.interrupt()
this.receiver = null
this.reader = null
}
override fun startReader() {
if (reader?.isAlive == true) {
warn("LogReader has already been started")
return
}
reader = LogReader { receiver?.onLog(it) }
reader?.start()
}
override fun getPid(): Int {
return Process.myPid()
}
override fun getPackageName(): String {
return packageName ?: PACKAGE_UNKNOWN
}
}
@@ -0,0 +1,69 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.utils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.Buffer;
import java.util.function.Consumer;
/**
* Reads application logs with `logcat`.
*
* @author Akash Yadav
*/
public class LogReader extends Thread {
private final ProcessBuilder processBuilder;
private final Consumer<String> logConsumer;
public LogReader(Consumer<String> logConsumer) {
this(defaultCmd(), logConsumer);
}
public LogReader(String[] cmd, Consumer<String> logConsumer) {
super("AndroidIDE-LogReader");
this.logConsumer = logConsumer;
this.processBuilder = new ProcessBuilder(cmd);
this.processBuilder.redirectErrorStream(true);
}
public static String[] defaultCmd() {
return new String[] { "logcat", "-v", "threadtime" };
}
@Override
public void run() {
Logger.info("Starting to read logs...");
try {
final var process = processBuilder.start();
try (final var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (logConsumer != null) {
logConsumer.accept(line);
}
}
}
} catch (Throwable err) {
Logger.error("An error occured while reading logs", err);
}
}
}
@@ -1,48 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.utils
/**
* Reads application logs with `logcat`.
*
* @author Akash Yadav
*/
class LogReader(command: Array<String> = defaultCmd(), private val consume: (String) -> Unit) : Thread("AndroidIDE-LogReader") {
private val process = buildProcess(command)
override fun run() {
info("Starting to read logs...")
try {
val process = process.start()
process.inputStream.bufferedReader().forEachLine(consume)
} catch (err: Throwable) {
error("Failed to start logcat process", err)
}
}
private fun buildProcess(command: Array<String>): ProcessBuilder {
return ProcessBuilder(*command).apply {
this.redirectErrorStream(true)
}
}
}
fun defaultCmd(): Array<String> {
return arrayOf("logcat", "-v", "threadtime")
}
@@ -0,0 +1,81 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.utils;
import android.app.Application;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.logsender.LogSender;
/**
* Content providers are loaded before the application class is created. {@link LogSenderInstaller}
* is used to install {@link LogSender} on application start.
* <p>
* {@link LogSenderInstaller} automatically sets up the LogSender code that runs in the main app
* process.
*
* @author Akash Yadav
* @see <a
* href="https://github.com/square/leakcanary/blob/main/leakcanary-object-watcher-android/src/main/java/leakcanary/internal/MainProcessAppWatcherInstaller.kt">MainProcessAppWatcherInstaller</a>
*/
public class LogSenderInstaller extends ContentProvider {
@Override
public boolean onCreate() {
final var application = ((Application) getContext());
LogSender.install(application);
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder
) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs
) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs
) {
return 0;
}
}
@@ -1,75 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.utils
import android.app.Application
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import com.itsaky.androidide.logsender.LogSender
/**
* Content providers are loaded before the application class is created. [LogSenderInstaller] is
* used to install [LogSender] on application start.
*
* [LogSenderInstaller] automatically sets up the LogSender code that runs in the main app process.
*
* @author Akash Yadav
* @see <a
* href="https://github.com/square/leakcanary/blob/main/leakcanary-object-watcher-android/src/main/java/leakcanary/internal/MainProcessAppWatcherInstaller.kt">MainProcessAppWatcherInstaller</a>
*/
internal class LogSenderInstaller : ContentProvider() {
override fun onCreate(): Boolean {
val app = context!!.applicationContext as Application
LogSender.install(app)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}
}
@@ -15,14 +15,31 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.utils
package com.itsaky.androidide.logsender.utils;
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.ILogger;
private val log = ILogger.newInstance("LogSender")
/**
* Static methods for logging messages.
*
* @author Akash Yadav
*/
public class Logger {
private static final ILogger LOG = ILogger.newInstance("LogSender");
fun error(vararg messages: Any?) = log.error(*messages)
fun warn(vararg messages: Any?) = log.warn(*messages)
fun info(vararg messages: Any?) = log.info(*messages)
fun debug(vararg messages: Any?) = log.debug(*messages)
fun verbose(vararg messages: Any?) = log.verbose(*messages)
public static void error(Object... messages) {
LOG.error(messages);
}
public static void warn(Object... messages) {
LOG.warn(messages);
}
public static void info(Object... messages) {
LOG.info(messages);
}
public static void debug(Object... messages) {
LOG.debug(messages);
}
}
-2
View File
@@ -17,7 +17,6 @@
plugins {
id("com.android.application")
id("kotlin-android")
}
android {
@@ -34,7 +33,6 @@ dependencies {
implementation(projects.logsenderApi)
implementation(libs.androidx.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
implementation(libs.google.material)
@@ -0,0 +1,54 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.sample;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PersistableBundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import java.util.concurrent.atomic.AtomicInteger;
public class MainActivity extends AppCompatActivity {
private final AtomicInteger count = new AtomicInteger(0);
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable logger = new Runnable() {
@Override
public void run() {
Log.d("LogSenderSample", "Log #" + count.getAndAdd(1));
if (count.get() == 20) {
return;
}
handler.postDelayed(this, 1000);
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState
) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.postDelayed(logger, 1000);
}
}
@@ -1,55 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.logsender.sample
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import com.itsaky.androidide.logsender.sample.databinding.ActivityMainBinding
import java.util.concurrent.atomic.AtomicInteger
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val count = AtomicInteger(0)
private val handler = Handler(Looper.getMainLooper())
private val logger =
object : Runnable {
override fun run() {
Log.d("LogSenderSample", "Log #${count.getAndAdd(1)}")
if (count.get() == 20) {
return
}
handler.postDelayed(this, 1000)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
handler.postDelayed(logger, 1000)
}
}