@@ -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(); | |||
} | |||
} |
@@ -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) | |||
} | |||
} |
@@ -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(); | |||
} | |||
}); | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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) | |||