diff options
author | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-26 01:55:44 -0600 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-26 01:55:44 -0600 |
commit | 85dd303c88f636024ce8b32e17ff76bcb3a19911 (patch) | |
tree | 4608314368a6b72262c1f97b67bd5b1d25d9a6eb | |
parent | 2958144fd0082489a996fbc7a11572f3edbe42eb (diff) |
ui: root: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
7 files changed, 352 insertions, 398 deletions
diff --git a/ui/src/main/java/com/wireguard/android/Application.java b/ui/src/main/java/com/wireguard/android/Application.java deleted file mode 100644 index 62cf25a1..00000000 --- a/ui/src/main/java/com/wireguard/android/Application.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.StrictMode; -import android.util.Log; - -import com.wireguard.android.backend.Backend; -import com.wireguard.android.backend.GoBackend; -import com.wireguard.android.backend.WgQuickBackend; -import com.wireguard.android.configStore.FileConfigStore; -import com.wireguard.android.model.TunnelManager; -import com.wireguard.android.util.AsyncWorker; -import com.wireguard.android.util.ExceptionLoggers; -import com.wireguard.android.util.ModuleLoader; -import com.wireguard.android.util.RootShell; -import com.wireguard.android.util.ToolsInstaller; -import com.wireguard.util.NonNullForAll; - -import java.lang.ref.WeakReference; -import java.util.Locale; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.preference.PreferenceManager; -import java9.util.concurrent.CompletableFuture; - -@NonNullForAll -public class Application extends android.app.Application implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final String USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, Build.SUPPORTED_ABIS.length > 0 ? Build.SUPPORTED_ABIS[0] : "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT); - private static final String TAG = "WireGuard/" + Application.class.getSimpleName(); - @SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf; - private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>(); - @SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker; - @Nullable private Backend backend; - @SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader; - @SuppressWarnings("NullableProblems") private RootShell rootShell; - @SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences; - @SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller; - @SuppressWarnings("NullableProblems") private TunnelManager tunnelManager; - - public Application() { - weakSelf = new WeakReference<>(this); - } - - public static Application get() { - return weakSelf.get(); - } - - public static AsyncWorker getAsyncWorker() { - return get().asyncWorker; - } - - public static Backend getBackend() { - final Application app = get(); - synchronized (app.futureBackend) { - if (app.backend == null) { - Backend backend = null; - boolean didStartRootShell = false; - if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) { - try { - app.rootShell.start(); - didStartRootShell = true; - app.moduleLoader.loadModule(); - } catch (final Exception ignored) { - } - } - if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) { - try { - if (!didStartRootShell) - app.rootShell.start(); - final WgQuickBackend wgQuickBackend = new WgQuickBackend(app.getApplicationContext(), app.rootShell, app.toolsInstaller); - wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false)); - backend = wgQuickBackend; - } catch (final Exception ignored) { - } - } - if (backend == null) { - backend = new GoBackend(app.getApplicationContext()); - GoBackend.setAlwaysOnCallback(() -> { - get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D); - }); - } - app.backend = backend; - } - return app.backend; - } - } - - public static CompletableFuture<Backend> getBackendAsync() { - return get().futureBackend; - } - - public static ModuleLoader getModuleLoader() { - return get().moduleLoader; - } - - public static RootShell getRootShell() { - return get().rootShell; - } - - public static SharedPreferences getSharedPreferences() { - return get().sharedPreferences; - } - - public static ToolsInstaller getToolsInstaller() { - return get().toolsInstaller; - } - - public static TunnelManager getTunnelManager() { - return get().tunnelManager; - } - - @Override - protected void attachBaseContext(final Context context) { - super.attachBaseContext(context); - - if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_HOME); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - System.exit(0); - } - - if (BuildConfig.DEBUG) { - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); - } - } - - @Override - public void onCreate() { - Log.i(TAG, USER_AGENT); - super.onCreate(); - - asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper())); - rootShell = new RootShell(getApplicationContext()); - toolsInstaller = new ToolsInstaller(getApplicationContext(), rootShell); - moduleLoader = new ModuleLoader(getApplicationContext(), rootShell, USER_AGENT); - - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - AppCompatDelegate.setDefaultNightMode( - sharedPreferences.getBoolean("dark_theme", false) ? - AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO); - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - } - - tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext())); - tunnelManager.onCreate(); - - asyncWorker.supplyAsync(Application::getBackend).thenAccept(futureBackend::complete); - - sharedPreferences.registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { - if ("multiple_tunnels".equals(key) && backend != null && backend instanceof WgQuickBackend) - ((WgQuickBackend) backend).setMultipleTunnels(sharedPreferences.getBoolean(key, false)); - } - - @Override - public void onTerminate() { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); - super.onTerminate(); - } -} diff --git a/ui/src/main/java/com/wireguard/android/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt new file mode 100644 index 00000000..0e522abf --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/Application.kt @@ -0,0 +1,159 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.wireguard.android + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.os.AsyncTask +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceManager +import com.wireguard.android.backend.Backend +import com.wireguard.android.backend.GoBackend +import com.wireguard.android.backend.WgQuickBackend +import com.wireguard.android.configStore.FileConfigStore +import com.wireguard.android.model.TunnelManager +import com.wireguard.android.util.AsyncWorker +import com.wireguard.android.util.ExceptionLoggers +import com.wireguard.android.util.ModuleLoader +import com.wireguard.android.util.RootShell +import com.wireguard.android.util.ToolsInstaller +import java9.util.concurrent.CompletableFuture +import java.lang.ref.WeakReference +import java.util.Locale + +class Application : android.app.Application(), OnSharedPreferenceChangeListener { + private val futureBackend = CompletableFuture<Backend>() + private lateinit var asyncWorker: AsyncWorker + private var backend: Backend? = null + private lateinit var moduleLoader: ModuleLoader + private lateinit var rootShell: RootShell + private lateinit var sharedPreferences: SharedPreferences + private lateinit var toolsInstaller: ToolsInstaller + private lateinit var tunnelManager: TunnelManager + + override fun attachBaseContext(context: Context) { + super.attachBaseContext(context) + if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) { + val intent = Intent(Intent.ACTION_MAIN) + intent.addCategory(Intent.CATEGORY_HOME) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + System.exit(0) + } + if (BuildConfig.DEBUG) { + StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build()) + } + } + + override fun onCreate() { + Log.i(TAG, USER_AGENT) + super.onCreate() + asyncWorker = AsyncWorker(AsyncTask.SERIAL_EXECUTOR, Handler(Looper.getMainLooper())) + rootShell = RootShell(applicationContext) + toolsInstaller = ToolsInstaller(applicationContext, rootShell) + moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT) + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + AppCompatDelegate.setDefaultNightMode( + if (sharedPreferences.getBoolean("dark_theme", false)) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO) + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + tunnelManager = TunnelManager(FileConfigStore(applicationContext)) + tunnelManager.onCreate() + asyncWorker.supplyAsync { getBackend() }.thenAccept { futureBackend.complete(it) } + sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + if ("multiple_tunnels" == key && backend != null && backend is WgQuickBackend) + (backend as WgQuickBackend).setMultipleTunnels(sharedPreferences.getBoolean(key, false)) + } + + override fun onTerminate() { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + super.onTerminate() + } + + companion object { + val USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, if (Build.SUPPORTED_ABIS.isNotEmpty()) Build.SUPPORTED_ABIS[0] else "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT) + private val TAG = "WireGuard/" + Application::class.java.simpleName + private lateinit var weakSelf: WeakReference<Application> + + @JvmStatic + fun get(): Application { + return weakSelf.get()!! + } + + @JvmStatic + fun getAsyncWorker() = get().asyncWorker + + @JvmStatic + fun getBackend(): Backend { + val app = get() + synchronized(app.futureBackend) { + if (app.backend == null) { + var backend: Backend? = null + var didStartRootShell = false + if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) { + try { + app.rootShell.start() + didStartRootShell = true + app.moduleLoader.loadModule() + } catch (ignored: Exception) { + } + } + if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) { + try { + if (!didStartRootShell) + app.rootShell.start() + val wgQuickBackend = WgQuickBackend(app.applicationContext, app.rootShell, app.toolsInstaller) + wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false)) + backend = wgQuickBackend + } catch (ignored: Exception) { + } + } + if (backend == null) { + backend = GoBackend(app.applicationContext) + GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D) } + } + app.backend = backend + } + return app.backend!! + } + } + + @JvmStatic + fun getBackendAsync() = get().futureBackend + + @JvmStatic + fun getModuleLoader() = get().moduleLoader + + @JvmStatic + fun getRootShell() = get().rootShell + + @JvmStatic + fun getSharedPreferences() = get().sharedPreferences + + @JvmStatic + fun getToolsInstaller() = get().toolsInstaller + + @JvmStatic + fun getTunnelManager() = get().tunnelManager + } + + init { + weakSelf = WeakReference(this) + } +} diff --git a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.java b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.java deleted file mode 100644 index 3f4bcd83..00000000 --- a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import com.wireguard.android.backend.WgQuickBackend; -import com.wireguard.android.model.TunnelManager; -import com.wireguard.android.util.ExceptionLoggers; -import com.wireguard.util.NonNullForAll; - -@NonNullForAll -public class BootShutdownReceiver extends BroadcastReceiver { - private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName(); - - @Override - public void onReceive(final Context context, final Intent intent) { - Application.getBackendAsync().thenAccept(backend -> { - if (!(backend instanceof WgQuickBackend)) - return; - final String action = intent.getAction(); - if (action == null) - return; - final TunnelManager tunnelManager = Application.getTunnelManager(); - if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - Log.i(TAG, "Broadcast receiver restoring state (boot)"); - tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D); - } else if (Intent.ACTION_SHUTDOWN.equals(action)) { - Log.i(TAG, "Broadcast receiver saving state (shutdown)"); - tunnelManager.saveState(); - } - }); - } -} diff --git a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt new file mode 100644 index 00000000..e9759143 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt @@ -0,0 +1,34 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.wireguard.android + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.wireguard.android.backend.Backend +import com.wireguard.android.backend.WgQuickBackend +import com.wireguard.android.util.ExceptionLoggers + +class BootShutdownReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Application.getBackendAsync().thenAccept { backend: Backend? -> + if (backend !is WgQuickBackend) return@thenAccept + val action = intent.action ?: return@thenAccept + val tunnelManager = Application.getTunnelManager() + if (Intent.ACTION_BOOT_COMPLETED == action) { + Log.i(TAG, "Broadcast receiver restoring state (boot)") + tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D) + } else if (Intent.ACTION_SHUTDOWN == action) { + Log.i(TAG, "Broadcast receiver saving state (shutdown)") + tunnelManager.saveState() + } + } + } + + companion object { + private val TAG = "WireGuard/" + BootShutdownReceiver::class.java.simpleName + } +} diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.java b/ui/src/main/java/com/wireguard/android/QuickTileService.java deleted file mode 100644 index a485ae1e..00000000 --- a/ui/src/main/java/com/wireguard/android/QuickTileService.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android; - -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.IBinder; -import android.service.quicksettings.Tile; -import android.service.quicksettings.TileService; -import android.util.Log; - -import com.wireguard.android.activity.MainActivity; -import com.wireguard.android.activity.TunnelToggleActivity; -import com.wireguard.android.backend.Tunnel.State; -import com.wireguard.android.model.ObservableTunnel; -import com.wireguard.android.widget.SlashDrawable; -import com.wireguard.util.NonNullForAll; - -import java.util.Objects; - -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.databinding.Observable; -import androidx.databinding.Observable.OnPropertyChangedCallback; - -/** - * Service that maintains the application's custom Quick Settings tile. This service is bound by the - * system framework as necessary to update the appearance of the tile in the system UI, and to - * forward click events to the application. - */ - -@RequiresApi(Build.VERSION_CODES.N) -@NonNullForAll -public class QuickTileService extends TileService { - private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName(); - - private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback(); - private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback(); - @Nullable private Icon iconOff; - @Nullable private Icon iconOn; - @Nullable private ObservableTunnel tunnel; - - /* This works around an annoying unsolved frameworks bug some people are hitting. */ - @Override - @Nullable - public IBinder onBind(final Intent intent) { - IBinder ret = null; - try { - ret = super.onBind(intent); - } catch (final Exception e) { - Log.d(TAG, "Failed to bind to TileService", e); - } - return ret; - } - - @Override - public void onClick() { - if (tunnel != null) { - unlockAndRun(() -> { - final Tile tile = getQsTile(); - if (tile != null) { - tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn); - tile.updateTile(); - } - tunnel.setState(State.TOGGLE).whenComplete((v, t) -> { - if (t == null) { - updateTile(); - } else { - final Intent toggleIntent = new Intent(this, TunnelToggleActivity.class); - toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(toggleIntent); - } - }); - }); - } else { - final Intent intent = new Intent(this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivityAndCollapse(intent); - } - } - - @Override - public void onCreate() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile); - return; - } - final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile, Application.get().getTheme())); - icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */ - icon.setSlashed(false); - Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - icon.setBounds(0, 0, c.getWidth(), c.getHeight()); - icon.draw(c); - iconOn = Icon.createWithBitmap(b); - icon.setSlashed(true); - b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - c = new Canvas(b); - icon.setBounds(0, 0, c.getWidth(), c.getHeight()); - icon.draw(c); - iconOff = Icon.createWithBitmap(b); - } - - @Override - public void onStartListening() { - Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback); - if (tunnel != null) - tunnel.addOnPropertyChangedCallback(onStateChangedCallback); - updateTile(); - } - - @Override - public void onStopListening() { - if (tunnel != null) - tunnel.removeOnPropertyChangedCallback(onStateChangedCallback); - Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback); - } - - private void updateTile() { - // Update the tunnel. - final ObservableTunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel(); - if (newTunnel != tunnel) { - if (tunnel != null) - tunnel.removeOnPropertyChangedCallback(onStateChangedCallback); - tunnel = newTunnel; - if (tunnel != null) - tunnel.addOnPropertyChangedCallback(onStateChangedCallback); - } - // Update the tile contents. - final String label; - final int state; - final Tile tile = getQsTile(); - if (tunnel != null) { - label = tunnel.getName(); - state = tunnel.getState() == State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; - } else { - label = getString(R.string.app_name); - state = Tile.STATE_INACTIVE; - } - if (tile == null) - return; - tile.setLabel(label); - if (tile.getState() != state) { - tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff); - tile.setState(state); - } - tile.updateTile(); - } - - private final class OnStateChangedCallback extends OnPropertyChangedCallback { - @Override - public void onPropertyChanged(final Observable sender, final int propertyId) { - if (!Objects.equals(sender, tunnel)) { - sender.removeOnPropertyChangedCallback(this); - return; - } - if (propertyId != 0 && propertyId != BR.state) - return; - updateTile(); - } - } - - private final class OnTunnelChangedCallback extends OnPropertyChangedCallback { - @Override - public void onPropertyChanged(final Observable sender, final int propertyId) { - if (propertyId != 0 && propertyId != BR.lastUsedTunnel) - return; - updateTile(); - } - } -} diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt new file mode 100644 index 00000000..cd95f12d --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt @@ -0,0 +1,158 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.wireguard.android + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Icon +import android.os.Build +import android.os.IBinder +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.databinding.Observable +import androidx.databinding.Observable.OnPropertyChangedCallback +import com.wireguard.android.activity.MainActivity +import com.wireguard.android.activity.TunnelToggleActivity +import com.wireguard.android.backend.Tunnel +import com.wireguard.android.model.ObservableTunnel +import com.wireguard.android.widget.SlashDrawable + +/** + * Service that maintains the application's custom Quick Settings tile. This service is bound by the + * system framework as necessary to update the appearance of the tile in the system UI, and to + * forward click events to the application. + */ +@RequiresApi(Build.VERSION_CODES.N) +class QuickTileService : TileService() { + private val onStateChangedCallback = OnStateChangedCallback() + private val onTunnelChangedCallback = OnTunnelChangedCallback() + private var iconOff: Icon? = null + private var iconOn: Icon? = null + private var tunnel: ObservableTunnel? = null + + /* This works around an annoying unsolved frameworks bug some people are hitting. */ + override fun onBind(intent: Intent): IBinder? { + var ret: IBinder? = null + try { + ret = super.onBind(intent) + } catch (e: Exception) { + Log.d(TAG, "Failed to bind to TileService", e) + } + return ret + } + + override fun onClick() { + if (tunnel != null) { + unlockAndRun { + val tile = qsTile + if (tile != null) { + tile.icon = if (tile.icon == iconOn) iconOff else iconOn + tile.updateTile() + } + tunnel!!.setState(Tunnel.State.TOGGLE).whenComplete { _, t -> + if (t == null) { + updateTile() + } else { + val toggleIntent = Intent(this, TunnelToggleActivity::class.java) + toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(toggleIntent) + } + } + } + } else { + val intent = Intent(this, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivityAndCollapse(intent) + } + } + + override fun onCreate() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + iconOn = Icon.createWithResource(this, R.drawable.ic_tile) + iconOff = iconOn + return + } + val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme)) + icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */ + icon.setSlashed(false) + var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) + ?: return + var c = Canvas(b) + icon.setBounds(0, 0, c.width, c.height) + icon.draw(c) + iconOn = Icon.createWithBitmap(b) + icon.setSlashed(true) + b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) + ?: return + c = Canvas(b) + icon.setBounds(0, 0, c.width, c.height) + icon.draw(c) + iconOff = Icon.createWithBitmap(b) + } + + override fun onStartListening() { + Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback) + if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback) + updateTile() + } + + override fun onStopListening() { + if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback) + Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback) + } + + private fun updateTile() { + // Update the tunnel. + val newTunnel = Application.getTunnelManager().lastUsedTunnel + if (newTunnel != tunnel) { + if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback) + tunnel = newTunnel + if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback) + } + // Update the tile contents. + val label: String + val state: Int + val tile = qsTile + if (tunnel != null) { + label = tunnel!!.name + state = if (tunnel!!.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + } else { + label = getString(R.string.app_name) + state = Tile.STATE_INACTIVE + } + if (tile == null) return + tile.label = label + if (tile.state != state) { + tile.icon = if (state == Tile.STATE_ACTIVE) iconOn else iconOff + tile.state = state + } + tile.updateTile() + } + + private inner class OnStateChangedCallback : OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable, propertyId: Int) { + if (sender != tunnel) { + sender.removeOnPropertyChangedCallback(this) + return + } + if (propertyId != 0 && propertyId != BR.state) return + updateTile() + } + } + + private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable, propertyId: Int) { + if (propertyId != 0 && propertyId != BR.lastUsedTunnel) return + updateTile() + } + } + + companion object { + private val TAG = "WireGuard/" + QuickTileService::class.java.simpleName + } +} diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt index 6c5284ec..0734df45 100644 --- a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt @@ -43,7 +43,7 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con } init { - Application.getBackendAsync().thenAccept { backend: Backend -> + Application.getBackendAsync().thenAccept { backend -> versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH)) Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete { version, exception -> versionSummary = if (exception == null) |