diff --git a/android/app/src/main/java/org/musicpd/Main.java b/android/app/src/main/java/org/musicpd/Main.java
deleted file mode 100644
index 5f440b964..000000000
--- a/android/app/src/main/java/org/musicpd/Main.java
+++ /dev/null
@@ -1,338 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// Copyright The Music Player Daemon Project
-
-package org.musicpd;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.media.AudioManager;
-import android.os.Build;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.OptIn;
-import androidx.media3.common.util.UnstableApi;
-import androidx.media3.session.MediaSession;
-
-import org.jetbrains.annotations.NotNull;
-import org.musicpd.data.LoggingRepository;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.util.Objects;
-
-import javax.inject.Inject;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-@AndroidEntryPoint
-public class Main extends Service implements Runnable {
-
-	private static final String TAG = "Main";
-	private static final String WAKELOCK_TAG = "mpd:wakelockmain";
-
-	private static final int MAIN_STATUS_ERROR = -1;
-	private static final int MAIN_STATUS_STOPPED = 0;
-	private static final int MAIN_STATUS_STARTED = 1;
-
-	private static final int MSG_SEND_STATUS = 0;
-
-	private Thread mThread = null;
-	private int mStatus = MAIN_STATUS_STOPPED;
-	private boolean mAbort = false;
-	private String mError = null;
-	private final RemoteCallbackList<IMainCallback> mCallbacks = new RemoteCallbackList<IMainCallback>();
-	private final IBinder mBinder = new MainStub(this);
-	private boolean mPauseOnHeadphonesDisconnect = false;
-	private PowerManager.WakeLock mWakelock = null;
-
-	private MediaSession mMediaSession = null;
-
-	@Inject
-	LoggingRepository logging;
-
-	@NotNull
-	public static final String SHUTDOWN_ACTION = "org.musicpd.action.ShutdownMPD";
-
-	static class MainStub extends IMain.Stub {
-		private Main mService;
-		MainStub(Main service) {
-			mService = service;
-		}
-		public void start() {
-			mService.start();
-		}
-		public void stop() {
-			mService.stop();
-		}
-		public void setPauseOnHeadphonesDisconnect(boolean enabled) {
-			mService.setPauseOnHeadphonesDisconnect(enabled);
-		}
-		public void setWakelockEnabled(boolean enabled) {
-			mService.setWakelockEnabled(enabled);
-		}
-		public boolean isRunning() {
-			return mService.isRunning();
-		}
-		public void registerCallback(IMainCallback cb) {
-			mService.registerCallback(cb);
-		}
-		public void unregisterCallback(IMainCallback cb) {
-			mService.unregisterCallback(cb);
-		}
-	}
-
-	private synchronized void sendMessage(int what, int arg1, int arg2, Object obj) {
-		int i = mCallbacks.beginBroadcast();
-		while (i > 0) {
-			i--;
-			final IMainCallback cb = mCallbacks.getBroadcastItem(i);
-			try {
-				switch (what) {
-				case MSG_SEND_STATUS:
-					switch (arg1) {
-					case MAIN_STATUS_ERROR:
-						cb.onError((String)obj);
-						break;
-					case MAIN_STATUS_STOPPED:
-						cb.onStopped();
-						break;
-					case MAIN_STATUS_STARTED:
-						cb.onStarted();
-						break;
-					}
-					break;
-				}
-			} catch (RemoteException e) {
-			}
-		}
-		mCallbacks.finishBroadcast();
-	}
-
-	private Bridge.LogListener mLogListener = new Bridge.LogListener() {
-		@Override
-		public void onLog(int priority, String msg) {
-			logging.addLogItem(priority, msg);
-		}
-	};
-
-	@Override
-	public IBinder onBind(Intent intent) {
-		return mBinder;
-	}
-
-	@Override
-	public int onStartCommand(Intent intent, int flags, int startId) {
-		if (intent != null && Objects.equals(intent.getAction(), SHUTDOWN_ACTION)) {
-			stop();
-		} else {
-			start();
-			if (intent != null && intent.getBooleanExtra("wakelock", false))
-				setWakelockEnabled(true);
-		}
-		return START_STICKY;
-	}
-
-	@Override
-	public void run() {
-		if (!Loader.loaded) {
-			final String error = "Failed to load the native MPD libary.\n" +
-				"Report this problem to us, and include the following information:\n" +
-				"SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" +
-				"PRODUCT=" + Build.PRODUCT + "\n" +
-				"FINGERPRINT=" + Build.FINGERPRINT + "\n" +
-				"error=" + Loader.error;
-			setStatus(MAIN_STATUS_ERROR, error);
-			stopSelf();
-			return;
-		}
-		synchronized (this) {
-			if (mAbort)
-				return;
-			setStatus(MAIN_STATUS_STARTED, null);
-		}
-		Bridge.run(this, mLogListener);
-		setStatus(MAIN_STATUS_STOPPED, null);
-	}
-
-	private synchronized void setStatus(int status, String error) {
-		mStatus = status;
-		mError = error;
-		sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
-	}
-
-	private Notification.Builder createNotificationBuilderWithChannel() {
-		final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
-		if (notificationManager == null)
-			return null;
-
-		final String id = "org.musicpd";
-		final String name = "MPD service";
-		final int importance = 3; /* NotificationManager.IMPORTANCE_DEFAULT */
-
-		try {
-			Class<?> ncClass = Class.forName("android.app.NotificationChannel");
-			Constructor<?> ncCtor = ncClass.getConstructor(String.class, CharSequence.class, int.class);
-			Object nc = ncCtor.newInstance(id, name, importance);
-
-			Method nmCreateNotificationChannelMethod =
-				NotificationManager.class.getMethod("createNotificationChannel", ncClass);
-			nmCreateNotificationChannelMethod.invoke(notificationManager, nc);
-
-			Constructor nbCtor = Notification.Builder.class.getConstructor(Context.class, String.class);
-			return (Notification.Builder) nbCtor.newInstance(this, id);
-		} catch (Exception e)
-		{
-			Log.e(TAG, "error creating the NotificationChannel", e);
-			return null;
-		}
-	}
-
-	@OptIn(markerClass = UnstableApi.class)
-	private void start() {
-		if (mThread != null)
-			return;
-
-		IntentFilter filter = new IntentFilter();
-		filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
-		registerReceiver(new BroadcastReceiver() {
-			@Override
-			public void onReceive(Context context, Intent intent) {
-				if (!mPauseOnHeadphonesDisconnect)
-					return;
-				if (intent.getAction() == AudioManager.ACTION_AUDIO_BECOMING_NOISY)
-					pause();
-			}
-		}, filter);
-
-		final Intent mainIntent = new Intent(this, MainActivity.class);
-		mainIntent.setAction("android.intent.action.MAIN");
-		mainIntent.addCategory("android.intent.category.LAUNCHER");
-		final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
-				mainIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-
-		Notification.Builder nBuilder;
-		if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */)
-		{
-			nBuilder = createNotificationBuilderWithChannel();
-			if (nBuilder == null)
-				return;
-		}
-		else
-			nBuilder = new Notification.Builder(this);
-
-		Notification notification = nBuilder.setContentTitle(getText(R.string.notification_title_mpd_running))
-			.setContentText(getText(R.string.notification_text_mpd_running))
-			.setSmallIcon(R.drawable.notification_icon)
-			.setContentIntent(contentIntent)
-			.build();
-
-		mThread = new Thread(this);
-		mThread.start();
-
-		MPDPlayer player = new MPDPlayer(Looper.getMainLooper());
-		mMediaSession = new MediaSession.Builder(this, player).build();
-
-		startForeground(R.string.notification_title_mpd_running, notification);
-		startService(new Intent(this, Main.class));
-	}
-
-	private void stop() {
-		mMediaSession.release();
-		mMediaSession = null;
-		if (mThread != null) {
-			if (mThread.isAlive()) {
-				synchronized (this) {
-					if (mStatus == MAIN_STATUS_STARTED)
-						Bridge.shutdown();
-					else
-						mAbort = true;
-				}
-			}
-			try {
-				mThread.join();
-				mThread = null;
-				mAbort = false;
-			} catch (InterruptedException ie) {}
-		}
-		setWakelockEnabled(false);
-		stopForeground(true);
-		stopSelf();
-	}
-
-	private void pause() {
-		if (mThread != null) {
-			if (mThread.isAlive()) {
-				synchronized (this) {
-					if (mStatus == MAIN_STATUS_STARTED)
-						Bridge.pause();
-				}
-			}
-		}
-	}
-
-	private void setPauseOnHeadphonesDisconnect(boolean enabled) {
-		mPauseOnHeadphonesDisconnect = enabled;
-	}
-
-	private void setWakelockEnabled(boolean enabled) {
-		if (enabled && mWakelock == null) {
-			PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
-			mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
-			mWakelock.acquire();
-			Log.d(TAG, "Wakelock acquired");
-		} else if (!enabled && mWakelock != null) {
-			mWakelock.release();
-			mWakelock = null;
-			Log.d(TAG, "Wakelock released");
-		}
-	}
-
-	private boolean isRunning() {
-		return mThread != null && mThread.isAlive();
-	}
-
-	private void registerCallback(IMainCallback cb) {
-		if (cb != null) {
-			mCallbacks.register(cb);
-			sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
-		}
-	}
-
-	private void unregisterCallback(IMainCallback cb) {
-		if (cb != null) {
-			mCallbacks.unregister(cb);
-		}
-	}
-
-	/*
-	 * start Main service without any callback
-	 */
-	public static void startService(Context context, boolean wakelock) {
-		Intent intent = new Intent(context, Main.class)
-			.putExtra("wakelock", wakelock);
-
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
-			/* in Android 8+, we need to use this method
-			   or else we'll get "IllegalStateException:
-			   app is in background" */
-			context.startForegroundService(intent);
-		else
-			context.startService(intent);
-	}
-
-	public static void stopService(Context context) {
-		Intent intent = new Intent(context, Main.class);
-		context.stopService(intent);
-	}
-}
diff --git a/android/app/src/main/java/org/musicpd/Main.kt b/android/app/src/main/java/org/musicpd/Main.kt
new file mode 100644
index 000000000..e2fcac4f5
--- /dev/null
+++ b/android/app/src/main/java/org/musicpd/Main.kt
@@ -0,0 +1,329 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Copyright The Music Player Daemon Project
+package org.musicpd
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioManager
+import android.os.Build
+import android.os.IBinder
+import android.os.Looper
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+import android.os.RemoteCallbackList
+import android.os.RemoteException
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.core.app.ServiceCompat
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.MediaSession
+import dagger.hilt.android.AndroidEntryPoint
+import org.musicpd.Bridge.LogListener
+import org.musicpd.data.LoggingRepository
+import java.lang.reflect.Constructor
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class Main : Service(), Runnable {
+    companion object {
+        private const val TAG = "Main"
+        private const val WAKELOCK_TAG = "mpd:wakelockmain"
+
+        private const val MAIN_STATUS_ERROR = -1
+        private const val MAIN_STATUS_STOPPED = 0
+        private const val MAIN_STATUS_STARTED = 1
+
+        private const val MSG_SEND_STATUS = 0
+
+        const val SHUTDOWN_ACTION: String = "org.musicpd.action.ShutdownMPD"
+
+        /*
+	 * start Main service without any callback
+	 */
+        @JvmStatic
+        fun startService(context: Context, wakelock: Boolean) {
+            val intent = Intent(context, Main::class.java)
+                .putExtra("wakelock", wakelock)
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) /* in Android 8+, we need to use this method
+			   or else we'll get "IllegalStateException:
+			   app is in background" */
+                context.startForegroundService(intent)
+            else context.startService(intent)
+        }
+    }
+
+    private var mThread: Thread? = null
+    private var mStatus = MAIN_STATUS_STOPPED
+    private var mAbort = false
+    private var mError: String? = null
+    private val mCallbacks = RemoteCallbackList<IMainCallback>()
+    private val mBinder: IBinder = MainStub(this)
+    private var mPauseOnHeadphonesDisconnect = false
+    private var mWakelock: WakeLock? = null
+
+    private var mMediaSession: MediaSession? = null
+
+    @JvmField
+    @Inject
+    var logging: LoggingRepository? = null
+
+    internal class MainStub(private val mService: Main) : IMain.Stub() {
+        override fun start() {
+            mService.start()
+        }
+
+        override fun stop() {
+            mService.stop()
+        }
+
+        override fun setPauseOnHeadphonesDisconnect(enabled: Boolean) {
+            mService.setPauseOnHeadphonesDisconnect(enabled)
+        }
+
+        override fun setWakelockEnabled(enabled: Boolean) {
+            mService.setWakelockEnabled(enabled)
+        }
+
+        override fun isRunning(): Boolean {
+            return mService.isRunning
+        }
+
+        override fun registerCallback(cb: IMainCallback) {
+            mService.registerCallback(cb)
+        }
+
+        override fun unregisterCallback(cb: IMainCallback) {
+            mService.unregisterCallback(cb)
+        }
+    }
+
+    @Synchronized
+    private fun sendMessage(
+        @Suppress("SameParameterValue") what: Int,
+        arg1: Int,
+        arg2: Int,
+        obj: Any?
+    ) {
+        var i = mCallbacks.beginBroadcast()
+        while (i > 0) {
+            i--
+            val cb = mCallbacks.getBroadcastItem(i)
+            try {
+                when (what) {
+                    MSG_SEND_STATUS -> when (arg1) {
+                        MAIN_STATUS_ERROR -> cb.onError(obj as String?)
+                        MAIN_STATUS_STOPPED -> cb.onStopped()
+                        MAIN_STATUS_STARTED -> cb.onStarted()
+                    }
+                }
+            } catch (ignored: RemoteException) {
+            }
+        }
+        mCallbacks.finishBroadcast()
+    }
+
+    private val mLogListener = LogListener { priority, msg ->
+        logging?.addLogItem(priority, msg)
+    }
+
+    override fun onBind(intent: Intent): IBinder {
+        return mBinder
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (intent?.action == SHUTDOWN_ACTION) {
+            stop()
+        } else {
+            start()
+            if (intent?.getBooleanExtra(
+                    "wakelock",
+                    false
+                ) == true
+            ) setWakelockEnabled(true)
+        }
+        return START_REDELIVER_INTENT
+    }
+
+    override fun run() {
+        if (!Loader.loaded) {
+            val error = """
+                Failed to load the native MPD library.
+                Report this problem to us, and include the following information:
+                SUPPORTED_ABIS=${java.lang.String.join(", ", *Build.SUPPORTED_ABIS)}
+                PRODUCT=${Build.PRODUCT}
+                FINGERPRINT=${Build.FINGERPRINT}
+                error=${Loader.error}
+                """.trimIndent()
+            setStatus(MAIN_STATUS_ERROR, error)
+            stopSelf()
+            return
+        }
+        synchronized(this) {
+            if (mAbort) return
+            setStatus(MAIN_STATUS_STARTED, null)
+        }
+        Bridge.run(this, mLogListener)
+        setStatus(MAIN_STATUS_STOPPED, null)
+    }
+
+    @Synchronized
+    private fun setStatus(status: Int, error: String?) {
+        mStatus = status
+        mError = error
+        sendMessage(MSG_SEND_STATUS, mStatus, 0, mError)
+    }
+
+    private fun createNotificationBuilderWithChannel(): Notification.Builder? {
+        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager
+            ?: return null
+
+        val id = "org.musicpd"
+        val name = "MPD service"
+        val importance = 3 /* NotificationManager.IMPORTANCE_DEFAULT */
+
+        try {
+            val ncClass = Class.forName("android.app.NotificationChannel")
+            val ncCtor = ncClass.getConstructor(
+                String::class.java,
+                CharSequence::class.java,
+                Int::class.javaPrimitiveType
+            )
+            val nc = ncCtor.newInstance(id, name, importance)
+
+            val nmCreateNotificationChannelMethod =
+                NotificationManager::class.java.getMethod("createNotificationChannel", ncClass)
+            nmCreateNotificationChannelMethod.invoke(notificationManager, nc)
+
+            val nbCtor: Constructor<*> = Notification.Builder::class.java.getConstructor(
+                Context::class.java, String::class.java
+            )
+            return nbCtor.newInstance(this, id) as Notification.Builder
+        } catch (e: Exception) {
+            Log.e(TAG, "error creating the NotificationChannel", e)
+            return null
+        }
+    }
+
+    @OptIn(markerClass = [UnstableApi::class])
+    private fun start() {
+        if (mThread != null) return
+
+        val filter = IntentFilter()
+        filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
+        registerReceiver(object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                if (!mPauseOnHeadphonesDisconnect) return
+                if (intent.action === AudioManager.ACTION_AUDIO_BECOMING_NOISY) pause()
+            }
+        }, filter)
+
+        val mainIntent = Intent(this, MainActivity::class.java)
+        mainIntent.setAction("android.intent.action.MAIN")
+        mainIntent.addCategory("android.intent.category.LAUNCHER")
+        val contentIntent = PendingIntent.getActivity(
+            this, 0,
+            mainIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
+        )
+
+        val nBuilder: Notification.Builder?
+        if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */) {
+            nBuilder = createNotificationBuilderWithChannel()
+            if (nBuilder == null) return
+        } else nBuilder = Notification.Builder(this)
+
+        val notification =
+            nBuilder.setContentTitle(getText(R.string.notification_title_mpd_running))
+                .setContentText(getText(R.string.notification_text_mpd_running))
+                .setSmallIcon(R.drawable.notification_icon)
+                .setContentIntent(contentIntent)
+                .build()
+
+        mThread = Thread(this).apply { start() }
+
+        val player = MPDPlayer(Looper.getMainLooper())
+        mMediaSession = MediaSession.Builder(this, player).build()
+
+        startForeground(R.string.notification_title_mpd_running, notification)
+        startService(Intent(this, Main::class.java))
+    }
+
+    private fun stop() {
+        mMediaSession?.let {
+            it.release()
+            mMediaSession = null
+        }
+        mThread?.let { thread ->
+            if (thread.isAlive) {
+                synchronized(this) {
+                    if (mStatus == MAIN_STATUS_STARTED) Bridge.shutdown()
+                    else mAbort = true
+                }
+            }
+            try {
+                thread.join()
+                mThread = null
+                mAbort = false
+            } catch (ie: InterruptedException) {
+                Log.e(TAG, "failed to join", ie)
+            }
+        }
+        setWakelockEnabled(false)
+        ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
+        stopSelf()
+    }
+
+    private fun pause() {
+        if (mThread?.isAlive == true) {
+            synchronized(this) {
+                if (mStatus == MAIN_STATUS_STARTED) Bridge.pause()
+            }
+        }
+    }
+
+    private fun setPauseOnHeadphonesDisconnect(enabled: Boolean) {
+        mPauseOnHeadphonesDisconnect = enabled
+    }
+
+    private fun setWakelockEnabled(enabled: Boolean) {
+        if (enabled) {
+            val wakeLock =
+                mWakelock ?: run {
+                    val pm = getSystemService(POWER_SERVICE) as PowerManager
+                    pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).also {
+                        mWakelock = it
+                    }
+                }
+            wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
+            Log.d(TAG, "Wakelock acquired")
+        } else {
+            mWakelock?.let {
+                it.release()
+                mWakelock = null
+            }
+            Log.d(TAG, "Wakelock released")
+        }
+    }
+
+    private val isRunning: Boolean
+        get() = mThread?.isAlive == true
+
+    private fun registerCallback(cb: IMainCallback?) {
+        if (cb != null) {
+            mCallbacks.register(cb)
+            sendMessage(MSG_SEND_STATUS, mStatus, 0, mError)
+        }
+    }
+
+    private fun unregisterCallback(cb: IMainCallback?) {
+        if (cb != null) {
+            mCallbacks.unregister(cb)
+        }
+    }
+}