Rename app module to ui

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya
2020-03-09 19:24:27 +05:30
parent 6bc3e257f8
commit 7d48bef70a
115 changed files with 1 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WireGuard β</string>
</resources>
+90
View File
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.wireguard.android"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<permission
android:name="${applicationId}.permission.CONTROL_TUNNELS"
android:description="@string/permission_description"
android:icon="@mipmap/ic_launcher"
android:label="@string/permission_label"
android:protectionLevel="dangerous" />
<application
android:name=".Application"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<activity android:name=".activity.TunnelToggleActivity" android:theme="@style/NoBackgroundTheme"/>
<activity android:name=".activity.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".activity.SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.TunnelCreatorActivity"
android:label="@string/create_activity_title"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<receiver android:name=".BootShutdownReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".model.TunnelManager$IntentReceiver"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_DOWN" />
</intent-filter>
</receiver>
<service
android:name=".QuickTileService"
android:icon="@drawable/ic_tile"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
</service>
</application>
</manifest>
@@ -0,0 +1,171 @@
/*
* 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 androidx.preference.PreferenceManager;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
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 java.lang.ref.WeakReference;
import java.util.Locale;
import java9.util.concurrent.CompletableFuture;
public class Application extends android.app.Application {
private static final String TAG = "WireGuard/" + Application.class.getSimpleName();
public static final String USER_AGENT;
static {
String preferredAbi = "unknown ABI";
if (Build.SUPPORTED_ABIS.length > 0)
preferredAbi = Build.SUPPORTED_ABIS[0];
USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, preferredAbi, Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT);
}
@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 RootShell rootShell;
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
@SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader;
@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 (ModuleLoader.isModuleLoaded()) {
try {
if (!didStartRootShell)
app.rootShell.start();
backend = new WgQuickBackend(app.getApplicationContext(), app.rootShell, app.toolsInstaller);
} 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 RootShell getRootShell() {
return get().rootShell;
}
public static SharedPreferences getSharedPreferences() {
return get().sharedPreferences;
}
public static ToolsInstaller getToolsInstaller() {
return get().toolsInstaller;
}
public static ModuleLoader getModuleLoader() {
return get().moduleLoader;
}
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);
}
}
@@ -0,0 +1,38 @@
/*
* 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;
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,174 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.content.Intent;
import androidx.databinding.Observable;
import androidx.databinding.Observable.OnPropertyChangedCallback;
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 androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
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 java.util.Objects;
/**
* 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)
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,99 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import androidx.databinding.CallbackRegistry;
import androidx.databinding.CallbackRegistry.NotifierCallback;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.model.ObservableTunnel;
import java.util.Objects;
/**
* Base class for activities that need to remember the currently-selected tunnel.
*/
public abstract class BaseActivity extends ThemeChangeAwareActivity {
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
@Nullable private ObservableTunnel selectedTunnel;
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.add(listener);
}
@Nullable
public ObservableTunnel getSelectedTunnel() {
return selectedTunnel;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
else
savedTunnelName = null;
if (savedTunnelName != null)
Application.getTunnelManager().getTunnels()
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
if (selectedTunnel != null)
outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
super.onSaveInstanceState(outState);
}
protected abstract void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel);
public void removeOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.remove(listener);
}
public void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) {
final ObservableTunnel oldTunnel = selectedTunnel;
if (Objects.equals(oldTunnel, tunnel))
return;
selectedTunnel = tunnel;
onSelectedTunnelChanged(oldTunnel, tunnel);
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
}
public interface OnSelectedTunnelChangedListener {
void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel);
}
private static final class SelectionChangeNotifier
extends NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> {
@Override
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
final ObservableTunnel oldTunnel, final int ignored,
final ObservableTunnel newTunnel) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
}
}
private static final class SelectionChangeRegistry
extends CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> {
private SelectionChangeRegistry() {
super(new SelectionChangeNotifier());
}
}
}
@@ -0,0 +1,144 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.appcompat.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View.OnApplyWindowInsetsListener;
import android.widget.LinearLayout;
import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.ObservableTunnel;
import java.util.List;
/**
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
* WireGuard application, and contains several fragments for listing, viewing details of, and
* editing the configuration and interface state of WireGuard tunnels.
*/
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Override
public void onBackPressed() {
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish();
return;
}
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
if (!isTwoPaneLayout && backStackEntries == 1) {
getSupportFragmentManager().popBackStack();
setSelectedTunnel(null);
return;
}
super.onBackPressed();
}
@Override public void onBackStackChanged() {
if (actionBar == null)
return;
// Do not show the home menu when the two-pane layout is at the detail view (see above).
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
}
// We use onTouchListener here to avoid the UI click sound, hence
// calling View#performClick defeats the purpose of it.
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
actionBar = getSupportActionBar();
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
getSupportFragmentManager().addOnBackStackChangedListener(this);
onBackStackChanged();
// Dispatch insets on back stack change
// This is required to ensure replaced fragments are also able to consume insets
findViewById(R.id.master_detail_wrapper).setOnApplyWindowInsetsListener((OnApplyWindowInsetsListener) (v, insets) -> {
final FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.addOnBackStackChangedListener(() -> {
final List<Fragment> fragments = fragmentManager.getFragments();
for (int i = 0; i < fragments.size(); i++) {
fragments.get(i).requireView().dispatchApplyWindowInsets(insets);
}
});
return insets;
});
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.main_activity, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
onBackPressed();
return true;
case R.id.menu_action_edit:
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_container, new TunnelEditorFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
return true;
case R.id.menu_action_save:
// This menu item is handled by the editor fragment.
return false;
case R.id.menu_settings:
startActivity(new Intent(this, SettingsActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel,
@Nullable final ObservableTunnel newTunnel) {
final FragmentManager fragmentManager = getSupportFragmentManager();
final int backStackEntries = fragmentManager.getBackStackEntryCount();
if (newTunnel == null) {
// Clear everything off the back stack (all editors and detail fragments).
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
return;
}
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
// method to avoid the editor picking up the new tunnel while it is still visible.
fragmentManager.popBackStackImmediate();
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, new TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
}
}
}
@@ -0,0 +1,129 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import android.util.SparseArray;
import android.view.MenuItem;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.util.ModuleLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Interface for changing application-global persistent settings.
*/
public class SettingsActivity extends ThemeChangeAwareActivity {
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
private int permissionRequestCounter;
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
final List<String> needPermissions = new ArrayList<>(permissions.length);
for (final String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED)
needPermissions.add(permission);
}
if (needPermissions.isEmpty()) {
final int[] granted = new int[permissions.length];
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
cb.done(permissions, granted);
return;
}
final int idx = permissionRequestCounter++;
permissionRequestCallbacks.put(idx, cb);
ActivityCompat.requestPermissions(this,
needPermissions.toArray(new String[needPermissions.size()]), idx);
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new SettingsFragment())
.commit();
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(final int requestCode,
final String[] permissions,
final int[] grantResults) {
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
if (f != null) {
permissionRequestCallbacks.remove(requestCode);
f.done(permissions, grantResults);
}
}
public interface PermissionRequestCallback {
void done(String[] permissions, int[] grantResults);
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences);
final PreferenceScreen screen = getPreferenceScreen();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
screen.removePreference(getPreferenceManager().findPreference("dark_theme"));
final Preference wgQuickOnlyPrefs[] = {
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
};
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
else
screen.removePreference(pref);
}
});
final Preference moduleInstaller = getPreferenceManager().findPreference("module_downloader");
moduleInstaller.setVisible(false);
if (ModuleLoader.isModuleLoaded()) {
screen.removePreference(moduleInstaller);
} else {
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> {
if (e == null)
moduleInstaller.setVisible(true);
else
screen.removePreference(moduleInstaller);
});
}
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import android.util.Log;
import com.wireguard.android.Application;
import java.lang.reflect.Field;
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onDestroy() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if ("dark_theme".equals(key)) {
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean(key, false) ?
AppCompatDelegate.MODE_NIGHT_YES :
AppCompatDelegate.MODE_NIGHT_NO);
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.ObservableTunnel;
/**
* Standalone activity for creating tunnels.
*/
public class TunnelCreatorActivity extends BaseActivity {
@Override
@SuppressWarnings("UnnecessaryFullyQualifiedName")
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new TunnelEditorFragment())
.commit();
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) {
finish();
}
}
@@ -0,0 +1,51 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.Build;
import android.service.quicksettings.TileService;
import android.util.Log;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.QuickTileService;
import com.wireguard.android.R;
import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
@RequiresApi(Build.VERSION_CODES.N)
public class TunnelToggleActivity extends AppCompatActivity {
private static final String TAG = "WireGuard/" + TunnelToggleActivity.class.getSimpleName();
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ObservableTunnel tunnel = Application.getTunnelManager().getLastUsedTunnel();
if (tunnel == null)
return;
tunnel.setState(State.TOGGLE).whenComplete((v, t) -> {
TileService.requestListeningState(this, new ComponentName(this, QuickTileService.class));
onToggleFinished(t);
finishAffinity();
});
}
private void onToggleFinished(@Nullable final Throwable throwable) {
if (throwable == null)
return;
final String error = ErrorMessages.get(throwable);
final String message = getString(R.string.toggle_error, error);
Log.e(TAG, message, throwable);
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
@@ -0,0 +1,67 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
import com.wireguard.config.Config;
import java.util.Set;
/**
* Interface for persistent storage providers for WireGuard configurations.
*/
public interface ConfigStore {
/**
* Create a persistent tunnel, which must have a unique name within the persistent storage
* medium.
*
* @param name The name of the tunnel to create.
* @param config Configuration for the new tunnel.
* @return The configuration that was actually saved to persistent storage.
*/
Config create(final String name, final Config config) throws Exception;
/**
* Delete a persistent tunnel.
*
* @param name The name of the tunnel to delete.
*/
void delete(final String name) throws Exception;
/**
* Enumerate the names of tunnels present in persistent storage.
*
* @return The set of present tunnel names.
*/
Set<String> enumerate();
/**
* Load the configuration for the tunnel given by {@code name}.
*
* @param name The identifier for the configuration in persistent storage (i.e. the name of the
* tunnel).
* @return An in-memory representation of the configuration loaded from persistent storage.
*/
Config load(final String name) throws Exception;
/**
* Rename the configuration for the tunnel given by {@code name}.
*
* @param name The identifier for the existing configuration in persistent storage.
* @param replacement The new identifier for the configuration in persistent storage.
*/
void rename(String name, String replacement) throws Exception;
/**
* Save the configuration for an existing tunnel given by {@code name}.
*
* @param name The identifier for the configuration in persistent storage (i.e. the name of
* the tunnel).
* @param config An updated configuration object for the tunnel.
* @return The configuration that was actually saved to persistent storage.
*/
Config save(final String name, final Config config) throws Exception;
}
@@ -0,0 +1,103 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
import android.content.Context;
import android.util.Log;
import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
*/
public final class FileConfigStore implements ConfigStore {
private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
private final Context context;
public FileConfigStore(final Context context) {
this.context = context;
}
@Override
public Config create(final String name, final Config config) throws IOException {
Log.d(TAG, "Creating configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.createNewFile())
throw new IOException(context.getString(R.string.config_file_exists_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
@Override
public void delete(final String name) throws IOException {
Log.d(TAG, "Deleting configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.delete())
throw new IOException(context.getString(R.string.config_delete_error, file.getName()));
}
@Override
public Set<String> enumerate() {
return Stream.of(context.fileList())
.filter(name -> name.endsWith(".conf"))
.map(name -> name.substring(0, name.length() - ".conf".length()))
.collect(Collectors.toUnmodifiableSet());
}
private File fileFor(final String name) {
return new File(context.getFilesDir(), name + ".conf");
}
@Override
public Config load(final String name) throws BadConfigException, IOException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.parse(stream);
}
}
@Override
public void rename(final String name, final String replacement) throws IOException {
Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
final File file = fileFor(name);
final File replacementFile = fileFor(replacement);
if (!replacementFile.createNewFile())
throw new IOException(context.getString(R.string.config_exists_error, replacement));
if (!file.renameTo(replacementFile)) {
if (!replacementFile.delete())
Log.w(TAG, "Couldn't delete marker file for new name " + replacement);
throw new IOException(context.getString(R.string.config_rename_error, file.getName()));
}
}
@Override
public Config save(final String name, final Config config) throws IOException {
Log.d(TAG, "Saving configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.isFile())
throw new FileNotFoundException(context.getString(R.string.config_not_found_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
}
@@ -0,0 +1,148 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import androidx.databinding.BindingAdapter;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.databinding.adapters.ListenerUtil;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.InputFilter;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.widget.ToggleSwitch;
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
import com.wireguard.config.Attribute;
import com.wireguard.config.InetNetwork;
import com.wireguard.util.Keyed;
import java9.util.Optional;
/**
* Static methods for use by generated code in the Android data binding library.
*/
@SuppressWarnings("unused")
public final class BindingAdapters {
private BindingAdapters() {
// Prevent instantiation.
}
@BindingAdapter("checked")
public static void setChecked(final ToggleSwitch view, final boolean checked) {
view.setCheckedInternal(checked);
}
@BindingAdapter("filter")
public static void setFilter(final TextView view, final InputFilter filter) {
view.setFilters(new InputFilter[]{filter});
}
@BindingAdapter({"items", "layout"})
public static <E>
void setItems(final LinearLayout view,
@Nullable final ObservableList<E> oldList, final int oldLayoutId,
@Nullable final ObservableList<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
// If the layout changes, any existing listener must be replaced.
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
listener.setList(null);
listener = null;
// Stop tracking the old listener.
ListenerUtil.trackListener(view, null, R.id.item_change_listener);
}
// Avoid adding a listener when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (listener == null) {
listener = new ItemChangeListener<>(view, newLayoutId);
ListenerUtil.trackListener(view, listener, R.id.item_change_listener);
}
// Either the list changed, or this is an entirely new listener because the layout changed.
listener.setList(newList);
}
@BindingAdapter({"items", "layout"})
public static <E>
void setItems(final LinearLayout view,
@Nullable final Iterable<E> oldList, final int oldLayoutId,
@Nullable final Iterable<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
view.removeAllViews();
if (newList == null)
return;
final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext());
for (final E item : newList) {
final ViewDataBinding binding =
DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false);
binding.setVariable(BR.collection, newList);
binding.setVariable(BR.item, item);
binding.executePendingBindings();
view.addView(binding.getRoot());
}
}
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
public static <K, E extends Keyed<? extends K>>
void setItems(final RecyclerView view,
@Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
final RowConfigurationHandler oldRowConfigurationHandler,
@Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId,
final RowConfigurationHandler newRowConfigurationHandler) {
if (view.getLayoutManager() == null)
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
if (oldList == newList && oldLayoutId == newLayoutId)
return;
// The ListAdapter interface is not generic, so this cannot be checked.
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
// If the layout changes, any existing adapter must be replaced.
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
adapter.setList(null);
adapter = null;
}
// Avoid setting an adapter when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (adapter == null) {
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
view.setAdapter(adapter);
}
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
// Either the list changed, or this is an entirely new listener because the layout changed.
adapter.setList(newList);
}
@BindingAdapter("onBeforeCheckedChanged")
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
final OnBeforeCheckedChangeListener listener) {
view.setOnBeforeCheckedChangeListener(listener);
}
@BindingAdapter("android:text")
public static void setText(final TextView view, final Optional<?> text) {
view.setText(text.map(Object::toString).orElse(""));
}
@BindingAdapter("android:text")
public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
view.setText(networks != null ? Attribute.join(networks) : "");
}
}
@@ -0,0 +1,140 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import java.lang.ref.WeakReference;
import java.util.Objects;
/**
* Helper class for binding an ObservableList to the children of a ViewGroup.
*/
class ItemChangeListener<T> {
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
private final ViewGroup container;
private final int layoutId;
private final LayoutInflater layoutInflater;
@Nullable private ObservableList<T> list;
ItemChangeListener(final ViewGroup container, final int layoutId) {
this.container = container;
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(container.getContext());
}
private View getView(final int position, @Nullable final View convertView) {
ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
if (binding == null) {
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
}
Objects.requireNonNull(list, "Trying to get a view while list is still null");
binding.setVariable(BR.collection, list);
binding.setVariable(BR.item, list.get(position));
binding.executePendingBindings();
return binding.getRoot();
}
void setList(@Nullable final ObservableList<T> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
callback.onChanged(list);
} else {
container.removeAllViews();
}
}
private static final class OnListChangedCallback<T>
extends ObservableList.OnListChangedCallback<ObservableList<T>> {
private final WeakReference<ItemChangeListener<T>> weakListener;
private OnListChangedCallback(final ItemChangeListener<T> listener) {
weakListener = new WeakReference<>(listener);
}
@Override
public void onChanged(final ObservableList<T> sender) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
// TODO: recycle views
listener.container.removeAllViews();
for (int i = 0; i < sender.size(); ++i)
listener.container.addView(listener.getView(i, null));
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
for (int i = positionStart; i < positionStart + itemCount; ++i) {
final View child = listener.container.getChildAt(i);
listener.container.removeViewAt(i);
listener.container.addView(listener.getView(i, child));
}
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
for (int i = positionStart; i < positionStart + itemCount; ++i)
listener.container.addView(listener.getView(i, null));
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition,
final int toPosition, final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
final View[] views = new View[itemCount];
for (int i = 0; i < itemCount; ++i)
views[i] = listener.container.getChildAt(fromPosition + i);
listener.container.removeViews(fromPosition, itemCount);
for (int i = 0; i < itemCount; ++i)
listener.container.addView(views[i], toPosition + i);
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
listener.container.removeViews(positionStart, itemCount);
} else {
sender.removeOnListChangedCallback(this);
}
}
}
}
@@ -0,0 +1,159 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import android.content.Context;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.util.Keyed;
import java.lang.ref.WeakReference;
/**
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
*/
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
private final int layoutId;
private final LayoutInflater layoutInflater;
@Nullable private ObservableKeyedList<K, E> list;
@Nullable private RowConfigurationHandler rowConfigurationHandler;
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(context);
setList(list);
}
@Nullable
private E getItem(final int position) {
if (list == null || position < 0 || position >= list.size())
return null;
return list.get(position);
}
@Override
public int getItemCount() {
return list != null ? list.size() : 0;
}
@Override
public long getItemId(final int position) {
final K key = getKey(position);
return key != null ? key.hashCode() : -1;
}
@Nullable
private K getKey(final int position) {
final E item = getItem(position);
return item != null ? item.getKey() : null;
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.binding.setVariable(BR.collection, list);
holder.binding.setVariable(BR.key, getKey(position));
holder.binding.setVariable(BR.item, getItem(position));
holder.binding.executePendingBindings();
if (rowConfigurationHandler != null) {
final E item = getItem(position);
if (item != null) {
rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
}
}
}
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
}
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
}
notifyDataSetChanged();
}
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
this.rowConfigurationHandler = rowConfigurationHandler;
}
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
void onConfigureRow(B binding, T item, int position);
}
private static final class OnListChangedCallback<E extends Keyed<?>>
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
weakAdapter = new WeakReference<>(adapter);
}
@Override
public void onChanged(final ObservableList<E> sender) {
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
else
sender.removeOnListChangedCallback(this);
}
@Override
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
final int toPosition, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final ViewDataBinding binding;
public ViewHolder(final ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
@@ -0,0 +1,106 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
import android.content.Intent
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.zxing.integration.android.IntentIntegrator
import com.wireguard.android.R
import com.wireguard.android.activity.TunnelCreatorActivity
import com.wireguard.android.util.resolveAttribute
class AddTunnelsSheet : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
dismiss()
}
}
}
override fun getTheme(): Int {
return R.style.BottomSheetDialogTheme
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (savedInstanceState != null) dismiss()
return inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
val dialog = dialog as BottomSheetDialog? ?: return
behavior = dialog.behavior
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = 0
behavior.addBottomSheetCallback(bottomSheetCallback)
dialog.findViewById<View>(R.id.create_empty)?.setOnClickListener {
dismiss()
onRequestCreateConfig()
}
dialog.findViewById<View>(R.id.create_from_file)?.setOnClickListener {
dismiss()
onRequestImportConfig()
}
dialog.findViewById<View>(R.id.create_from_qrcode)?.setOnClickListener {
dismiss()
onRequestScanQRCode()
}
}
})
val gradientDrawable = GradientDrawable().apply {
setColor(requireContext().resolveAttribute(R.attr.colorBackground))
}
view.background = gradientDrawable
}
override fun dismiss() {
super.dismiss()
behavior.removeBottomSheetCallback(bottomSheetCallback)
}
private fun requireTargetFragment(): Fragment {
return requireNotNull(targetFragment) { "A target fragment should always be set" }
}
private fun onRequestCreateConfig() {
startActivity(Intent(activity, TunnelCreatorActivity::class.java))
}
private fun onRequestImportConfig() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
requireTargetFragment().startActivityForResult(intent, TunnelListFragment.REQUEST_IMPORT)
}
private fun onRequestScanQRCode() {
val integrator = IntentIntegrator.forSupportFragment(requireTargetFragment()).apply {
setOrientationLocked(false)
setBeepEnabled(false)
setPrompt(getString(R.string.qr_code_hint))
}
integrator.initiateScan(listOf(IntentIntegrator.QR_CODE))
}
}
@@ -0,0 +1,140 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AlertDialog;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.AppListDialogFragmentBinding;
import com.wireguard.android.model.ApplicationData;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.ObservableKeyedArrayList;
import com.wireguard.android.util.ObservableKeyedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java9.util.Comparators;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
public class AppListDialogFragment extends DialogFragment {
private static final String KEY_EXCLUDED_APPS = "excludedApps";
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
private List<String> currentlyExcludedApps = Collections.emptyList();
public static <T extends Fragment & AppExclusionListener>
AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
final Bundle extras = new Bundle();
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
final AppListDialogFragment fragment = new AppListDialogFragment();
fragment.setTargetFragment(target, 0);
fragment.setArguments(extras);
return fragment;
}
private void loadData() {
final Activity activity = getActivity();
if (activity == null) {
return;
}
final PackageManager pm = activity.getPackageManager();
Application.getAsyncWorker().supplyAsync(() -> {
final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
final List<ApplicationData> applicationData = new ArrayList<>();
for (ResolveInfo resolveInfo : resolveInfos) {
String packageName = resolveInfo.activityInfo.packageName;
applicationData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
}
Collections.sort(applicationData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
return applicationData;
}).whenComplete(((data, throwable) -> {
if (data != null) {
appData.clear();
appData.addAll(data);
} else {
final String error = ErrorMessages.get(throwable);
final String message = activity.getString(R.string.error_fetching_apps, error);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
dismissAllowingStateLoss();
}
}));
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final List<String> excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS);
currentlyExcludedApps = (excludedApps != null) ? excludedApps : Collections.emptyList();
}
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(requireActivity());
alertDialogBuilder.setTitle(R.string.excluded_applications);
final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(requireActivity().getLayoutInflater(), null, false);
binding.executePendingBindings();
alertDialogBuilder.setView(binding.getRoot());
alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss());
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
alertDialogBuilder.setNeutralButton(R.string.toggle_all, (dialog, which) -> {
});
binding.setFragment(this);
binding.setAppData(appData);
loadData();
final AlertDialog dialog = alertDialogBuilder.create();
dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
final List<ApplicationData> selectedItems = StreamSupport.stream(appData)
.filter(ApplicationData::isExcludedFromTunnel)
.collect(Collectors.toList());
final boolean excludeAll = selectedItems.isEmpty();
for (final ApplicationData app : appData)
app.setExcludedFromTunnel(excludeAll);
}));
return dialog;
}
private void setExclusionsAndDismiss() {
final List<String> excludedApps = new ArrayList<>();
for (final ApplicationData data : appData) {
if (data.isExcludedFromTunnel()) {
excludedApps.add(data.getPackageName());
}
}
((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps);
dismiss();
}
public interface AppExclusionListener {
void onExcludedAppsSelected(List<String> excludedApps);
}
}
@@ -0,0 +1,126 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.content.Context;
import android.content.Intent;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.BaseActivity;
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
* attached to a {@code BaseActivity}.
*/
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
@Nullable private BaseActivity activity;
@Nullable private ObservableTunnel pendingTunnel;
@Nullable private Boolean pendingTunnelUp;
@Nullable
protected ObservableTunnel getSelectedTunnel() {
return activity != null ? activity.getSelectedTunnel() : null;
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
if (pendingTunnel != null && pendingTunnelUp != null)
setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
pendingTunnel = null;
pendingTunnelUp = null;
}
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (context instanceof BaseActivity) {
activity = (BaseActivity) context;
activity.addOnSelectedTunnelChangedListener(this);
} else {
activity = null;
}
}
@Override
public void onDetach() {
if (activity != null)
activity.removeOnSelectedTunnelChangedListener(this);
activity = null;
super.onDetach();
}
protected void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) {
if (activity != null)
activity.setSelectedTunnel(tunnel);
}
public void setTunnelState(final View view, final boolean checked) {
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
final ObservableTunnel tunnel;
if (binding instanceof TunnelDetailFragmentBinding)
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
else if (binding instanceof TunnelListItemBinding)
tunnel = ((TunnelListItemBinding) binding).getItem();
else
return;
if (tunnel == null)
return;
Application.getBackendAsync().thenAccept(backend -> {
if (backend instanceof GoBackend) {
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
if (intent != null) {
pendingTunnel = tunnel;
pendingTunnelUp = checked;
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
return;
}
}
setTunnelStateWithPermissionsResult(tunnel, checked);
});
}
private void setTunnelStateWithPermissionsResult(final ObservableTunnel tunnel, final boolean checked) {
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null)
return;
final String error = ErrorMessages.get(throwable);
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = requireContext().getString(messageResId, error);
final View view = getView();
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
else
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -0,0 +1,118 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.view.inputmethod.InputMethodManager;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class ConfigNamingDialogFragment extends DialogFragment {
private static final String KEY_CONFIG_TEXT = "config_text";
@Nullable private ConfigNamingDialogFragmentBinding binding;
@Nullable private Config config;
@Nullable private InputMethodManager imm;
public static ConfigNamingDialogFragment newInstance(final String configText) {
final Bundle extras = new Bundle();
extras.putString(KEY_CONFIG_TEXT, configText);
final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
fragment.setArguments(extras);
return fragment;
}
private void createTunnelAndDismiss() {
if (binding != null) {
final String name = binding.tunnelNameText.getText().toString();
Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
if (tunnel != null) {
dismiss();
} else {
binding.tunnelNameTextLayout.setError(throwable.getMessage());
}
});
}
}
@Override
public void dismiss() {
setKeyboardVisible(false);
super.dismiss();
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
final String configText = arguments.getString(KEY_CONFIG_TEXT);
final byte[] configBytes = configText.getBytes(StandardCharsets.UTF_8);
try {
config = Config.parse(new ByteArrayInputStream(configBytes));
} catch (final BadConfigException | IOException e) {
throw new IllegalArgumentException("Invalid config passed to " + getClass().getSimpleName(), e);
}
}
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Activity activity = requireActivity();
imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
alertDialogBuilder.setTitle(R.string.import_from_qr_code);
binding = ConfigNamingDialogFragmentBinding.inflate(activity.getLayoutInflater(), null, false);
binding.executePendingBindings();
alertDialogBuilder.setView(binding.getRoot());
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
return alertDialogBuilder.create();
}
@Override public void onResume() {
super.onResume();
final AlertDialog dialog = (AlertDialog) getDialog();
if (dialog != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
setKeyboardVisible(true);
}
}
private void setKeyboardVisible(final boolean visible) {
Objects.requireNonNull(imm);
if (visible) {
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (binding != null) {
imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
}
}
}
@@ -0,0 +1,162 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelDetailPeerBinding;
import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.ui.EdgeToEdge;
import com.wireguard.crypto.Key;
import java.util.Timer;
import java.util.TimerTask;
/**
* Fragment that shows details about a specific tunnel.
*/
public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding;
@Nullable private Timer timer;
@Nullable private State lastState = State.TOGGLE;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.tunnel_detail, menu);
}
@Override
public void onStop() {
super.onStop();
if (timer != null) {
timer.cancel();
timer = null;
}
}
@Override
public void onResume() {
super.onResume();
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
updateStats();
}
}, 0, 1000);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
EdgeToEdge.setUpRoot((ViewGroup) binding.getRoot());
EdgeToEdge.setUpScrollingContent((ViewGroup) binding.getRoot(), null);
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) {
if (binding == null)
return;
binding.setTunnel(newTunnel);
if (newTunnel == null)
binding.setConfig(null);
else
newTunnel.getConfigAsync().thenAccept(binding::setConfig);
lastState = State.TOGGLE;
updateStats();
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
private String formatBytes(final long bytes) {
if (bytes < 1024)
return requireContext().getString(R.string.transfer_bytes, bytes);
else if (bytes < 1024*1024)
return requireContext().getString(R.string.transfer_kibibytes, bytes/1024.0);
else if (bytes < 1024*1024*1024)
return requireContext().getString(R.string.transfer_mibibytes, bytes/(1024.0*1024.0));
else if (bytes < 1024*1024*1024*1024)
return requireContext().getString(R.string.transfer_gibibytes, bytes/(1024.0*1024.0*1024.0));
return requireContext().getString(R.string.transfer_tibibytes, bytes/(1024.0*1024.0*1024.0)/1024.0);
}
private void updateStats() {
if (binding == null || !isResumed())
return;
final ObservableTunnel tunnel = binding.getTunnel();
if (tunnel == null)
return;
final State state = tunnel.getState();
if (state != State.UP && lastState == state)
return;
lastState = state;
tunnel.getStatisticsAsync().whenComplete((statistics, throwable) -> {
if (throwable != null) {
for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
if (peer == null)
continue;
peer.transferLabel.setVisibility(View.GONE);
peer.transferText.setVisibility(View.GONE);
}
return;
}
for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
if (peer == null)
continue;
final Key publicKey = peer.getItem().getPublicKey();
final long rx = statistics.peerRx(publicKey);
final long tx = statistics.peerTx(publicKey);
if (rx == 0 && tx == 0) {
peer.transferLabel.setVisibility(View.GONE);
peer.transferText.setVisibility(View.GONE);
continue;
}
peer.transferText.setText(requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx)));
peer.transferLabel.setVisibility(View.VISIBLE);
peer.transferText.setVisibility(View.VISIBLE);
}
});
}
}
@@ -0,0 +1,264 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.content.Context;
import androidx.databinding.ObservableList;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.ui.EdgeToEdge;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.viewmodel.ConfigProxy;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Fragment for editing a WireGuard configuration.
*/
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
private static final String KEY_LOCAL_CONFIG = "local_config";
private static final String KEY_ORIGINAL_NAME = "original_name";
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
@Nullable private TunnelEditorFragmentBinding binding;
@Nullable private ObservableTunnel tunnel;
private void onConfigLoaded(final Config config) {
if (binding != null) {
binding.setConfig(new ConfigProxy(config));
}
}
private void onConfigSaved(final ObservableTunnel savedTunnel,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.getName());
Log.d(TAG, message);
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.config_editor, menu);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
EdgeToEdge.setUpRoot((ViewGroup) binding.getRoot());
EdgeToEdge.setUpScrollingContent(binding.mainContainer, null);
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onExcludedAppsSelected(final List<String> excludedApps) {
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
final ObservableList<String> excludedApplications =
binding.getConfig().getInterface().getExcludedApplications();
excludedApplications.clear();
excludedApplications.addAll(excludedApps);
}
private void onFinished() {
// Hide the keyboard; it rarely goes away on its own.
final Activity activity = getActivity();
if (activity == null) return;
final View focusedView = activity.getCurrentFocus();
if (focusedView != null) {
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final InputMethodManager inputManager = (InputMethodManager) service;
if (inputManager != null)
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
// Tell the activity to finish itself or go back to the detail view.
getActivity().runOnUiThread(() -> {
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
// The selected tunnel has to actually change, but we have to remember this one.
final ObservableTunnel savedTunnel = tunnel;
if (savedTunnel == getSelectedTunnel())
setSelectedTunnel(null);
setSelectedTunnel(savedTunnel);
});
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_save:
if (binding == null)
return false;
final Config newConfig;
try {
newConfig = binding.getConfig().resolve();
} catch (final Exception e) {
final String error = ErrorMessages.get(e);
final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
final String message = getString(R.string.config_save_error, tunnelName, error);
Log.e(TAG, message, e);
Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
return false;
}
if (tunnel == null) {
Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
final TunnelManager manager = Application.getTunnelManager();
manager.create(binding.getName(), newConfig)
.whenComplete(this::onTunnelCreated);
} else if (!tunnel.getName().equals(binding.getName())) {
Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
tunnel.setName(binding.getName())
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
} else {
Log.d(TAG, "Attempting to save config of " + tunnel.getName());
tunnel.setConfig(newConfig)
.whenComplete((a, b) -> onConfigSaved(tunnel, b));
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
if (binding != null) {
final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
fragment.show(getParentFragmentManager(), null);
}
}
@Override
public void onSaveInstanceState(final Bundle outState) {
if (binding != null)
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
super.onSaveInstanceState(outState);
}
@Override
public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel,
@Nullable final ObservableTunnel newTunnel) {
tunnel = newTunnel;
if (binding == null)
return;
binding.setConfig(new ConfigProxy());
if (tunnel != null) {
binding.setName(tunnel.getName());
tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
} else {
binding.setName("");
}
}
private void onTunnelCreated(final ObservableTunnel newTunnel, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
tunnel = newTunnel;
message = getString(R.string.tunnel_create_success, tunnel.getName());
Log.d(TAG, message);
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_create_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
private void onTunnelRenamed(final ObservableTunnel renamedTunnel, final Config newConfig,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
Log.d(TAG, message);
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_rename_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
if (savedInstanceState == null) {
onSelectedTunnelChanged(null, getSelectedTunnel());
} else {
tunnel = getSelectedTunnel();
final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
if (tunnel != null && !tunnel.getName().equals(originalName))
onSelectedTunnelChanged(null, tunnel);
else
binding.setConfig(config);
}
super.onViewStateRestored(savedInstanceState);
}
}
@@ -0,0 +1,449 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.TunnelCreatorActivity;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.ui.EdgeToEdge;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.widget.MultiselectableRelativeLayout;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture;
import java9.util.stream.StreamSupport;
/**
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
*/
public class TunnelListFragment extends BaseFragment {
public static final int REQUEST_IMPORT = 1;
private static final int REQUEST_TARGET_FRAGMENT = 2;
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
private final ActionModeListener actionModeListener = new ActionModeListener();
@Nullable private ActionMode actionMode;
@Nullable private TunnelListFragmentBinding binding;
private void importTunnel(@NonNull final String configText) {
try {
// Ensure the config text is parseable before proceeding…
Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
// Config text is valid, now create the tunnel…
ConfigNamingDialogFragment.newInstance(configText).show(getParentFragmentManager(), null);
} catch (final BadConfigException | IOException e) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
}
}
private void importTunnel(@Nullable final Uri uri) {
final Activity activity = getActivity();
if (activity == null || uri == null)
return;
final ContentResolver contentResolver = activity.getContentResolver();
final Collection<CompletableFuture<ObservableTunnel>> futureTunnels = new ArrayList<>();
final List<Throwable> throwables = new ArrayList<>();
Application.getAsyncWorker().supplyAsync(() -> {
final String[] columns = {OpenableColumns.DISPLAY_NAME};
String name = null;
try (Cursor cursor = contentResolver.query(uri, columns,
null, null, null)) {
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
name = cursor.getString(0);
}
if (name == null)
name = Uri.decode(uri.getLastPathSegment());
int idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
name = name.substring(idx + 1);
}
boolean isZip = name.toLowerCase(Locale.ENGLISH).endsWith(".zip");
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else if (!isZip)
throw new IllegalArgumentException(getResources().getString(R.string.bad_extension_error));
if (isZip) {
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri));
BufferedReader reader = new BufferedReader(new InputStreamReader(zip))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory())
continue;
name = entry.getName();
idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
continue;
name = name.substring(name.lastIndexOf('/') + 1);
}
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else
continue;
Config config = null;
try {
config = Config.parse(reader);
} catch (Exception e) {
throwables.add(e);
}
if (config != null)
futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
}
}
} else {
futureTunnels.add(Application.getTunnelManager().create(name,
Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
}
if (futureTunnels.isEmpty()) {
if (throwables.size() == 1)
throw throwables.get(0);
else if (throwables.isEmpty())
throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
}
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
}).whenComplete((future, exception) -> {
if (exception != null) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} else {
future.whenComplete((ignored1, ignored2) -> {
final List<ObservableTunnel> tunnels = new ArrayList<>(futureTunnels.size());
for (final CompletableFuture<ObservableTunnel> futureTunnel : futureTunnels) {
ObservableTunnel tunnel = null;
try {
tunnel = futureTunnel.getNow(null);
} catch (final Exception e) {
throwables.add(e);
}
if (tunnel != null)
tunnels.add(tunnel);
}
onTunnelImportFinished(tunnels, throwables);
});
}
});
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
if (checkedItems != null) {
for (final Integer i : checkedItems)
actionModeListener.setItemChecked(i, true);
}
}
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
switch (requestCode) {
case REQUEST_IMPORT:
if (resultCode == Activity.RESULT_OK && data != null)
importTunnel(data.getData());
return;
case IntentIntegrator.REQUEST_CODE:
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null && result.getContents() != null) {
importTunnel(result.getContents());
}
return;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
binding.createFab.setOnClickListener(v -> {
final AddTunnelsSheet bottomSheet = new AddTunnelsSheet();
bottomSheet.setTargetFragment(this, REQUEST_TARGET_FRAGMENT);
bottomSheet.show(getParentFragmentManager(), "BOTTOM_SHEET");
});
binding.executePendingBindings();
EdgeToEdge.setUpRoot((ViewGroup) binding.getRoot());
EdgeToEdge.setUpFAB(binding.createFab);
EdgeToEdge.setUpScrollingContent(binding.tunnelList, binding.createFab);
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onPause() {
super.onPause();
}
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
@Override
public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) {
if (binding == null)
return;
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
if (newTunnel != null)
viewForTunnel(newTunnel, tunnels).setSingleSelected(true);
if (oldTunnel != null)
viewForTunnel(oldTunnel, tunnels).setSingleSelected(false);
});
}
private void showSnackbar(final CharSequence message) {
if (binding != null) {
final Snackbar snackbar = Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG);
snackbar.setAnchorView(binding.createFab);
snackbar.show();
}
}
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getResources().getQuantityString(R.plurals.delete_success, count, count);
} else {
final String error = ErrorMessages.get(throwable);
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
Log.e(TAG, message, throwable);
}
showSnackbar(message);
}
private void onTunnelImportFinished(final List<ObservableTunnel> tunnels, final Collection<Throwable> throwables) {
String message = null;
for (final Throwable throwable : throwables) {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.import_error, error);
Log.e(TAG, message, throwable);
}
if (tunnels.size() == 1 && throwables.isEmpty())
message = getString(R.string.import_success, tunnels.get(0).getName());
else if (tunnels.isEmpty() && throwables.size() == 1)
/* Use the exception message from above. */ ;
else if (throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_total_success,
tunnels.size(), tunnels.size());
else if (!throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_partial_success,
tunnels.size() + throwables.size(),
tunnels.size(), tunnels.size() + throwables.size());
showSnackbar(message);
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (binding == null) {
return;
}
binding.setFragment(this);
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
if (actionMode == null) {
setSelectedTunnel(tunnel);
} else {
actionModeListener.toggleItemChecked(position);
}
});
binding.getRoot().setOnLongClickListener(clicked -> {
actionModeListener.toggleItemChecked(position);
return true;
});
if (actionMode != null)
((MultiselectableRelativeLayout) binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
else
((MultiselectableRelativeLayout) binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
});
}
private MultiselectableRelativeLayout viewForTunnel(final ObservableTunnel tunnel, final List tunnels) {
return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
}
private final class ActionModeListener implements ActionMode.Callback {
private final Collection<Integer> checkedItems = new HashSet<>();
@Nullable private Resources resources;
public ArrayList<Integer> getCheckedItems() {
return new ArrayList<>(checkedItems);
}
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<ObservableTunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(ObservableTunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
});
checkedItems.clear();
mode.finish();
return true;
case R.id.menu_action_select_all:
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (int i = 0; i < tunnels.size(); ++i) {
setItemChecked(i, true);
}
});
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
actionMode = mode;
if (getActivity() != null) {
resources = getActivity().getResources();
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@Override
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
@Override
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
updateTitle(mode);
return false;
}
void setItemChecked(final int position, final boolean checked) {
if (checked) {
checkedItems.add(position);
} else {
checkedItems.remove(position);
}
final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
((AppCompatActivity) getActivity()).startSupportActionMode(this);
} else if (actionMode != null && checkedItems.isEmpty()) {
actionMode.finish();
}
if (adapter != null)
adapter.notifyItemChanged(position);
updateTitle(actionMode);
}
void toggleItemChecked(final int position) {
setItemChecked(position, !checkedItems.contains(position));
}
private void updateTitle(@Nullable final ActionMode mode) {
if (mode == null) {
return;
}
final int count = checkedItems.size();
if (count == 0) {
mode.setTitle("");
} else {
mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
}
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import android.graphics.drawable.Drawable;
import com.wireguard.android.BR;
import com.wireguard.util.Keyed;
public class ApplicationData extends BaseObservable implements Keyed<String> {
private final Drawable icon;
private final String name;
private final String packageName;
private boolean excludedFromTunnel;
public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) {
this.icon = icon;
this.name = name;
this.packageName = packageName;
this.excludedFromTunnel = excludedFromTunnel;
}
public Drawable getIcon() {
return icon;
}
@Override
public String getKey() {
return name;
}
public String getName() {
return name;
}
public String getPackageName() {
return packageName;
}
@Bindable
public boolean isExcludedFromTunnel() {
return excludedFromTunnel;
}
public void setExcludedFromTunnel(final boolean excludedFromTunnel) {
this.excludedFromTunnel = excludedFromTunnel;
notifyPropertyChanged(BR.excludedFromTunnel);
}
}
@@ -0,0 +1,142 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.android.backend.Statistics;
import com.wireguard.android.backend.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config;
import com.wireguard.util.Keyed;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
/**
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
public class ObservableTunnel extends BaseObservable implements Keyed<String>, Tunnel {
private final TunnelManager manager;
@Nullable private Config config;
private State state;
private String name;
@Nullable private Statistics statistics;
ObservableTunnel(final TunnelManager manager, final String name,
@Nullable final Config config, final State state) {
this.name = name;
this.manager = manager;
this.config = config;
this.state = state;
}
public CompletionStage<Void> delete() {
return manager.delete(this);
}
@Bindable
@Nullable
public Config getConfig() {
if (config == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
return config;
}
public CompletionStage<Config> getConfigAsync() {
if (config == null)
return manager.getTunnelConfig(this);
return CompletableFuture.completedFuture(config);
}
@Override
public String getKey() {
return name;
}
@Override
@Bindable
public String getName() {
return name;
}
@Bindable
public State getState() {
return state;
}
public CompletionStage<State> getStateAsync() {
return TunnelManager.getTunnelState(this);
}
@Bindable
@Nullable
public Statistics getStatistics() {
if (statistics == null || statistics.isStale())
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
return statistics;
}
public CompletionStage<Statistics> getStatisticsAsync() {
if (statistics == null || statistics.isStale())
return TunnelManager.getTunnelStatistics(this);
return CompletableFuture.completedFuture(statistics);
}
Config onConfigChanged(final Config config) {
this.config = config;
notifyPropertyChanged(BR.config);
return config;
}
String onNameChanged(final String name) {
this.name = name;
notifyPropertyChanged(BR.name);
return name;
}
State onStateChanged(final State state) {
if (state != State.UP)
onStatisticsChanged(null);
this.state = state;
notifyPropertyChanged(BR.state);
return state;
}
@Override
public void onStateChange(final State newState) {
onStateChanged(state);
}
@Nullable
Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
this.statistics = statistics;
notifyPropertyChanged(BR.statistics);
return statistics;
}
public CompletionStage<Config> setConfig(final Config config) {
if (!config.equals(this.config))
return manager.setTunnelConfig(this, config);
return CompletableFuture.completedFuture(this.config);
}
public CompletionStage<String> setName(final String name) {
if (!name.equals(this.name))
return manager.setTunnelName(this, name);
return CompletableFuture.completedFuture(this.name);
}
public CompletionStage<State> setState(final State state) {
if (state != this.state)
return manager.setTunnelState(this, state);
return CompletableFuture.completedFuture(this.state);
}
}
@@ -0,0 +1,301 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.backend.Tunnel;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.backend.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java9.util.Comparators;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
/**
* Maintains and mediates changes to the set of available WireGuard tunnels,
*/
public final class TunnelManager extends BaseObservable {
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> completableTunnels = new CompletableFuture<>();
private final ConfigStore configStore;
private final Context context = Application.get();
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
private final ObservableSortedKeyedList<String, ObservableTunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
private boolean haveLoaded;
@Nullable private ObservableTunnel lastUsedTunnel;
public TunnelManager(final ConfigStore configStore) {
this.configStore = configStore;
}
static CompletionStage<State> getTunnelState(final ObservableTunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged);
}
static CompletionStage<Statistics> getTunnelStatistics(final ObservableTunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged);
}
private ObservableTunnel addToList(final String name, @Nullable final Config config, final State state) {
final ObservableTunnel tunnel = new ObservableTunnel(this, name, config, state);
tunnels.add(tunnel);
return tunnel;
}
public CompletionStage<ObservableTunnel> create(final String name, @Nullable final Config config) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
}
CompletionStage<Void> delete(final ObservableTunnel tunnel) {
final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return Application.getAsyncWorker().runAsync(() -> {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN, null);
try {
configStore.delete(tunnel.getName());
} catch (final Exception e) {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
// Re-throw the exception to fail the completion.
throw e;
}
}).whenComplete((x, e) -> {
if (e == null)
return;
// Failure, put the tunnel back.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
});
}
@Bindable
@Nullable
public ObservableTunnel getLastUsedTunnel() {
return lastUsedTunnel;
}
CompletionStage<Config> getTunnelConfig(final ObservableTunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
.thenApply(tunnel::onConfigChanged);
}
public CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames()), this::onTunnelsLoaded)
.whenComplete(ExceptionLoggers.E);
}
@SuppressWarnings("unchecked")
private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
for (final String name : present)
addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
if (lastUsedName != null)
setLastUsedTunnel(tunnels.get(lastUsedName));
final CompletableFuture<Void>[] toComplete;
synchronized (delayedLoadRestoreTunnels) {
haveLoaded = true;
toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
delayedLoadRestoreTunnels.clear();
}
restoreState(true).whenComplete((v, t) -> {
for (final CompletableFuture<Void> f : toComplete) {
if (t == null)
f.complete(v);
else
f.completeExceptionally(t);
}
});
completableTunnels.complete(tunnels);
}
public void refreshTunnelStates() {
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames())
.thenAccept(running -> {
for (final ObservableTunnel tunnel : tunnels)
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
})
.whenComplete(ExceptionLoggers.E);
}
public CompletionStage<Void> restoreState(final boolean force) {
if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
return CompletableFuture.completedFuture(null);
synchronized (delayedLoadRestoreTunnels) {
if (!haveLoaded) {
final CompletableFuture<Void> f = new CompletableFuture<>();
delayedLoadRestoreTunnels.add(f);
return f;
}
}
final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
if (previouslyRunning == null)
return CompletableFuture.completedFuture(null);
return CompletableFuture.allOf(StreamSupport.stream(tunnels)
.filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
.map(tunnel -> setTunnelState(tunnel, State.UP))
.toArray(CompletableFuture[]::new));
}
public void saveState() {
final Set<String> runningTunnels = StreamSupport.stream(tunnels)
.filter(tunnel -> tunnel.getState() == State.UP)
.map(ObservableTunnel::getName)
.collect(Collectors.toUnmodifiableSet());
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
}
private void setLastUsedTunnel(@Nullable final ObservableTunnel tunnel) {
if (tunnel == lastUsedTunnel)
return;
lastUsedTunnel = tunnel;
notifyPropertyChanged(BR.lastUsedTunnel);
if (tunnel != null)
Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
else
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply();
}
CompletionStage<Config> setTunnelConfig(final ObservableTunnel tunnel, final Config config) {
return Application.getAsyncWorker().supplyAsync(() -> {
Application.getBackend().setState(tunnel, tunnel.getState(), config);
return configStore.save(tunnel.getName(), config);
}).thenApply(tunnel::onConfigChanged);
}
CompletionStage<String> setTunnelName(final ObservableTunnel tunnel, final String name) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return Application.getAsyncWorker().supplyAsync(() -> {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN, null);
configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
return newName;
}).whenComplete((newName, e) -> {
// On failure, we don't know what state the tunnel might be in. Fix that.
if (e != null)
getTunnelState(tunnel);
// Add the tunnel back to the manager, under whatever name it thinks it has.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
});
}
CompletionStage<State> setTunnelState(final ObservableTunnel tunnel, final State state) {
// Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(config ->
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state, config))
).whenComplete((newState, e) -> {
// Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(e == null ? newState : tunnel.getState());
if (e == null && newState == State.UP)
setLastUsedTunnel(tunnel);
saveState();
});
}
public static final class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, @Nullable final Intent intent) {
final TunnelManager manager = Application.getTunnelManager();
if (intent == null)
return;
final String action = intent.getAction();
if (action == null)
return;
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
manager.refreshTunnelStates();
return;
}
/* We disable the below, for now, as the security model of allowing this
* might take a bit more consideration.
*/
if (true)
return;
final State state;
if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
state = State.UP;
else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
state = State.DOWN;
else
return;
final String tunnelName = intent.getStringExtra("tunnel");
if (tunnelName == null)
return;
manager.getTunnels().thenAccept(tunnels -> {
final ObservableTunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -0,0 +1,112 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.DownloadsFileSaver;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* Preference implementing a button that asynchronously exports logs.
*/
public class LogExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public LogExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportLog() {
Application.getAsyncWorker().supplyAsync(() -> {
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-log.txt", "text/plain", true);
try {
final Process process = Runtime.getRuntime().exec(new String[]{
"logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"});
try (final BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
final BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())))
{
String line;
while ((line = stdout.readLine()) != null) {
outputFile.getOutputStream().write(line.getBytes());
outputFile.getOutputStream().write('\n');
}
outputFile.getOutputStream().close();
stdout.close();
if (process.waitFor() != 0) {
final StringBuilder errors = new StringBuilder();
errors.append(R.string.logcat_error);
while ((line = stderr.readLine()) != null)
errors.append(line);
throw new Exception(errors.toString());
}
}
} catch (final Exception e) {
outputFile.delete();
throw e;
}
return outputFile.getFileName();
}).whenComplete(this::exportLogComplete);
}
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.log_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.log_export_summary) :
getContext().getString(R.string.log_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.log_export_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportLog();
}
});
}
}
@@ -0,0 +1,92 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import android.content.Intent;
import android.system.OsConstants;
import android.util.AttributeSet;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.ModuleLoader;
import com.wireguard.android.util.ToolsInstaller;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
public class ModuleDownloaderPreference extends Preference {
private State state = State.INITIAL;
public ModuleDownloaderPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() {
return getContext().getString(state.messageResourceId);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.module_installer_title);
}
@Override
protected void onClick() {
setState(State.WORKING);
Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult);
}
private void onDownloadResult(final Integer result, @Nullable final Throwable throwable) {
if (throwable != null) {
setState(State.FAILURE);
Toast.makeText(getContext(), ErrorMessages.get(throwable), Toast.LENGTH_LONG).show();
} else if (result == OsConstants.ENOENT)
setState(State.NOTFOUND);
else if (result == OsConstants.EXIT_SUCCESS) {
setState(State.SUCCESS);
Application.getAsyncWorker().runAsync(() -> {
Thread.sleep(1000 * 5);
Intent i = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
if (i == null)
return;
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Application.get().startActivity(i);
System.exit(0);
});
} else
setState(State.FAILURE);
}
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
if (isEnabled() != state.shouldEnableView)
setEnabled(state.shouldEnableView);
notifyChanged();
}
private enum State {
INITIAL(R.string.module_installer_initial, true),
FAILURE(R.string.module_installer_error, true),
WORKING(R.string.module_installer_working, false),
SUCCESS(R.string.module_installer_success, false),
NOTFOUND(R.string.module_installer_not_found, false);
private final int messageResourceId;
private final boolean shouldEnableView;
State(final int messageResourceId, final boolean shouldEnableView) {
this.messageResourceId = messageResourceId;
this.shouldEnableView = shouldEnableView;
}
}
}
@@ -0,0 +1,102 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ToolsInstaller;
/**
* Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
* result as the preference summary.
*/
public class ToolsInstallerPreference extends Preference {
private State state = State.INITIAL;
public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() {
return getContext().getString(state.messageResourceId);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.tools_installer_title);
}
@Override
public void onAttached() {
super.onAttached();
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
}
private void onCheckResult(final int state, @Nullable final Throwable throwable) {
if (throwable != null || state == ToolsInstaller.ERROR)
setState(State.INITIAL);
else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
setState(State.ALREADY);
else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
setState(State.INITIAL_MAGISK);
else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
setState(State.INITIAL_SYSTEM);
else
setState(State.INITIAL);
}
@Override
protected void onClick() {
setState(State.WORKING);
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
}
private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
if (throwable != null)
setState(State.FAILURE);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
setState(State.SUCCESS_MAGISK);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
setState(State.SUCCESS_SYSTEM);
else
setState(State.FAILURE);
}
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
if (isEnabled() != state.shouldEnableView)
setEnabled(state.shouldEnableView);
notifyChanged();
}
private enum State {
INITIAL(R.string.tools_installer_initial, true),
ALREADY(R.string.tools_installer_already, false),
FAILURE(R.string.tools_installer_failure, true),
WORKING(R.string.tools_installer_working, false),
INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
private final int messageResourceId;
private final boolean shouldEnableView;
State(final int messageResourceId, final boolean shouldEnableView) {
this.messageResourceId = messageResourceId;
this.shouldEnableView = shouldEnableView;
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend;
import java.util.Locale;
public class VersionPreference extends Preference {
@Nullable private String versionSummary;
private String getBackendPrettyName(final Context context, final Backend backend) {
if (backend instanceof GoBackend)
return context.getString(R.string.type_name_kernel_module);
if (backend instanceof WgQuickBackend)
return context.getString(R.string.type_name_go_userspace);
return "";
}
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
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 = exception == null
? getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), version)
: getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH));
notifyChanged();
});
});
}
@Nullable
@Override
public CharSequence getSummary() {
return versionSummary;
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
}
@Override
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/"));
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
}
}
@@ -0,0 +1,119 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.model.ObservableTunnel;
import com.wireguard.android.util.DownloadsFileSaver;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import com.wireguard.config.Config;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java9.util.concurrent.CompletableFuture;
/**
* Preference implementing a button that asynchronously exports config zips.
*/
public class ZipExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportZip() {
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
}
private void exportZip(final List<ObservableTunnel> tunnels) {
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final ObservableTunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
if (futureConfigs.isEmpty()) {
exportZipComplete(null, new IllegalArgumentException(
getContext().getString(R.string.no_tunnels_error)));
return;
}
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
if (exception != null)
throw exception;
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-export.zip", "application/zip", true);
try (ZipOutputStream zip = new ZipOutputStream(outputFile.getOutputStream())) {
for (int i = 0; i < futureConfigs.size(); ++i) {
zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
zip.write(futureConfigs.get(i).getNow(null).
toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
zip.closeEntry();
} catch (final Exception e) {
outputFile.delete();
throw e;
}
return outputFile.getFileName();
}).whenComplete(this::exportZipComplete));
}
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.zip_export_summary) :
getContext().getString(R.string.zip_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.zip_export_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportZip();
}
});
}
}
@@ -0,0 +1,67 @@
/*
* Copyright © 2017-2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.ui
import android.view.View
import android.view.ViewGroup
import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
/**
* A utility for edge-to-edge display. It provides several features needed to make the app
* displayed edge-to-edge on Android Q with gestural navigation.
*/
object EdgeToEdge {
@JvmStatic
fun setUpRoot(root: ViewGroup) {
root.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
@JvmStatic
fun setUpScrollingContent(scrollingContent: ViewGroup, fab: ExtendedFloatingActionButton?) {
val originalPaddingLeft = scrollingContent.paddingLeft
val originalPaddingRight = scrollingContent.paddingRight
val originalPaddingBottom = scrollingContent.paddingBottom
val fabPaddingBottom = fab?.height ?: 0
val originalMarginTop = scrollingContent.marginTop
scrollingContent.setOnApplyWindowInsetsListener { _, windowInsets ->
scrollingContent.updatePadding(
left = originalPaddingLeft + windowInsets.systemWindowInsetLeft,
right = originalPaddingRight + windowInsets.systemWindowInsetRight,
bottom = originalPaddingBottom + fabPaddingBottom + windowInsets.systemWindowInsetBottom
)
scrollingContent.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = originalMarginTop + windowInsets.systemWindowInsetTop
}
windowInsets
}
}
@JvmStatic
fun setUpFAB(fab: ExtendedFloatingActionButton) {
val originalMarginLeft = fab.marginLeft
val originalMarginRight = fab.marginRight
val originalMarginBottom = fab.marginBottom
fab.setOnApplyWindowInsetsListener { _, windowInsets ->
fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = originalMarginLeft + windowInsets.systemWindowInsetLeft
rightMargin = originalMarginRight + windowInsets.systemWindowInsetRight
bottomMargin = originalMarginBottom + windowInsets.systemWindowInsetBottom
}
windowInsets
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
import android.widget.TextView;
/**
* Standalone utilities for interacting with the system clipboard.
*/
public final class ClipboardUtils {
private ClipboardUtils() {
// Prevent instantiation
}
public static void copyTextView(final View view) {
if (!(view instanceof TextView))
return;
final CharSequence text = ((TextView) view).getText();
if (text == null || text.length() == 0)
return;
final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
if (!(service instanceof ClipboardManager))
return;
final CharSequence description = view.getContentDescription();
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
}
}
@@ -0,0 +1,98 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import com.wireguard.android.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class DownloadsFileSaver {
public static class DownloadsFile {
private Context context;
private OutputStream outputStream;
private String fileName;
private Uri uri;
private DownloadsFile(final Context context, final OutputStream outputStream, final String fileName, final Uri uri) {
this.context = context;
this.outputStream = outputStream;
this.fileName = fileName;
this.uri = uri;
}
public OutputStream getOutputStream() { return outputStream; }
public String getFileName() { return fileName; }
public void delete() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getContentResolver().delete(uri, null, null);
else
new File(fileName).delete();
}
}
public static DownloadsFile save(final Context context, final String name, final String mimeType, final boolean overwriteExisting) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
final ContentResolver contentResolver = context.getContentResolver();
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), new String[]{name});
final ContentValues contentValues = new ContentValues();
contentValues.put(MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaColumns.MIME_TYPE, mimeType);
final Uri contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
if (contentUri == null)
throw new IOException(context.getString(R.string.create_downloads_file_error));
final OutputStream contentStream = contentResolver.openOutputStream(contentUri);
if (contentStream == null)
throw new IOException(context.getString(R.string.create_downloads_file_error));
@SuppressWarnings("deprecation")
Cursor cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DATA}, null, null, null);
String path = null;
if (cursor != null) {
try {
if (cursor.moveToFirst())
path = cursor.getString(0);
} finally {
cursor.close();
}
}
if (path == null) {
path = "Download/";
cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DISPLAY_NAME}, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst())
path += cursor.getString(0);
} finally {
cursor.close();
}
}
}
return new DownloadsFile(context, contentStream, path, contentUri);
} else {
@SuppressWarnings("deprecation")
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
final File file = new File(path, name);
if (!path.isDirectory() && !path.mkdirs())
throw new IOException(context.getString(R.string.create_output_dir_error));
return new DownloadsFile(context, new FileOutputStream(file), file.getAbsolutePath(), null);
}
}
}
@@ -0,0 +1,160 @@
/*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.res.Resources;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.backend.BackendException;
import com.wireguard.android.util.RootShell.RootShellException;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.ParseException;
import com.wireguard.crypto.Key.Format;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyFormatException.Type;
import java.net.InetAddress;
import java.util.EnumMap;
import java.util.Map;
import java9.util.Maps;
public final class ErrorMessages {
private static final Map<BadConfigException.Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of(
BadConfigException.Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key,
BadConfigException.Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number,
BadConfigException.Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value,
BadConfigException.Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute,
BadConfigException.Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section,
BadConfigException.Reason.MISSING_VALUE, R.string.bad_config_reason_missing_value,
BadConfigException.Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error,
BadConfigException.Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute,
BadConfigException.Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section
));
private static final Map<BackendException.Reason, Integer> BE_REASON_MAP = new EnumMap<>(Maps.of(
BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME, R.string.module_version_error,
BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE, R.string.tunnel_config_error,
BackendException.Reason.TUNNEL_MISSING_CONFIG, R.string.no_config_error,
BackendException.Reason.VPN_NOT_AUTHORIZED, R.string.vpn_not_authorized_error,
BackendException.Reason.UNABLE_TO_START_VPN, R.string.vpn_start_error,
BackendException.Reason.TUN_CREATION_ERROR, R.string.tun_create_error,
BackendException.Reason.GO_ACTIVATION_ERROR_CODE, R.string.tunnel_on_error
));
private static final Map<RootShellException.Reason, Integer> RSE_REASON_MAP = new EnumMap<>(Maps.of(
RootShellException.Reason.NO_ROOT_ACCESS, R.string.error_root,
RootShellException.Reason.SHELL_MARKER_COUNT_ERROR, R.string.shell_marker_count_error,
RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR, R.string.shell_exit_status_read_error,
RootShellException.Reason.SHELL_START_ERROR, R.string.shell_start_error,
RootShellException.Reason.CREATE_BIN_DIR_ERROR, R.string.create_bin_dir_error,
RootShellException.Reason.CREATE_TEMP_DIR_ERROR, R.string.create_temp_dir_error
));
private static final Map<Format, Integer> KFE_FORMAT_MAP = new EnumMap<>(Maps.of(
Format.BASE64, R.string.key_length_explanation_base64,
Format.BINARY, R.string.key_length_explanation_binary,
Format.HEX, R.string.key_length_explanation_hex
));
private static final Map<Type, Integer> KFE_TYPE_MAP = new EnumMap<>(Maps.of(
Type.CONTENTS, R.string.key_contents_error,
Type.LENGTH, R.string.key_length_error
));
private static final Map<Class, Integer> PE_CLASS_MAP = Maps.of(
InetAddress.class, R.string.parse_error_inet_address,
InetEndpoint.class, R.string.parse_error_inet_endpoint,
InetNetwork.class, R.string.parse_error_inet_network,
Integer.class, R.string.parse_error_integer
);
private ErrorMessages() {
// Prevent instantiation
}
public static String get(@Nullable final Throwable throwable) {
final Resources resources = Application.get().getResources();
if (throwable == null)
return resources.getString(R.string.unknown_error);
final Throwable rootCause = rootCause(throwable);
final String message;
if (rootCause instanceof BadConfigException) {
final BadConfigException bce = (BadConfigException) rootCause;
final String reason = getBadConfigExceptionReason(resources, bce);
final String context = bce.getLocation() == Location.TOP_LEVEL ?
resources.getString(R.string.bad_config_context_top_level,
bce.getSection().getName()) :
resources.getString(R.string.bad_config_context,
bce.getSection().getName(),
bce.getLocation().getName());
final String explanation = getBadConfigExceptionExplanation(resources, bce);
message = resources.getString(R.string.bad_config_error, reason, context) + explanation;
} else if (rootCause instanceof BackendException) {
final BackendException be = (BackendException) rootCause;
message = resources.getString(BE_REASON_MAP.get(be.getReason()), be.getFormat());
} else if (rootCause instanceof RootShellException) {
final RootShellException rse = (RootShellException) rootCause;
message = resources.getString(RSE_REASON_MAP.get(rse.getReason()), rse.getFormat());
} else if (rootCause.getMessage() != null) {
message = rootCause.getMessage();
} else {
final String errorType = rootCause.getClass().getSimpleName();
message = resources.getString(R.string.generic_error, errorType);
}
return message;
}
private static String getBadConfigExceptionExplanation(final Resources resources,
final BadConfigException bce) {
if (bce.getCause() instanceof KeyFormatException) {
final KeyFormatException kfe = (KeyFormatException) bce.getCause();
if (kfe.getType() == Type.LENGTH)
return resources.getString(KFE_FORMAT_MAP.get(kfe.getFormat()));
} else if (bce.getCause() instanceof ParseException) {
final ParseException pe = (ParseException) bce.getCause();
if (pe.getMessage() != null)
return ": " + pe.getMessage();
} else if (bce.getLocation() == Location.LISTEN_PORT) {
return resources.getString(R.string.bad_config_explanation_udp_port);
} else if (bce.getLocation() == Location.MTU) {
return resources.getString(R.string.bad_config_explanation_positive_number);
} else if (bce.getLocation() == Location.PERSISTENT_KEEPALIVE) {
return resources.getString(R.string.bad_config_explanation_pka);
}
return "";
}
private static String getBadConfigExceptionReason(final Resources resources,
final BadConfigException bce) {
if (bce.getCause() instanceof KeyFormatException) {
final KeyFormatException kfe = (KeyFormatException) bce.getCause();
return resources.getString(KFE_TYPE_MAP.get(kfe.getType()));
} else if (bce.getCause() instanceof ParseException) {
final ParseException pe = (ParseException) bce.getCause();
final String type = resources.getString(PE_CLASS_MAP.containsKey(pe.getParsingClass()) ?
PE_CLASS_MAP.get(pe.getParsingClass()) : R.string.parse_error_generic);
return resources.getString(R.string.parse_error_reason, type, pe.getText());
}
return resources.getString(BCE_REASON_MAP.get(bce.getReason()), bce.getText());
}
private static Throwable rootCause(final Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null) {
if (cause instanceof BadConfigException || cause instanceof BackendException ||
cause instanceof RootShellException)
break;
final Throwable nextCause = cause.getCause();
if (nextCause instanceof RemoteException)
break;
cause = nextCause;
}
return cause;
}
}
@@ -0,0 +1,36 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.annotation.Nullable;
import android.util.Log;
import java9.util.function.BiConsumer;
/**
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
* {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain.
*/
public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
D(Log.DEBUG),
E(Log.ERROR);
private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
private final int priority;
ExceptionLoggers(final int priority) {
this.priority = priority;
}
@Override
public void accept(final Object result, @Nullable final Throwable throwable) {
if (throwable != null)
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable));
else if (priority <= Log.DEBUG)
Log.println(priority, TAG, "Future completed successfully");
}
}
@@ -0,0 +1,16 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.content.Context
import android.util.TypedValue
import androidx.annotation.AttrRes
fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attrRes, typedValue, true)
return typedValue.data
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import androidx.preference.Preference;
import android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity;
public final class FragmentUtils {
private FragmentUtils() {
// Prevent instantiation
}
public static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
if (context instanceof ContextThemeWrapper) {
if (context instanceof SettingsActivity) {
return ((SettingsActivity) context);
}
}
return null;
}
}
@@ -0,0 +1,186 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import android.system.OsConstants;
import android.util.Base64;
import com.wireguard.android.util.RootShell.RootShellException;
import net.i2p.crypto.eddsa.EdDSAEngine;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
public class ModuleLoader {
private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig";
private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
private static final String MODULE_NAME = "wireguard-%s.ko";
private final RootShell rootShell;
private final String userAgent;
private final File moduleDir;
private final File tmpDir;
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
moduleDir = new File(context.getCacheDir(), "kmod");
tmpDir = new File(context.getCacheDir(), "tmp");
this.rootShell = rootShell;
this.userAgent = userAgent;
}
public boolean moduleMightExist() {
return moduleDir.exists() && moduleDir.isDirectory();
}
public void loadModule() throws IOException, RootShellException {
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
}
public static boolean isModuleLoaded() {
return new File("/sys/module/wireguard").exists();
}
private static final class Sha256Digest {
private byte[] bytes;
private Sha256Digest(final String hex) {
if (hex.length() != 64)
throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
bytes = new byte[32];
for (int i = 0; i < 32; ++i)
bytes[i] = (byte)Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
}
@Nullable
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
return null;
final String[] lines = signifyDigest.split("\n", 3);
if (lines.length != 3)
return null;
if (!lines[0].startsWith("untrusted comment: "))
return null;
final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
if (signatureBytes == null || signatureBytes.length != 64 + 10)
return null;
for (int i = 0; i < 10; ++i) {
if (signatureBytes[i] != publicKeyBytes[i])
return null;
}
try {
EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
byte[] rawPublicKeyBytes = new byte[32];
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
if (!signature.verify(signatureBytes, 10, 64))
return null;
} catch (final Exception ignored) {
return null;
}
Map<String, Sha256Digest> hashes = new HashMap<>();
for (final String line : lines[2].split("\n")) {
final String[] components = line.split(" ", 2);
if (components.length != 2)
return null;
try {
hashes.put(components[1], new Sha256Digest(components[0]));
} catch (final Exception ignored) {
return null;
}
}
return hashes;
}
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
final List<String> output = new ArrayList<>();
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
if (output.size() != 1 || output.get(0).length() != 64)
throw new InvalidParameterException("Invalid sha256 of /proc/version");
final String moduleName = String.format(MODULE_NAME, output.get(0));
HttpURLConnection connection = (HttpURLConnection)new URL(MODULE_LIST_URL).openConnection();
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new IOException("Hash list could not be found");
byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
int len;
try (final InputStream inputStream = connection.getInputStream()) {
len = inputStream.read(input);
}
if (len <= 0)
throw new IOException("Hash list was empty");
final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8));
if (modules == null)
throw new InvalidParameterException("The signature did not verify or invalid hash list format");
if (!modules.containsKey(moduleName))
return OsConstants.ENOENT;
connection = (HttpURLConnection)new URL(String.format(MODULE_URL, moduleName)).openConnection();
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new IOException("Module file could not be found, despite being on hash list");
tmpDir.mkdirs();
moduleDir.mkdir();
File tempFile = null;
try {
tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (final InputStream inputStream = connection.getInputStream();
final FileOutputStream outputStream = new FileOutputStream(tempFile)) {
int total = 0;
while ((len = inputStream.read(input)) > 0) {
total += len;
if (total > 1024 * 1024 * 15 /* 15 MiB */)
throw new IOException("File too big");
outputStream.write(input, 0, len);
digest.update(input, 0, len);
}
outputStream.getFD().sync();
}
if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes))
throw new IOException("Incorrect file hash");
if (!tempFile.renameTo(new File(moduleDir, moduleName)))
throw new IOException("Unable to rename to final destination");
} finally {
if (tempFile != null)
tempFile.delete();
}
return OsConstants.EXIT_SUCCESS;
}
}
@@ -0,0 +1,109 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.databinding.ObservableArrayList;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
import java.util.Collection;
import java.util.ListIterator;
import java.util.Objects;
/**
* ArrayList that allows looking up elements by some key property. As the key property must always
* be retrievable, this list cannot hold {@code null} elements. Because this class places no
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
* operations, require O(n) time.
*/
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
@Override
public boolean add(@Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to add a null element");
return super.add(e);
}
@Override
public void add(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to add a null element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
if (c.contains(null))
throw new NullPointerException("Trying to add a collection with null element(s)");
return super.addAll(c);
}
@Override
public boolean addAll(final int index, final Collection<? extends E> c) {
if (c.contains(null))
throw new NullPointerException("Trying to add a collection with null element(s)");
return super.addAll(index, c);
}
@Override
public boolean containsAllKeys(final Collection<K> keys) {
for (final K key : keys)
if (!containsKey(key))
return false;
return true;
}
@Override
public boolean containsKey(final K key) {
return indexOfKey(key) >= 0;
}
@Nullable
@Override
public E get(final K key) {
final int index = indexOfKey(key);
return index >= 0 ? get(index) : null;
}
@Nullable
@Override
public E getLast(final K key) {
final int index = lastIndexOfKey(key);
return index >= 0 ? get(index) : null;
}
@Override
public int indexOfKey(final K key) {
final ListIterator<E> iterator = listIterator();
while (iterator.hasNext()) {
final int index = iterator.nextIndex();
if (Objects.equals(iterator.next().getKey(), key))
return index;
}
return -1;
}
@Override
public int lastIndexOfKey(final K key) {
final ListIterator<E> iterator = listIterator(size());
while (iterator.hasPrevious()) {
final int index = iterator.previousIndex();
if (Objects.equals(iterator.previous().getKey(), key))
return index;
}
return -1;
}
@Override
public E set(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to set a null key");
return super.set(index, e);
}
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.databinding.ObservableList;
import com.wireguard.util.Keyed;
import com.wireguard.util.KeyedList;
/**
* A list that is both keyed and observable.
*/
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
extends KeyedList<K, E>, ObservableList<E> {
}
@@ -0,0 +1,198 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
/**
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
* array-based nature of this class, insertion and removal of elements with anything but the largest
* key still require O(n) time.
*/
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
@Nullable private final Comparator<? super K> comparator;
private final transient KeyList<K, E> keyList = new KeyList<>(this);
@SuppressWarnings("WeakerAccess")
public ObservableSortedKeyedArrayList() {
comparator = null;
}
public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
this.comparator = comparator;
}
public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
this();
addAll(c);
}
public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
this(other.comparator());
addAll(other);
}
@Override
public boolean add(final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0) {
// Skipping insertion is non-destructive if the new and existing objects are the same.
if (e == get(-insertionPoint - 1))
return false;
throw new IllegalArgumentException("Element with same key already exists in list");
}
super.add(insertionPoint, e);
return true;
}
@Override
public void add(final int index, final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0)
throw new IllegalArgumentException("Element with same key already exists in list");
if (insertionPoint != index)
throw new IndexOutOfBoundsException("Wrong index given for element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
boolean didChange = false;
for (final E e : c)
if (add(e))
didChange = true;
return didChange;
}
@Override
public boolean addAll(int index, final Collection<? extends E> c) {
for (final E e : c)
add(index++, e);
return true;
}
@Nullable
@Override
public Comparator<? super K> comparator() {
return comparator;
}
@Override
public K firstKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(0).getKey();
}
private int getInsertionPoint(final E e) {
if (comparator != null) {
return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
return -Collections.binarySearch(list, e.getKey()) - 1;
}
}
@Override
public int indexOfKey(final K key) {
final int index;
if (comparator != null) {
index = Collections.binarySearch(keyList, key, comparator);
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
index = Collections.binarySearch(list, key);
}
return index >= 0 ? index : -1;
}
@Override
public Set<K> keySet() {
return keyList;
}
@Override
public int lastIndexOfKey(final K key) {
// There can never be more than one element with the same key in the list.
return indexOfKey(key);
}
@Override
public K lastKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(size() - 1).getKey();
}
@Override
public E set(final int index, final E e) {
final int order;
if (comparator != null) {
order = comparator.compare(e.getKey(), get(index).getKey());
} else {
@SuppressWarnings("unchecked") final Comparable<? super K> key =
(Comparable<? super K>) e.getKey();
order = key.compareTo(get(index).getKey());
}
if (order != 0) {
// Allow replacement if the new key would be inserted adjacent to the replaced element.
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < index || insertionPoint > index + 1)
throw new IndexOutOfBoundsException("Wrong index given for element");
}
return super.set(index, e);
}
@Override
public Collection<E> values() {
return this;
}
private static final class KeyList<K, E extends Keyed<? extends K>>
extends AbstractList<K> implements Set<K> {
private final ObservableSortedKeyedArrayList<K, E> list;
private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
this.list = list;
}
@Override
public K get(final int index) {
return list.get(index).getKey();
}
@Override
public int size() {
return list.size();
}
@Override
@SuppressWarnings("EmptyMethod")
public Spliterator<K> spliterator() {
return super.spliterator();
}
}
}
@@ -0,0 +1,17 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
/**
* A list that is both sorted/keyed and observable.
*/
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
}
@@ -0,0 +1,93 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import com.wireguard.config.Peer;
import java.util.ArrayList;
import java.util.Collection;
public class ConfigProxy implements Parcelable {
public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator();
private final InterfaceProxy interfaze;
private final ObservableList<PeerProxy> peers = new ObservableArrayList<>();
private ConfigProxy(final Parcel in) {
interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader());
in.readTypedList(peers, PeerProxy.CREATOR);
for (final PeerProxy proxy : peers)
proxy.bind(this);
}
public ConfigProxy(final Config other) {
interfaze = new InterfaceProxy(other.getInterface());
for (final Peer peer : other.getPeers()) {
final PeerProxy proxy = new PeerProxy(peer);
peers.add(proxy);
proxy.bind(this);
}
}
public ConfigProxy() {
interfaze = new InterfaceProxy();
}
public PeerProxy addPeer() {
final PeerProxy proxy = new PeerProxy();
peers.add(proxy);
proxy.bind(this);
return proxy;
}
@Override
public int describeContents() {
return 0;
}
public InterfaceProxy getInterface() {
return interfaze;
}
public ObservableList<PeerProxy> getPeers() {
return peers;
}
public Config resolve() throws BadConfigException {
final Collection<Peer> resolvedPeers = new ArrayList<>();
for (final PeerProxy proxy : peers)
resolvedPeers.add(proxy.resolve());
return new Config.Builder()
.setInterface(interfaze.resolve())
.addPeers(resolvedPeers)
.build();
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(interfaze, flags);
dest.writeTypedList(peers);
}
private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> {
@Override
public ConfigProxy createFromParcel(final Parcel in) {
return new ConfigProxy(in);
}
@Override
public ConfigProxy[] newArray(final int size) {
return new ConfigProxy[size];
}
}
}
@@ -0,0 +1,190 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Interface;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair;
import java.net.InetAddress;
import java.util.List;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
public class InterfaceProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator();
private final ObservableList<String> excludedApplications = new ObservableArrayList<>();
private String addresses;
private String dnsServers;
private String listenPort;
private String mtu;
private String privateKey;
private String publicKey;
private InterfaceProxy(final Parcel in) {
addresses = in.readString();
dnsServers = in.readString();
in.readStringList(excludedApplications);
listenPort = in.readString();
mtu = in.readString();
privateKey = in.readString();
publicKey = in.readString();
}
public InterfaceProxy(final Interface other) {
addresses = Attribute.join(other.getAddresses());
final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers())
.map(InetAddress::getHostAddress)
.collect(Collectors.toUnmodifiableList());
dnsServers = Attribute.join(dnsServerStrings);
excludedApplications.addAll(other.getExcludedApplications());
listenPort = other.getListenPort().map(String::valueOf).orElse("");
mtu = other.getMtu().map(String::valueOf).orElse("");
final KeyPair keyPair = other.getKeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
}
public InterfaceProxy() {
addresses = "";
dnsServers = "";
listenPort = "";
mtu = "";
privateKey = "";
publicKey = "";
}
@Override
public int describeContents() {
return 0;
}
public void generateKeyPair() {
final KeyPair keyPair = new KeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Bindable
public String getAddresses() {
return addresses;
}
@Bindable
public String getDnsServers() {
return dnsServers;
}
public ObservableList<String> getExcludedApplications() {
return excludedApplications;
}
@Bindable
public String getListenPort() {
return listenPort;
}
@Bindable
public String getMtu() {
return mtu;
}
@Bindable
public String getPrivateKey() {
return privateKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
public Interface resolve() throws BadConfigException {
final Interface.Builder builder = new Interface.Builder();
if (!addresses.isEmpty())
builder.parseAddresses(addresses);
if (!dnsServers.isEmpty())
builder.parseDnsServers(dnsServers);
if (!excludedApplications.isEmpty())
builder.excludeApplications(excludedApplications);
if (!listenPort.isEmpty())
builder.parseListenPort(listenPort);
if (!mtu.isEmpty())
builder.parseMtu(mtu);
if (!privateKey.isEmpty())
builder.parsePrivateKey(privateKey);
return builder.build();
}
public void setAddresses(final String addresses) {
this.addresses = addresses;
notifyPropertyChanged(BR.addresses);
}
public void setDnsServers(final String dnsServers) {
this.dnsServers = dnsServers;
notifyPropertyChanged(BR.dnsServers);
}
public void setListenPort(final String listenPort) {
this.listenPort = listenPort;
notifyPropertyChanged(BR.listenPort);
}
public void setMtu(final String mtu) {
this.mtu = mtu;
notifyPropertyChanged(BR.mtu);
}
public void setPrivateKey(final String privateKey) {
this.privateKey = privateKey;
try {
publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
} catch (final KeyFormatException ignored) {
publicKey = "";
}
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(addresses);
dest.writeString(dnsServers);
dest.writeStringList(excludedApplications);
dest.writeString(listenPort);
dest.writeString(mtu);
dest.writeString(privateKey);
dest.writeString(publicKey);
}
private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> {
@Override
public InterfaceProxy createFromParcel(final Parcel in) {
return new InterfaceProxy(in);
}
@Override
public InterfaceProxy[] newArray(final int size) {
return new InterfaceProxy[size];
}
}
}
@@ -0,0 +1,380 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.Observable;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java9.util.Lists;
import java9.util.Sets;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
public class PeerProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator();
private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
));
private static final Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0");
private final List<String> dnsRoutes = new ArrayList<>();
private String allowedIps;
private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID;
private String endpoint;
@Nullable private InterfaceDnsListener interfaceDnsListener;
@Nullable private ConfigProxy owner;
@Nullable private PeerListListener peerListListener;
private String persistentKeepalive;
private String preSharedKey;
private String publicKey;
private int totalPeers;
private PeerProxy(final Parcel in) {
allowedIps = in.readString();
endpoint = in.readString();
persistentKeepalive = in.readString();
preSharedKey = in.readString();
publicKey = in.readString();
}
public PeerProxy(final Peer other) {
allowedIps = Attribute.join(other.getAllowedIps());
endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse("");
persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse("");
preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse("");
publicKey = other.getPublicKey().toBase64();
}
public PeerProxy() {
allowedIps = "";
endpoint = "";
persistentKeepalive = "";
preSharedKey = "";
publicKey = "";
}
public void bind(final ConfigProxy owner) {
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener == null)
interfaceDnsListener = new InterfaceDnsListener(this);
interfaze.addOnPropertyChangedCallback(interfaceDnsListener);
setInterfaceDns(interfaze.getDnsServers());
if (peerListListener == null)
peerListListener = new PeerListListener(this);
peers.addOnListChangedCallback(peerListListener);
setTotalPeers(peers.size());
this.owner = owner;
}
private void calculateAllowedIpsState() {
final AllowedIpsState newState;
if (totalPeers == 1) {
// String comparison works because we only care if allowedIps is a superset of one of
// the above sets of (valid) *networks*. We are not checking for a superset based on
// the individual addresses in each set.
final Collection<String> networkStrings = getAllowedIpsSet();
// If allowedIps contains both the wildcard and the public networks, then private
// networks aren't excluded!
if (networkStrings.containsAll(IPV4_WILDCARD))
newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD;
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
else
newState = AllowedIpsState.OTHER;
} else {
newState = AllowedIpsState.INVALID;
}
if (newState != allowedIpsState) {
allowedIpsState = newState;
notifyPropertyChanged(BR.ableToExcludePrivateIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
}
@Override
public int describeContents() {
return 0;
}
@Bindable
public String getAllowedIps() {
return allowedIps;
}
private Set<String> getAllowedIpsSet() {
return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps)));
}
@Bindable
public String getEndpoint() {
return endpoint;
}
@Bindable
public String getPersistentKeepalive() {
return persistentKeepalive;
}
@Bindable
public String getPreSharedKey() {
return preSharedKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
@Bindable
public boolean isAbleToExcludePrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|| allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD;
}
@Bindable
public boolean isExcludingPrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
}
public Peer resolve() throws BadConfigException {
final Peer.Builder builder = new Peer.Builder();
if (!allowedIps.isEmpty())
builder.parseAllowedIPs(allowedIps);
if (!endpoint.isEmpty())
builder.parseEndpoint(endpoint);
if (!persistentKeepalive.isEmpty())
builder.parsePersistentKeepalive(persistentKeepalive);
if (!preSharedKey.isEmpty())
builder.parsePreSharedKey(preSharedKey);
if (!publicKey.isEmpty())
builder.parsePublicKey(publicKey);
return builder.build();
}
public void setAllowedIps(final String allowedIps) {
this.allowedIps = allowedIps;
notifyPropertyChanged(BR.allowedIps);
calculateAllowedIpsState();
}
public void setEndpoint(final String endpoint) {
this.endpoint = endpoint;
notifyPropertyChanged(BR.endpoint);
}
public void setExcludingPrivateIps(final boolean excludingPrivateIps) {
if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps)
return;
final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS;
final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD;
final Collection<String> input = getAllowedIpsSet();
final int outputSize = input.size() - oldNetworks.size() + newNetworks.size();
final Collection<String> output = new LinkedHashSet<>(outputSize);
boolean replaced = false;
// Replace the first instance of the wildcard with the public network list, or vice versa.
for (final String network : input) {
if (oldNetworks.contains(network)) {
if (!replaced) {
for (final String replacement : newNetworks)
if (!output.contains(replacement))
output.add(replacement);
replaced = true;
}
} else if (!output.contains(network)) {
output.add(network);
}
}
// DNS servers only need to handled specially when we're excluding private IPs.
if (excludingPrivateIps)
output.addAll(dnsRoutes);
else
output.removeAll(dnsRoutes);
allowedIps = Attribute.join(output);
allowedIpsState = excludingPrivateIps ?
AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD;
notifyPropertyChanged(BR.allowedIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
private void setInterfaceDns(final CharSequence dnsServers) {
final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers))
.filter(server -> !server.contains(":"))
.map(server -> server + "/32")
.collect(Collectors.toUnmodifiableList());
if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
final Collection<String> input = getAllowedIpsSet();
final Collection<String> output = new LinkedHashSet<>(input.size() + 1);
// Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
for (final String network : input)
if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network))
output.add(network);
// Since output is a Set, this does the Right Thing™ (it does not duplicate networks).
output.addAll(newDnsRoutes);
// None of the public networks are /32s, so this cannot change the AllowedIPs state.
allowedIps = Attribute.join(output);
notifyPropertyChanged(BR.allowedIps);
}
dnsRoutes.clear();
dnsRoutes.addAll(newDnsRoutes);
}
public void setPersistentKeepalive(final String persistentKeepalive) {
this.persistentKeepalive = persistentKeepalive;
notifyPropertyChanged(BR.persistentKeepalive);
}
public void setPreSharedKey(final String preSharedKey) {
this.preSharedKey = preSharedKey;
notifyPropertyChanged(BR.preSharedKey);
}
public void setPublicKey(final String publicKey) {
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}
private void setTotalPeers(final int totalPeers) {
if (this.totalPeers == totalPeers)
return;
this.totalPeers = totalPeers;
calculateAllowedIpsState();
}
public void unbind() {
if (owner == null)
return;
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener != null)
interfaze.removeOnPropertyChangedCallback(interfaceDnsListener);
if (peerListListener != null)
peers.removeOnListChangedCallback(peerListListener);
peers.remove(this);
setInterfaceDns("");
setTotalPeers(0);
owner = null;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(allowedIps);
dest.writeString(endpoint);
dest.writeString(persistentKeepalive);
dest.writeString(preSharedKey);
dest.writeString(publicKey);
}
private enum AllowedIpsState {
CONTAINS_IPV4_PUBLIC_NETWORKS,
CONTAINS_IPV4_WILDCARD,
INVALID,
OTHER
}
private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback {
private final WeakReference<PeerProxy> weakPeerProxy;
private InterfaceDnsListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnPropertyChangedCallback(this);
return;
}
// This shouldn't be possible, but try to avoid a ClassCastException anyway.
if (!(sender instanceof InterfaceProxy))
return;
if (!(propertyId == BR._all || propertyId == BR.dnsServers))
return;
peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers());
}
}
private static final class PeerListListener
extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> {
private final WeakReference<PeerProxy> weakPeerProxy;
private PeerListListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onChanged(final ObservableList<PeerProxy> sender) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnListChangedCallback(this);
return;
}
peerProxy.setTotalPeers(sender.size());
}
@Override
public void onItemRangeChanged(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeInserted(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<PeerProxy> sender,
final int fromPosition, final int toPosition,
final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeRemoved(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
}
private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> {
@Override
public PeerProxy createFromParcel(final Parcel in) {
return new PeerProxy(in);
}
@Override
public PeerProxy[] newArray(final int size) {
return new PeerProxy[size];
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import androidx.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.crypto.Key;
/**
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
public class KeyInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
}
public static InputFilter newInstance() {
return new KeyInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
(dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -0,0 +1,59 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import com.wireguard.android.R;
public class MultiselectableRelativeLayout extends RelativeLayout {
private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
private boolean multiselected;
public MultiselectableRelativeLayout(final Context context) {
super(context);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected int[] onCreateDrawableState(final int extraSpace) {
if (multiselected) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(drawableState, STATE_MULTISELECTED);
return drawableState;
}
return super.onCreateDrawableState(extraSpace);
}
public void setMultiSelected(final boolean on) {
if (!multiselected) {
multiselected = true;
refreshDrawableState();
}
setActivated(on);
}
public void setSingleSelected(final boolean on) {
if (multiselected) {
multiselected = false;
refreshDrawableState();
}
setActivated(on);
}
}
@@ -0,0 +1,53 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import androidx.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.android.backend.Tunnel;
/**
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
public class NameInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
}
public static InputFilter newInstance() {
return new NameInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -0,0 +1,217 @@
/*
* Copyright © 2018 The Android Open Source Project
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.FloatProperty;
@RequiresApi(Build.VERSION_CODES.N)
public class SlashDrawable extends Drawable {
private static final float CENTER_X = 10.65f;
private static final float CENTER_Y = 11.869239f;
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
// Draw the slash washington-monument style; rotate to no-u-turn style
private static final float DEFAULT_ROTATION = -45f;
private static final long QS_ANIM_LENGTH = 350;
private static final float SCALE = 24f;
private static final float SLASH_HEIGHT = 28f;
// These values are derived in un-rotated (vertical) orientation
private static final float SLASH_WIDTH = 1.8384776f;
// Bottom is derived during animation
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
@Override
public Float get(final SlashDrawable object) {
return object.mCurrentSlashLength;
}
@Override
public void setValue(final SlashDrawable object, final float value) {
object.mCurrentSlashLength = value;
}
};
private final Drawable mDrawable;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path mPath = new Path();
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
private boolean mAnimationEnabled = true;
// Animate this value on change
private float mCurrentSlashLength;
private float mRotation;
private boolean mSlashed;
public SlashDrawable(final Drawable d) {
mDrawable = d;
}
@SuppressWarnings("deprecation")
@Override
public void draw(final Canvas canvas) {
canvas.save();
final Matrix m = new Matrix();
final int width = getBounds().width();
final int height = getBounds().height();
final float radiusX = scale(CORNER_RADIUS, width);
final float radiusY = scale(CORNER_RADIUS, height);
updateRect(
scale(LEFT, width),
scale(TOP, height),
scale(RIGHT, width),
scale(TOP + mCurrentSlashLength, height)
);
mPath.reset();
// Draw the slash vertically
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
// Rotate -45 + desired rotation
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
canvas.drawPath(mPath, mPaint);
// Rotate back to vertical
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
// Draw another rect right next to the first, for clipping
m.setTranslate(mSlashRect.width(), 0);
mPath.transform(m);
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
else
canvas.clipOutPath(mPath);
mDrawable.draw(canvas);
canvas.restore();
}
@Override
public int getIntrinsicHeight() {
return mDrawable.getIntrinsicHeight();
}
@Override
public int getIntrinsicWidth() {
return mDrawable.getIntrinsicWidth();
}
@SuppressWarnings("deprecation")
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
protected void onBoundsChange(final Rect bounds) {
super.onBoundsChange(bounds);
mDrawable.setBounds(bounds);
}
private float scale(final float frac, final int width) {
return frac * width;
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
mDrawable.setAlpha(alpha);
mPaint.setAlpha(alpha);
}
public void setAnimationEnabled(final boolean enabled) {
mAnimationEnabled = enabled;
}
@Override
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
mDrawable.setColorFilter(colorFilter);
mPaint.setColorFilter(colorFilter);
}
private void setDrawableTintList(@Nullable final ColorStateList tint) {
mDrawable.setTintList(tint);
}
public void setRotation(final float rotation) {
if (mRotation == rotation)
return;
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unchecked")
public void setSlashed(final boolean slashed) {
if (mSlashed == slashed) return;
mSlashed = slashed;
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
if (mAnimationEnabled) {
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
anim.setDuration(QS_ANIM_LENGTH);
anim.start();
} else {
mCurrentSlashLength = end;
invalidateSelf();
}
}
@Override
public void setTint(@ColorInt final int tintColor) {
super.setTint(tintColor);
mDrawable.setTint(tintColor);
mPaint.setColor(tintColor);
}
@Override
public void setTintList(@Nullable final ColorStateList tint) {
super.setTintList(tint);
setDrawableTintList(tint);
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
invalidateSelf();
}
@Override
public void setTintMode(final Mode tintMode) {
super.setTintMode(tintMode);
mDrawable.setTintMode(tintMode);
}
private void updateRect(final float left, final float top, final float right, final float bottom) {
mSlashRect.left = left;
mSlashRect.top = top;
mSlashRect.right = right;
mSlashRect.bottom = bottom;
}
}
@@ -0,0 +1,59 @@
/*
* Copyright © 2013 The Android Open Source Project
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.Switch;
public class ToggleSwitch extends Switch {
private boolean isRestoringState;
@Nullable private OnBeforeCheckedChangeListener listener;
public ToggleSwitch(final Context context) {
this(context, null);
}
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
isRestoringState = true;
super.onRestoreInstanceState(state);
isRestoringState = false;
}
@Override
public void setChecked(final boolean checked) {
if (checked == isChecked())
return;
if (isRestoringState || listener == null) {
super.setChecked(checked);
return;
}
setEnabled(false);
listener.onBeforeCheckedChanged(this, checked);
}
public void setCheckedInternal(final boolean checked) {
super.setChecked(checked);
setEnabled(true);
}
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
this.listener = listener;
}
public interface OnBeforeCheckedChangeListener {
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
}
}
@@ -0,0 +1,14 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
/**
* Interface for objects that have a identifying key of the given type.
*/
public interface Keyed<K> {
K getKey();
}
@@ -0,0 +1,32 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.List;
/**
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
* {@code null} elements.
*/
public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsAllKeys(Collection<K> keys);
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
int lastIndexOfKey(K key);
}
@@ -0,0 +1,31 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
/**
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
* applied to their keys.
*/
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
Comparator<? super K> comparator();
@Nullable
K firstKey();
Set<K> keySet();
@Nullable
K lastKey();
Collection<E> values();
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M3 5L5 5 5 3C3.9 3 3 3.9 3 5Zm0 8l2 0 0 -2 -2 0 0 2zm4 8l2 0 0 -2 -2 0 0 2zM3 9L5 9 5 7 3 7 3 9Zm10 -6l-2 0 0 2 2 0 0 -2zm6 0l0 2 2 0C21 3.9 20.1 3 19 3ZM5 21L5 19 3 19c0 1.1 0.9 2 2 2zm-2 -4l2 0 0 -2 -2 0 0 2zM9 3L7 3 7 5 9 5 9 3Zm2 18l2 0 0 -2 -2 0 0 2zm8 -8l2 0 0 -2 -2 0 0 2zm0 8c1.1 0 2 -0.9 2 -2l-2 0 0 2zm0 -12l2 0 0 -2 -2 0 0 2zm0 8l2 0 0 -2 -2 0 0 2zm-4 4l2 0 0 -2 -2 0 0 2zm0 -16l2 0 0 -2 -2 0 0 2zM7 17L17 17 17 7 7 7 7 17Zm2 -8l6 0 0 6 -6 0 0 -6z" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="2160"
android:viewportWidth="2160">
<group
android:scaleX="1"
android:scaleY="-1"
android:translateX="630"
android:translateY="1750">
<group>
<clip-path android:pathData="M0 1347.452l773.449 0L773.449 0 0 0Z" />
<group
android:translateX="349.0264"
android:translateY="572.616">
<path
android:fillColor="#ffffff"
android:pathData="M0 0c-12.169 -6.44 -21.541 -11.184 -30.71 -16.292 -37.523 -20.902 -69.603 -48.262 -95.162 -82.767 -8.264 -11.156 -13.945 -12.055 -26.528 -4.36 -163.692 100.101 -174.212 351.318 4.549 460.681 139.045 85.064 316.68 33.074 383.242 -94.85 12.614 -24.244 14.218 -61.567 6.228 -87 -27.582 -87.807 -92.71 -137.049 -182.1 -157.968 26.353 22.561 47.329 48.145 54.006 83.494 6.725 35.606 -0.388 67.807 -21.041 97.072 -31.371 44.451 -92.029 62.74 -142.721 43.492 -55.035 -20.896 -85.181 -71.123 -79.747 -132.863C-124.935 51.288 -81.419 14.12 0 0" />
</group>
<group android:translateY="285.9856">
<path
android:fillColor="#ffffff"
android:pathData="M0 0C13.148 88.712 117.033 170.407 204.881 161.087 177.673 124.291 165.104 82.664 162.071 41.145 132.88 35.769 105.368 32.152 78.66 25.373 52.364 18.698 26.882 8.816 0 0" />
</group>
<group
android:translateX="580.2814"
android:translateY="1243.915">
<path
android:fillColor="#ffffff"
android:pathData="M0 0C4.917 3.762 9.98 6.922 16.085 1.891 19.557 -0.97 22.93 -3.933 27.136 -7.523 21.915 -10.28 17.676 -12.599 13.355 -14.779 7.307 -17.83 2.785 -15.792 -0.877 -10.972 -3.847 -7.062 -4.384 -3.354 0 0m71.552 -730.934c-7.403 6.401 -12.094 6.399 -20.775 0.845 -29.454 -18.844 -59.602 -36.696 -90.239 -53.556 -17.562 -9.664 -36.584 -16.675 -58.61 -26.516 7.564 -1.952 11.203 -2.865 14.829 -3.83 82.337 -21.913 126.326 -94.196 106.841 -175.157 -17.329 -71.999 -90.422 -118.033 -161.255 -105.889 -59.053 10.125 -110.601 59.161 -119.21 117.917 -9.382 64.032 22.508 125.618 79.246 151.417 31.472 14.31 63.79 26.766 95.186 41.229 35.697 16.445 74.29 29.435 105.458 52.322 77.355 56.801 125.124 135.006 143.747 229.401 11.155 56.543 10.401 112.837 -15.467 166.524 -19.851 41.202 -52.429 71.133 -87.429 98.447 -36.018 28.108 -74.148 53.518 -110.002 81.821 -9.702 7.659 -16.252 20.865 -20.742 32.84 -1.903 5.075 4.287 18.838 8.426 19.581 21.985 3.946 44.45 5.978 66.818 6.823 25.82 0.974 51.713 0.148 77.571 -0.192 5.606 -0.073 13.217 0.653 16.439 -2.514 13.394 -13.167 23.897 -4.697 33.194 3.965 7.823 7.29 13.399 16.992 19.62 25.168 -3.775 0.555 -11.519 2.505 -19.304 2.689 -26.003 0.616 -52.035 0.221 -78.021 1.176 -4.63 0.17 -9.09 4.935 -13.629 7.579 4.776 1.898 9.537 5.399 14.33 5.444 44.849 0.421 89.703 0.25 134.594 0.25 0.052 23.336 -31.136 55.291 -58.846 63.95 -0.207 -3.158 -0.4 -6.097 -0.606 -9.233C106.184 0.913 79.16 1.426 54.61 14.481 48.141 17.921 43.912 25.57 38.647 31.279 32.019 38.467 26.58 47.709 18.5 52.399 1.934 62.014 -16.148 68.982 -33.431 77.4c-61.418 29.914 -126.278 28.862 -195.946 22.484 41.644 -9.693 79.255 -18.447 116.865 -27.202 -0.428 -2.286 -0.856 -4.573 -1.284 -6.86 -50.308 -6.74 -97.898 11.71 -147.101 18.545 17.831 -10.443 35.897 -20.152 54.561 -28.527 18.965 -8.51 38.52 -15.705 58.076 -23.58 -24.845 -21.229 -49.776 -25.887 -81.008 -18.751 -17.072 3.901 -35.132 5.972 -52.564 5.121 -18.006 -0.88 -36.141 -5.311 -52.491 -16.238 17.507 -8.875 33.643 -16.24 48.864 -25.165 6.277 -3.681 13.473 -9.93 15.223 -16.377 4.191 -15.44 5.402 -31.689 7.817 -47.623 -28.667 -3.247 -79.074 -32.4 -89.261 -51.373 44.059 -8.478 92.031 1.777 134.06 -26.617 -13.844 -10.477 -46.085 -23.507 -57.911 -32.457 14.621 -3.831 48.498 -1.955 61.751 -1.057 11.157 0.756 16.306 1.029 20.881 -2.735l129.701 -101.541c13.638 -10.994 68.719 -63.131 83.098 -95.903 12.241 -27.902 13.739 -51.638 13.736 -57.431 -0.01 -15.541 -1.917 -39.876 -12.605 -67.022 -4.488 -11.399 -17.658 -36.65 -44.826 -66.083 -42.107 -45.617 -96.27 -70.274 -155.501 -82.487 -137.722 -28.395 -252.153 -175.469 -219.85 -337.61 37.714 -189.296 246.646 -291.784 417.386 -201.739 110.359 58.201 168.871 171.751 153.193 295.356 -9.471 74.672 -43.252 135.578 -99.881 184.538" />
</group>
</group>
</group>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?android:attr/colorForeground"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>
+24
View File
@@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="400dp"
android:height="400dp"
android:viewportHeight="400.0"
android:viewportWidth="400.0">
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m197.7,0c-6.2,0.1 -12.5,0.5 -19,1.1 12.4,2.9 23.5,5.5 34.7,8.1 -0.1,0.7 -0.3,1.4 -0.4,2 -14.9,2 -29.1,-3.5 -43.7,-5.5 5.3,3.1 10.7,6 16.2,8.5 5.6,2.5 11.4,4.7 17.2,7 -7.4,6.3 -14.8,7.7 -24,5.6 -5.1,-1.2 -10.4,-1.8 -15.6,-1.5 -5.3,0.3 -10.7,1.6 -15.6,4.8 5.2,2.6 10,4.8 14.5,7.5 1.9,1.1 4,2.9 4.5,4.9 1.2,4.6 1.6,9.4 2.3,14.1 -8.5,1 -23.5,9.6 -26.5,15.3 13.1,2.5 27.3,-0.5 39.8,7.9 -4.1,3.1 -13.7,7 -17.2,9.6 4.3,1.1 14.4,0.6 18.3,0.3 3.3,-0.2 4.8,-0.3 6.2,0.8l38.5,30.1c4,3.3 20.4,18.7 24.7,28.5 3.6,8.3 4.1,15.3 4.1,17.1 -0,4.6 -0.6,11.8 -3.7,19.9 -1.3,3.4 -5.2,10.9 -13.3,19.6 -12.5,13.5 -28.6,20.9 -46.2,24.5 -40.9,8.4 -74.9,52.1 -65.3,100.2 11.2,56.2 73.2,86.6 123.9,59.9 32.8,-17.3 50.1,-51 45.5,-87.7 -2.8,-22.2 -12.8,-40.2 -29.7,-54.8 -2.2,-1.9 -3.6,-1.9 -6.2,-0.3 -8.7,5.6 -17.7,10.9 -26.8,15.9 -5.2,2.9 -10.9,5 -17.4,7.9 2.2,0.6 3.3,0.9 4.4,1.1 24.4,6.5 37.5,28 31.7,52 -5.1,21.4 -26.8,35 -47.9,31.4 -17.5,-3 -32.8,-17.6 -35.4,-35 -2.8,-19 6.7,-37.3 23.5,-44.9 9.3,-4.2 18.9,-7.9 28.3,-12.2 10.6,-4.9 22.1,-8.7 31.3,-15.5 23,-16.9 37.1,-40.1 42.7,-68.1 3.3,-16.8 3.1,-33.5 -4.6,-49.4 -5.9,-12.2 -15.6,-21.1 -26,-29.2 -10.7,-8.3 -22,-15.9 -32.7,-24.3 -2.9,-2.3 -4.8,-6.2 -6.2,-9.7 -0.6,-1.5 1.3,-5.6 2.5,-5.8 6.5,-1.2 13.2,-1.8 19.8,-2 7.7,-0.3 15.4,-0 23,0.1 1.7,0 3.9,-0.2 4.9,0.7 4,3.9 7.1,1.4 9.9,-1.2 2.3,-2.2 4,-5 5.8,-7.5 -1.1,-0.2 -3.4,-0.7 -5.7,-0.8 -7.7,-0.2 -15.4,-0.1 -23.2,-0.3 -1.4,-0.1 -2.7,-1.5 -4,-2.2 1.4,-0.6 2.8,-1.6 4.3,-1.6 13.3,-0.1 26.6,-0.1 40,-0.1 0,-6.9 -9.2,-16.4 -17.5,-19 -0.1,0.9 -0.1,1.8 -0.2,2.7 -8.2,0.2 -16.2,0 -23.5,-3.8 -1.9,-1 -3.2,-3.3 -4.7,-5 -2,-2.1 -3.6,-4.9 -6,-6.3 -4.9,-2.9 -10.3,-4.9 -15.4,-7.4C224.3,1.7 211.3,-0.1 197.7,0ZM249.6,29.4c0.6,-0 1.2,0.2 1.9,0.8 1,0.8 2,1.7 3.3,2.8 -1.5,0.8 -2.8,1.5 -4.1,2.2 -1.8,0.9 -3.1,0.3 -4.2,-1.1 -0.9,-1.2 -1,-2.3 0.3,-3.3 0.9,-0.7 1.8,-1.3 2.9,-1.3z"
android:strokeColor="#00000000"
android:strokeWidth="1.33333325" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m97.9,307.6c-7.8,2 -15.4,4.9 -23.4,7.5 3.9,-26.3 34.7,-50.6 60.8,-47.8 -8.1,10.9 -11.8,23.3 -12.7,35.6 -8.7,1.6 -16.8,2.7 -24.8,4.7"
android:strokeColor="#00000000"
android:strokeWidth="1.33333325" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m134.3,124c41.3,-25.3 94,-9.8 113.8,28.2 3.7,7.2 4.2,18.3 1.8,25.8 -8.2,26.1 -27.5,40.7 -54.1,46.9 7.8,-6.7 14.1,-14.3 16,-24.8 2,-10.6 -0.1,-20.1 -6.2,-28.8 -9.3,-13.2 -27.3,-18.6 -42.4,-12.9 -16.3,6.2 -25.3,21.1 -23.7,39.4 1.5,17 14.4,28.1 38.6,32.2 -3.6,1.9 -6.4,3.3 -9.1,4.8 -11.1,6.2 -20.7,14.3 -28.2,24.6 -2.5,3.3 -4.1,3.6 -7.9,1.3 -48.6,-29.7 -51.7,-104.3 1.4,-136.8"
android:strokeColor="#00000000"
android:strokeWidth="1.33333325" />
</vector>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item>
<selector>
<item app:state_multiselected="true" android:state_activated="true">
<color android:color="?attr/colorMultiselectActiveBackground" />
</item>
</selector>
</item>
<item android:drawable="?attr/selectableItemBackground" />
</layer-list>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<LinearLayout
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:divider="?attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/list_fragment"
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:tag="LIST" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/bottom_sheet_top_padding">
<com.google.android.material.button.MaterialButton
android:id="@+id/create_empty"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:text="@string/create_empty"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
app:icon="@drawable/ic_action_edit"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/create_from_file"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:rippleColor="?attr/colorSecondary"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/create_from_file"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:text="@string/create_from_file"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
app:icon="@drawable/ic_action_open_white"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@+id/create_empty"
app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:rippleColor="?attr/colorSecondary"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/create_from_qrcode"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:text="@string/create_from_qr_code"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
app:icon="@drawable/ic_action_scan_qr_code_white"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@+id/create_from_file"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:rippleColor="?attr/colorSecondary"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.model.ApplicationData" />
<variable
name="fragment"
type="com.wireguard.android.fragment.AppListDialogFragment" />
<variable
name="appData"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, ApplicationData&gt;" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="200dp">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"
tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{appData}"
app:layout="@{@layout/app_list_item}"
tools:itemCount="10"
tools:listitem="@layout/app_list_item" />
</FrameLayout>
</layout>
+61
View File
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.model.ApplicationData" />
<variable
name="collection"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, com.wireguard.android.model.ApplicationData&gt;" />
<variable
name="key"
type="String" />
<variable
name="item"
type="com.wireguard.android.model.ApplicationData" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_background"
android:gravity="center_vertical"
android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/app_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:src="@{item.icon}"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/app_name"
style="?android:attr/textAppearanceMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@{key}"
tools:text="@tools:sample/full_names" />
<CheckBox
android:id="@+id/excluded_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={item.excludedFromTunnel}"
tools:checked="true" />
</LinearLayout>
</layout>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.widget.NameInputFilter" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tunnel_name_text_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/tunnel_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/tunnel_name"
android:inputType="textNoSuggestions|textVisiblePassword"
app:filter="@{NameInputFilter.newInstance()}" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
</layout>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity"
android:fitsSystemWindows="true">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/detail_container"
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="LIST"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.backend.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelDetailFragment" />
<variable
name="tunnel"
type="com.wireguard.android.model.ObservableTunnel" />
<variable
name="config"
type="com.wireguard.config.Config" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/interface_title"
android:layout_alignParentEnd="true"
app:checked="@{tunnel.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<TextView
android:id="@+id/interface_name_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:text="@{tunnel.name}" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/interface_name_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.keyPair.publicKey.toBase64}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<TextView
android:id="@+id/addresses_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/addresses_label"
android:contentDescription="@string/addresses"
android:text="@{config.interface.addresses}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/peers_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
tools:ignore="UselessLeaf" />
</LinearLayout>
</ScrollView>
</layout>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="item"
type="com.wireguard.config.Peer" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/peer" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.publicKey.toBase64}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<TextView
android:id="@+id/allowed_ips_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:text="@{item.allowedIps}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<TextView
android:id="@+id/endpoint_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_label"
android:text="@{item.endpoint}" />
<TextView
android:id="@+id/transfer_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/transfer_text"
android:text="@string/transfer"
android:visibility="gone" />
<TextView
android:id="@+id/transfer_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/transfer_label"
android:visibility="gone" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</layout>
@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<import type="com.wireguard.android.widget.NameInputFilter" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelEditorFragment" />
<variable
name="config"
type="com.wireguard.android.viewmodel.ConfigProxy" />
<variable
name="name"
type="String" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorBackground">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<EditText
android:id="@+id/interface_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={name}"
app:filter="@{NameInputFilter.newInstance()}" />
<TextView
android:id="@+id/private_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_text"
android:labelFor="@+id/private_key_text"
android:text="@string/private_key" />
<EditText
android:id="@+id/private_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/private_key_label"
android:layout_toStartOf="@+id/generate_private_key_button"
android:contentDescription="@string/public_key_description"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.privateKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<Button
android:id="@+id/generate_private_key_button"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/private_key_text"
android:layout_alignParentEnd="true"
android:layout_below="@+id/private_key_label"
android:onClick="@{() -> config.interface.generateKeyPair()}"
android:text="@string/generate" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/private_key_text"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?attr/editTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_generated"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.publicKey}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/public_key_text"
android:layout_toStartOf="@+id/listen_port_label"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<EditText
android:id="@+id/addresses_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_label"
android:layout_toStartOf="@+id/listen_port_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.addresses}" />
<TextView
android:id="@+id/listen_port_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port" />
<EditText
android:id="@+id/listen_port_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_random"
android:inputType="number"
android:text="@={config.interface.listenPort}"
android:textAlignment="center" />
<TextView
android:id="@+id/dns_servers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_text"
android:layout_toStartOf="@+id/mtu_label"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers" />
<EditText
android:id="@+id/dns_servers_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/dns_servers_label"
android:layout_toStartOf="@+id/mtu_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.dnsServers}" />
<TextView
android:id="@+id/mtu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu" />
<EditText
android:id="@+id/mtu_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_automatic"
android:inputType="number"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
<Button
android:id="@+id/set_excluded_applications"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/dns_servers_text"
android:layout_marginLeft="-8dp"
android:onClick="@{fragment::onRequestSetExcludedApplications}"
android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_editor_peer}"
tools:ignore="UselessLeaf" />
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:onClick="@{() -> config.addPeer()}"
android:text="@string/add_peer" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<variable
name="collection"
type="androidx.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
<variable
name="item"
type="com.wireguard.android.viewmodel.PeerProxy" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_toStartOf="@+id/peer_action_delete"
android:text="@string/peer" />
<ImageButton
android:id="@+id/peer_action_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="@null"
android:contentDescription="@string/delete"
android:onClick="@{() -> item.unbind()}"
android:src="@drawable/ic_action_delete" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<EditText
android:id="@+id/public_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.publicKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<TextView
android:id="@+id/pre_shared_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key" />
<EditText
android:id="@+id/pre_shared_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_label"
android:hint="@string/hint_optional"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.preSharedKey}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_text"
android:layout_toStartOf="@+id/exclude_private_ips"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<CheckBox
android:id="@+id/exclude_private_ips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/allowed_ips_label"
android:layout_alignParentEnd="true"
android:checked="@={item.excludingPrivateIps}"
android:text="@string/exclude_private_ips"
android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}" />
<EditText
android:id="@+id/allowed_ips_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.allowedIps}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/allowed_ips_text"
android:layout_toStartOf="@+id/persistent_keepalive_label"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<EditText
android:id="@+id/endpoint_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/endpoint_label"
android:layout_toStartOf="@+id/persistent_keepalive_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.endpoint}" />
<TextView
android:id="@+id/persistent_keepalive_label"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_label"
android:layout_alignParentEnd="true"
android:labelFor="@+id/persistent_keepalive_text"
android:text="@string/persistent_keepalive" />
<EditText
android:id="@+id/persistent_keepalive_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/persistent_keepalive_label"
android:hint="@string/hint_optional"
android:inputType="number"
android:text="@={item.persistentKeepalive}"
android:textAlignment="center" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</layout>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.model.ObservableTunnel" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelListFragment" />
<variable
name="rowConfigurationHandler"
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
<variable
name="tunnels"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, ObservableTunnel&gt;" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorBackground"
android:clipChildren="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoiceModal"
android:clipToPadding="false"
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
app:configurationHandler="@{rowConfigurationHandler}"
app:items="@{tunnels}"
app:layout="@{@layout/tunnel_list_item}"
tools:listitem="@layout/tunnel_list_item"
tools:itemCount="12" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
tools:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/logo_placeholder"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginBottom="20dp"
android:alpha="0.3333333"
android:src="@mipmap/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/tunnel_list_placeholder"
android:textSize="20sp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:id="@+id/create_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:icon="@drawable/ic_action_add_white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.model.ObservableTunnel" />
<import type="com.wireguard.android.backend.Tunnel.State" />
<variable
name="collection"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, ObservableTunnel&gt;" />
<variable
name="key"
type="String" />
<variable
name="item"
type="com.wireguard.android.model.ObservableTunnel" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelListFragment" />
</data>
<com.wireguard.android.widget.MultiselectableRelativeLayout
android:id="@+id/tunnel_list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_background"
android:descendantFocusability="beforeDescendants"
android:focusable="true"
android:nextFocusRight="@+id/tunnel_switch"
android:padding="16dp">
<TextView
android:id="@+id/tunnel_name"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:maxLines="1"
android:text="@{key}"
tools:text="@sample/interface_names.json/names/names/name" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/tunnel_name"
android:layout_alignParentEnd="true"
android:nextFocusLeft="@+id/tunnel_list_item"
app:checked="@{item.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}"
tools:checked="@sample/interface_names.json/names/checked/checked" />
</com.wireguard.android.widget.MultiselectableRelativeLayout>
</layout>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_action_save"
android:alphabeticShortcut="s"
android:icon="@drawable/ic_action_save"
android:title="@string/save"
app:showAsAction="always" />
</menu>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_settings"
android:alphabeticShortcut="s"
android:icon="@drawable/ic_settings"
android:orderInCategory="1000"
android:title="@string/settings"
app:showAsAction="always" />
</menu>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_action_edit"
android:alphabeticShortcut="e"
android:icon="@drawable/ic_action_edit"
android:title="@string/edit"
app:showAsAction="always" />
</menu>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_action_select_all"
android:alphabeticShortcut="s"
android:icon="@drawable/ic_action_select_all"
android:title="@string/select_all"
app:showAsAction="always" />
<item
android:id="@+id/menu_action_delete"
android:alphabeticShortcut="d"
android:icon="@drawable/ic_action_delete"
android:title="@string/delete"
app:showAsAction="always" />
</menu>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+179
View File
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">%d टनल हटाने में असमर्थ: %s</item>
<item quantity="other">%d टनलस को हटाने में असमर्थ: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">%d टनल को सफलतापूर्वक हटा दिया गया</item>
<item quantity="other">%d टनलस को सफलतापूर्वक हटा दिया गया</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d टनल चयनित</item>
<item quantity="other">%d टनलस का चयन किया गया</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">आयातित %d %d टनल</item>
<item quantity="other">आयातित %d %d टनलस</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">आयातित %d टनल</item>
<item quantity="other">आयातित %d टनलस</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d बहिष्कृत अनुप्रयोग</item>
<item quantity="other">%d बहिष्कृत अनुप्रयोग</item>
</plurals>
<string name="add_peer">पीयर जोड़ें</string>
<string name="addresses">एड्रेससैस</string>
<string name="allowed_ips">अनुमत आईपी</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s in %2$s</string>
<string name="bad_config_explanation_pka">: सकारात्मक होना चाहिए और 65535 से अधिक नहीं होना चाहिए</string>
<string name="bad_config_explanation_positive_number">: सकारात्मक होना चाहिए</string>
<string name="bad_config_explanation_udp_port">: एक वैध यूडीपी पोर्ट नंबर होना चाहिए</string>
<string name="bad_config_reason_invalid_key">अमान्य चाबी</string>
<string name="bad_config_reason_invalid_number">अमान्य संख्या</string>
<string name="bad_config_reason_invalid_value">अमान्य मूल्य</string>
<string name="bad_config_reason_missing_attribute">गुम विशेषता</string>
<string name="bad_config_reason_missing_section">छूटा हुआ भाग</string>
<string name="bad_config_reason_missing_value">अनुपस्थित मान</string>
<string name="bad_config_reason_syntax_error">वक्य रचना त्रुटि</string>
<string name="bad_config_reason_unknown_attribute">अज्ञात एट्रिब्यूट</string>
<string name="bad_config_reason_unknown_section">अज्ञात एट्रिब्यूट </string>
<string name="bad_config_reason_value_out_of_range">मूल्य सीमा से बाहर</string>
<string name="bad_extension_error">फ़ाइल .conf या .zip होनी चाहिए</string>
<string name="cancel">रद्द</string>
<string name="config_delete_error">कॉन्फ़िगरेशन फ़ाइल %s को नहीं हटा सकता</string>
<string name="config_exists_error">“%s” के लिए कॉन्फ़िगरेशन पहले से मौजूद है</string>
<string name="config_file_exists_error">कॉन्फ़िगरेशन फ़ाइल “%s” पहले से मौजूद है</string>
<string name="config_not_found_error">कॉन्फ़िगरेशन फ़ाइल “%s” नहीं मिली</string>
<string name="config_rename_error">कॉन्फ़िगरेशन फ़ाइल “%s” का नाम नहीं बदल सकता</string>
<string name="config_save_error">“%1$s” के लिए कॉन्फ़िगरेशन को नहीं बचा सकता: %2$s</string>
<string name="config_save_success">“%s” के लिए सफलतापूर्वक सहेजा गया कॉन्फ़िगरेशन</string>
<string name="create_activity_title">वायरगार्ड टनल बनाएं</string>
<string name="create_bin_dir_error">स्थानीय बाइनरी निर्देशिका नहीं बना सकते</string>
<string name="create_empty">स्क्रैच से बनाएँ</string>
<string name="create_from_file">फ़ाइल या संग्रह से बनाएँ</string>
<string name="create_from_qr_code">क्यूआर कोड से बनाएं</string>
<string name="create_output_dir_error">आउटपुट निर्देशिका नहीं बना सकता</string>
<string name="create_downloads_file_error">डाउनलोड निर्देशिका में फ़ाइल नहीं बना सकते</string>
<string name="create_temp_dir_error">स्थानीय अस्थायी निर्देशिका नहीं बना सकते</string>
<string name="create_tunnel">टनल बनाए</string>
<string name="dark_theme_summary_off">अभी प्रकाश (दिन) थीम का उपयोग कर रहे हैं</string>
<string name="dark_theme_summary_on">अभी डार्क (रात) थीम का उपयोग कर रहे हैं</string>
<string name="dark_theme_title">डार्क थीम का इस्तेमाल करें</string>
<string name="delete">हटाएं</string>
<string name="toggle_all">सबको स्विच करो</string>
<string name="dns_servers">DNS सर्वर</string>
<string name="edit">संपादित करें</string>
<string name="endpoint">अंतिम</string>
<string name="error_down">टनल को लाने में त्रुटि: %s</string>
<string name="error_fetching_apps">ऐप्स सूची लाने में त्रुटि: %s</string>
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
<string name="error_up">टनल को लाने में त्रुटि: %s</string>
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
<string name="excluded_applications">निकाले गए ऐप्स</string>
<string name="generate">उत्पन्न</string>
<string name="generic_error">अज्ञात “%s” त्रुटि</string>
<string name="hint_automatic">(ऑटो)</string>
<string name="hint_generated">(उत्पन्न)</string>
<string name="hint_optional">(ऐच्छिक)</string>
<string name="hint_random">(क्रमरहित)</string>
<string name="illegal_filename_error">अवैध फ़ाइल नाम “%s”</string>
<string name="import_error">टनल को आयात करने में असमर्थ: %s</string>
<string name="import_from_qr_code">क्यूआर कोड से टनल को आयात करें</string>
<string name="import_success">आयातित “%s”</string>
<string name="interface_title">इंटरफेस</string>
<string name="key_length_explanation_base64">: वायरगार्ड बेस 64 कीज़ में 44 अक्षर (32 बाइट्स) होने चाहिए</string>
<string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
<string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
<string name="listen_port">पोर्ट सुनो</string>
<string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string>
<string name="log_export_success">“%s” में सहेजा गया</string>
<string name="log_export_summary">लॉग फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string>
<string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
<string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
<string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string>
<string name="module_installer_not_found">आपके डिवाइस के लिए कोई मॉड्यूल उपलब्ध नहीं हैं</string>
<string name="module_installer_initial">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
<string name="module_installer_success">सफलता। ऐप 5 सेकंड में रीस्टार्ट होगा</string>
<string name="module_installer_title">कर्नेल मॉड्यूल डाउनलोड और इंस्टॉल करें</string>
<string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string>
<string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string>
<string name="mtu">MTU</string>
<string name="name">नाम</string>
<string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string>
<string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string>
<string name="no_tunnels_error">कोई टनल मौजूद नहीं है</string>
<string name="parse_error_generic">पाठ</string>
<string name="parse_error_inet_address">आईपी पता</string>
<string name="parse_error_inet_endpoint">समाप्त</string>
<string name="parse_error_inet_network">आईपी नेटवर्क</string>
<string name="parse_error_integer">संख्या</string>
<string name="parse_error_reason">%1$s “%2$s” को पार्स नहीं कर सकता</string>
<string name="peer">पीयर</string>
<string name="permission_description">किसी एप्लिकेशन को वायरगार्ड सुरंगों को नियंत्रित करने की अनुमति देता है। इस अनुमति वाले ऐप्स, इंटरनेट ट्रैफ़िक को संभावित रूप से गलत तरीके से वायरगार्ड सुरंगों को सक्षम और अक्षम कर सकते हैं।</string>
<string name="permission_label">वायरगार्ड सुरंगों को नियंत्रित करें</string>
<string name="persistent_keepalive">लगातार जिंदा रहो</string>
<string name="pre_shared_key">प्री-शेयर्ड कीस</string>
<string name="private_key">निजी कीस</string>
<string name="public_key">सार्वजनिक कीस</string>
<string name="public_key_description">सार्वजनिक कीस</string>
<string name="qr_code_hint">टिप: `qrencode -t ansiutf8 &lt; tunnel.conf` के साथ उत्पन्न करो</string>
<string name="restore_on_boot_summary">बूट पर पहले से सक्षम टनल को ऊपर लाएं</string>
<string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
<string name="save">सहेजें</string>
<string name="select_all">सभी का चयन करे</string>
<string name="set_exclusions">बहिष्करण सेट करें</string>
<string name="settings">सेटिंग्स</string>
<string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
<string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
<string name="shell_start_error">शेल शुरू करने में विफल: %d</string>
<string name="toggle_error">वायरगार्ड टनल टॉगल करने में त्रुटि: %s</string>
<string name="tools_installer_already">wg और wg-quick पहले से इंस्टॉल हैं</string>
<string name="tools_installer_failure">कमांड-लाइन टूल स्थापित करने में असमर्थ (कोई रूट नहीं)</string>
<string name="tools_installer_initial">स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
<string name="tools_installer_initial_magisk">Magisk मॉड्यूल के रूप में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
<string name="tools_installer_initial_system">सिस्टम विभाजन में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
<string name="tools_installer_success_magisk">wg और wg-quick को मैजिक मॉड्यूल के रूप में स्थापित किया गया है (रिबूट आवश्यक)</string>
<string name="tools_installer_success_system">wg और wg-quick सिस्टम विभाजन में स्थापित है</string>
<string name="tools_installer_title">कमांड लाइन उपकरण स्थापित करें</string>
<string name="tools_installer_working">Wg और wg-quick इंस्टॉल करना</string>
<string name="tools_unavailable_error">आवश्यक उपकरण अनुपलब्ध हैं</string>
<string name="transfer">स्थानांतरण</string>
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_kibibytes">%.2f KiB</string>
<string name="transfer_mibibytes">%.2f MiB</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tun_create_error">ट्यून डिवाइस बनाने में असमर्थ</string>
<string name="tunnel_config_error">टनल को कॉन्फ़िगर करने में असमर्थ (wg-quick लौटा %d)</string>
<string name="tunnel_create_error">टनल बनाने में असमर्थ: %s</string>
<string name="tunnel_create_success">सफलतापूर्वक बनाया गया टनल “%s”</string>
<string name="tunnel_error_already_exists">टनल “%s” पहले से मौजूद है</string>
<string name="tunnel_error_invalid_name">गलत नाम</string>
<string name="tunnel_list_placeholder">नीले बटन का उपयोग करके एक टनल को जोड़ें</string>
<string name="tunnel_name">टनल का नाम</string>
<string name="tunnel_on_error">टनल चालू करने में असमर्थ (wgTurnOn लौटा %d)</string>
<string name="tunnel_rename_error">टनल का नाम बदलने में असमर्थ: %s</string>
<string name="tunnel_rename_success">टनल का नाम बदलकर “%s” करने के लिए</string>
<string name="type_name_go_userspace">userspace पे जाए </string>
<string name="type_name_kernel_module">कर्नेल मॉड्यूल</string>
<string name="unknown_error">अज्ञात त्रुटि</string>
<string name="version_summary">%1$s बैकएंड v%2$s</string>
<string name="version_summary_checking">%s बैकएंड संस्करण की जाँच कर रहा है</string>
<string name="version_summary_unknown">अज्ञात %s संस्करण</string>
<string name="version_title">WireGuard for Android v%s</string>
<string name="vpn_not_authorized_error">वीपीएन सेवा उपयोगकर्ता द्वारा अधिकृत नहीं है</string>
<string name="vpn_start_error">Android VPN सेवा प्रारंभ करने में असमर्थ</string>
<string name="zip_export_error">टनल का निर्यात करने में असमर्थ: %s</string>
<string name="zip_export_success">“%s” पर सहेजा गया</string>
<string name="zip_export_summary">ज़िप फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string>
<string name="zip_export_title">जिप फाइल के लिए टनल को एक्सपोर्ट करें</string>
<string name="key_length_error">चाबी की लम्बाई गलत </string>
<string name="key_contents_error">चाबी में खराब वर्ण</string>
</resources>
+179
View File
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Impossibile eliminare %d tunnel: %s</item>
<item quantity="other">Impossibile eliminare %d tunnel: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">%d tunnel eliminato correttamente</item>
<item quantity="other">%d tunnel eliminati correttamente</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d tunnel selezionato</item>
<item quantity="other">%d tunnel selezionati</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">Importato %d di %d tunnel</item>
<item quantity="other">Importati %d di %d tunnel</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">Importato %d tunnel</item>
<item quantity="other">Importati %d tunnel</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d applicazione esclusa</item>
<item quantity="other">%d applicazioni escluse</item>
</plurals>
<string name="add_peer">Aggiungi peer</string>
<string name="addresses">Indirizzi</string>
<string name="allowed_ips">IP consentiti</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%2$s di %1$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s in %2$s</string>
<string name="bad_config_explanation_pka">: deve essere positivo e non maggiore di 65535</string>
<string name="bad_config_explanation_positive_number">: deve essere positivo</string>
<string name="bad_config_explanation_udp_port">: deve essere un numero di porta UDP valido</string>
<string name="bad_config_reason_invalid_key">Chiave non valida</string>
<string name="bad_config_reason_invalid_number">Numero non valido</string>
<string name="bad_config_reason_invalid_value">Valore non valido</string>
<string name="bad_config_reason_missing_attribute">Attributo mancante</string>
<string name="bad_config_reason_missing_section">Sezione mancante</string>
<string name="bad_config_reason_missing_value">Valore mancante</string>
<string name="bad_config_reason_syntax_error">Errore di sintassi</string>
<string name="bad_config_reason_unknown_attribute">Attributo sconosciuto</string>
<string name="bad_config_reason_unknown_section">Sezione sconosciuta</string>
<string name="bad_config_reason_value_out_of_range">Valore fuori scala</string>
<string name="bad_extension_error">Il file deve essere .conf o .zip</string>
<string name="cancel">Annulla</string>
<string name="config_delete_error">Impossibile eliminare il file di configurazione %s</string>
<string name="config_exists_error">La configurazione per “%s” esiste già</string>
<string name="config_file_exists_error">Il file di configurazione “%s” esiste già</string>
<string name="config_not_found_error">File di configurazione “%s” non trovato</string>
<string name="config_rename_error">Impossibile rinominare il file di configurazione “%s”</string>
<string name="config_save_error">Impossibile salvare la configurazione per “%1$s”: %2$s</string>
<string name="config_save_success">Configurazione per “%s” salvata correttamente</string>
<string name="create_activity_title">Crea un tunnel WireGuard</string>
<string name="create_bin_dir_error">Impossibile creare cartella locale binari</string>
<string name="create_empty">Crea da zero</string>
<string name="create_from_file">Crea da un file o archivio</string>
<string name="create_from_qr_code">Crea da un codice QR</string>
<string name="create_output_dir_error">Impossibile creare la cartella di output</string>
<string name="create_downloads_file_error">Impossibile creare il file nella cartella di download</string>
<string name="create_temp_dir_error">Impossibile creare la cartella locale temporanea</string>
<string name="create_tunnel">Crea tunnel</string>
<string name="dark_theme_summary_off">Stai usando il tema chiaro (giorno)</string>
<string name="dark_theme_summary_on">Stai usando il tema scuro (notte)</string>
<string name="dark_theme_title">Usa tema scuro</string>
<string name="delete">Elimina</string>
<string name="toggle_all">Inverti tutto</string>
<string name="dns_servers">Server DNS</string>
<string name="edit">Modifica</string>
<string name="endpoint">Endpoint</string>
<string name="error_down">Errore di disattivazione del tunnel: %s</string>
<string name="error_fetching_apps">Errore di recupero della lista app: %s</string>
<string name="error_root">Si prega di ottenere l\'accesso root e riprovare</string>
<string name="error_up">Errore di attivazione del tunnel: %s</string>
<string name="exclude_private_ips">Escludi IP privati</string>
<string name="excluded_applications">Applicazioni escluse</string>
<string name="generate">Genera</string>
<string name="generic_error">Errore “%s” sconosciuto</string>
<string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generato)</string>
<string name="hint_optional">(facoltativo)</string>
<string name="hint_random">(casuale)</string>
<string name="illegal_filename_error">Nome file “%s” non valido</string>
<string name="import_error">Impossibile importare il tunnel: %s</string>
<string name="import_from_qr_code">Importa tunnel da codice QR</string>
<string name="import_success">Importato “%s”</string>
<string name="interface_title">Interfaccia</string>
<string name="key_length_explanation_base64">: le chiavi base64 di WireGuard devono essere di 44 caratteri (32 byte)</string>
<string name="key_length_explanation_binary">: le chiavi di WireGuard devono essere di 32 byte</string>
<string name="key_length_explanation_hex">: le chiavi hex di WireGuard devono essere di 64 caratteri (32 byte)</string>
<string name="listen_port">Porta in ascolto</string>
<string name="log_export_error">Impossibile esportare il registro: %s</string>
<string name="log_export_success">Salvato in “%s”</string>
<string name="log_export_summary">Il file del registro verrà salvato nella cartella di download</string>
<string name="log_export_title">Esporta file registro</string>
<string name="logcat_error">Impossibile eseguire logcat: </string>
<string name="module_version_error">Impossibile determinare la versione modulo del kernel</string>
<string name="module_installer_not_found">Nessun modulo disponibile per il tuo dispositivo</string>
<string name="module_installer_initial">Il modulo del kernel sperimentale può migliorare le prestazioni</string>
<string name="module_installer_success">Fatto. L\'applicazione si riavvierà in 5 secondi</string>
<string name="module_installer_title">Scarica e installa il modulo del kernel</string>
<string name="module_installer_working">Scaricamento e installazione…</string>
<string name="module_installer_error">Qualcosa è andato storto. Riprova</string>
<string name="mtu">MTU</string>
<string name="name">Nome</string>
<string name="no_config_error">Tentativo di attivare un tunnel senza configurazione</string>
<string name="no_configs_error">Nessuna configurazione trovata</string>
<string name="no_tunnels_error">Non esistono tunnel</string>
<string name="parse_error_generic">stringa</string>
<string name="parse_error_inet_address">indirizzo IP</string>
<string name="parse_error_inet_endpoint">endpoint</string>
<string name="parse_error_inet_network">rete IP</string>
<string name="parse_error_integer">numero</string>
<string name="parse_error_reason">Impossibile analizzare %1$s “%2$s”</string>
<string name="peer">Peer</string>
<string name="permission_description">Permette ad un\'app di controllare i tunnel WireGuard. Le app con questa autorizzazione possono attivare e disattivare i tunnel WireGuard a piacimento, potenzialmente deviando il traffico internet.</string>
<string name="permission_label">controlla tunnel WireGuard</string>
<string name="persistent_keepalive">Tieni sempre attivo</string>
<string name="pre_shared_key">Chiave condivisa (PSK)</string>
<string name="private_key">Chiave privata</string>
<string name="public_key">Chiave pubblica</string>
<string name="public_key_description">La chiave pubblica</string>
<string name="qr_code_hint">Suggerimento: genera con `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_summary">Attiva tunnel attivati in precedenza all\'avvio</string>
<string name="restore_on_boot_title">Ripristina all\'avvio</string>
<string name="save">Salva</string>
<string name="select_all">Seleziona tutto</string>
<string name="set_exclusions">Imposta esclusioni</string>
<string name="settings">Impostazioni</string>
<string name="shell_exit_status_read_error">La shell non riesce a leggere l\'exit status</string>
<string name="shell_marker_count_error">La shell si aspettava 4 marker, ne ha ricevuti %d</string>
<string name="shell_start_error">Avvio della shell fallito: %d</string>
<string name="toggle_error">Errore di commutazione tunnel WireGuard: %s</string>
<string name="tools_installer_already">wg e wg-quick sono già installati</string>
<string name="tools_installer_failure">Impossibile installare strumenti di riga di comando (non root?)</string>
<string name="tools_installer_initial">Installa strumenti facoltativi per script</string>
<string name="tools_installer_initial_magisk">Installa strumenti facoltativi per script come moduli Magisk</string>
<string name="tools_installer_initial_system">Installa strumenti facoltativi per script nella partizione di sistema</string>
<string name="tools_installer_success_magisk">wg e wg-quick installati come moduli Magisk (riavvio necessario)</string>
<string name="tools_installer_success_system">wg e wg-quick installati nella partizione di sistema</string>
<string name="tools_installer_title">Installa strumenti di riga di comando</string>
<string name="tools_installer_working">Installazione di wg e wg-quick</string>
<string name="tools_unavailable_error">Strumenti necessari non disponibili</string>
<string name="transfer">Trasferisci</string>
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_kibibytes">%.2f KiB</string>
<string name="transfer_mibibytes">%.2f MiB</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tun_create_error">Impossibile creare il dispositivo tun</string>
<string name="tunnel_config_error">Impossibile configurare il tunnel (wg-quick ha risposto %d)</string>
<string name="tunnel_create_error">Impossibile creare il tunnel: %s</string>
<string name="tunnel_create_success">Tunnel “%s” creato correttamente</string>
<string name="tunnel_error_already_exists">Il tunnel “%s” esiste già</string>
<string name="tunnel_error_invalid_name">Nome non valido</string>
<string name="tunnel_list_placeholder">Aggiungi un tunnel usando il pulsante blu</string>
<string name="tunnel_name">Nome tunnel</string>
<string name="tunnel_on_error">Impossibile attivare il tunnel (wgTurnOn ha risposto %d)</string>
<string name="tunnel_rename_error">Impossibile rinominare il tunnel: %s</string>
<string name="tunnel_rename_success">Tunnel rinominato correttamente in “%s”</string>
<string name="type_name_go_userspace">Spazio utente Go</string>
<string name="type_name_kernel_module">Modulo kernel</string>
<string name="unknown_error">Errore sconosciuto</string>
<string name="version_summary">%1$s backend v%2$s</string>
<string name="version_summary_checking">Controllo versione backend %s</string>
<string name="version_summary_unknown">Versione %s sconosciuta</string>
<string name="version_title">WireGuard per Android v%s</string>
<string name="vpn_not_authorized_error">Servizio VPN non autorizzato dall\'utente</string>
<string name="vpn_start_error">Impossibile avviare il servizio VPN di Android</string>
<string name="zip_export_error">Impossibile esportare i tunnel: %s</string>
<string name="zip_export_success">Salvato in “%s”</string>
<string name="zip_export_summary">Il file zip verrà salvato nella cartella di download</string>
<string name="zip_export_title">Esporta i tunnel in un file zip</string>
<string name="key_length_error">Lunghezza chiave non valida</string>
<string name="key_contents_error">Caratteri errati nella chiave</string>
</resources>
+173
View File
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="other">%d トンネルを削除できません: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="other">%d トンネルを削除しました</item>
</plurals>
<plurals name="delete_title">
<item quantity="other">%d トンネルを選択</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="other">%d 個(全 %d 個)のトンネル設定をインポート</item>
</plurals>
<plurals name="import_total_success">
<item quantity="other">%d 個のトンネル設定をインポート済</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="other">除外アプリ %d 個</item>
</plurals>
<string name="add_peer">ピアを追加する</string>
<string name="addresses">Addresses</string>
<string name="allowed_ips">Allowed IPs</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s in %2$s</string>
<string name="bad_config_explanation_pka">: 65535未満の正の整数を指定してください</string>
<string name="bad_config_explanation_positive_number">: 正の整数を指定</string>
<string name="bad_config_explanation_udp_port">: 有効な UDP ポート番号を指定してください</string>
<string name="bad_config_reason_invalid_key">無効な鍵</string>
<string name="bad_config_reason_invalid_number">無効な数字</string>
<string name="bad_config_reason_invalid_value">無効な値</string>
<string name="bad_config_reason_missing_attribute">属性が不足しています</string>
<string name="bad_config_reason_missing_section">セクションが不足しています</string>
<string name="bad_config_reason_missing_value">値が不足しています</string>
<string name="bad_config_reason_syntax_error">構文エラー</string>
<string name="bad_config_reason_unknown_attribute">未知の属性</string>
<string name="bad_config_reason_unknown_section">未知のセクション</string>
<string name="bad_config_reason_value_out_of_range">範囲外の値</string>
<string name="bad_extension_error">ファイルの拡張子は .conf か .zip です</string>
<string name="cancel">キャンセル</string>
<string name="config_delete_error">%s の定義を削除できません</string>
<string name="config_exists_error">"%s" の定義はすでに存在します</string>
<string name="config_file_exists_error">設定ファイル "%s" はすでに存在します</string>
<string name="config_not_found_error">設定ファイル "%s" が見つかりません</string>
<string name="config_rename_error">設定ファイル "%s" の名前を変更できません</string>
<string name="config_save_error">“%1$s” の設定を保存できません: %2$s</string>
<string name="config_save_success">"%s" の設定を保存しました</string>
<string name="create_activity_title">WireGuard トンネルの作成</string>
<string name="create_bin_dir_error">ローカルバイナリディレクトリを作成できません</string>
<string name="create_empty">空の状態から作成</string>
<string name="create_from_file">ファイル、アーカイブから作成</string>
<string name="create_from_qr_code">QRコードから作成</string>
<string name="create_output_dir_error">出力ディレクトリを作成できません</string>
<string name="create_downloads_file_error">ダウンロードディレクトリにファイルを作成できません</string>
<string name="create_temp_dir_error">ローカルに一時ディレクトリを作成できません</string>
<string name="create_tunnel">トンネルを作成</string>
<string name="dark_theme_summary_off">ライト(日中)テーマを使用中</string>
<string name="dark_theme_summary_on">ダーク(夜間)テーマを使用中</string>
<string name="dark_theme_title">ダークテーマを使用する</string>
<string name="delete">削除</string>
<string name="toggle_all">すべての状態を切り替え</string>
<string name="dns_servers">DNS サーバ</string>
<string name="edit">編集</string>
<string name="endpoint">エンドポイント</string>
<string name="error_down">トンネル停止時エラー: %s</string>
<string name="error_fetching_apps">アプリ一覧取得エラー: %s</string>
<string name="error_root">root 権限を取得して再試行してください</string>
<string name="error_up">トンネル起動時エラー: %s</string>
<string name="exclude_private_ips">プライベート IP アドレスの除外</string>
<string name="excluded_applications">除外されたアプリケーション</string>
<string name="generate">生成</string>
<string name="generic_error">未知のエラー “%s”</string>
<string name="hint_automatic">(自動)</string>
<string name="hint_generated">(生成済み)</string>
<string name="hint_optional">(任意)</string>
<string name="hint_random">(ランダム)</string>
<string name="illegal_filename_error">不正なファイル名 “%s”</string>
<string name="import_error">トンネル設定をインポートできません: %s</string>
<string name="import_from_qr_code">QR コードからトンネル設定をインポートできません</string>
<string name="import_success">インポートしました “%s”</string>
<string name="interface_title">インターフェース</string>
<string name="key_length_explanation_base64">: WireGuard base64 鍵は44文字(32バイト)でなければなりません</string>
<string name="key_length_explanation_binary">: WireGuard 鍵は32バイトでなければなりません</string>
<string name="key_length_explanation_hex">: WireGuard hex 鍵は64文字(32バイト)でなければなりません</string>
<string name="listen_port">Listen ポート</string>
<string name="log_export_error">ログをエクスポートできません: %s</string>
<string name="log_export_success">“%s” に保存しました</string>
<string name="log_export_summary">ログはダウンロードフォルダに保存されます</string>
<string name="log_export_title">ログのエクスポート</string>
<string name="logcat_error">logcat を実行できません: </string>
<string name="module_version_error">カーネルモジュールバージョンを特定できません</string>
<string name="module_installer_not_found">このデバイス用のモジュールは利用できません</string>
<string name="module_installer_initial">実験的カーネルモジュールはパフォーマンスが向上する場合があります</string>
<string name="module_installer_success">成功. アプリは5秒後以内に再起動します</string>
<string name="module_installer_title">カーネルモジュールをダウンロードしてインストールする</string>
<string name="module_installer_working">ダウンロードしてインストールしています…</string>
<string name="module_installer_error">失敗しました. 再度実行してみてください</string>
<string name="mtu">MTU</string>
<string name="name">名前</string>
<string name="no_config_error">未設定のままトンネルを起動しようとしています</string>
<string name="no_configs_error">設定が見つかりません</string>
<string name="no_tunnels_error">トンネルが存在しません</string>
<string name="parse_error_generic">文字</string>
<string name="parse_error_inet_address">IP アドレス</string>
<string name="parse_error_inet_endpoint">エンドポイント</string>
<string name="parse_error_inet_network">IP ネットワーク</string>
<string name="parse_error_integer">数値</string>
<string name="parse_error_reason">%1$s の内容を解読できません “%2$s”</string>
<string name="peer">ピア</string>
<string name="permission_description">アプリに WireGuard トンネルの制御を許可します。この権限を持つアプリはトンネルの起動停止ができるようになりますが、インターネットトラフィックが意図しない方向に向かう可能性があります。</string>
<string name="permission_label">control WireGuard tunnels</string>
<string name="persistent_keepalive">持続的キープアライブ</string>
<string name="pre_shared_key">事前共有鍵</string>
<string name="private_key">秘密鍵</string>
<string name="public_key">公開鍵</string>
<string name="public_key_description">公開鍵</string>
<string name="qr_code_hint">Tip: `qrencode -t ansiutf8 &lt; tunnel.conf` で生成できます</string>
<string name="restore_on_boot_summary">起動時に前回有効だったトンネルを起動する</string>
<string name="restore_on_boot_title">起動時に復元</string>
<string name="save">保存</string>
<string name="select_all">すべて選択</string>
<string name="set_exclusions">例外を設定</string>
<string name="settings">設定</string>
<string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string>
<string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d を受け取りました</string>
<string name="shell_start_error">シェル実行に失敗しました: %d</string>
<string name="toggle_error">WireGuard トンネルのトグル時にエラー: %s</string>
<string name="tools_installer_already">wg および wg-quick はインストール済みです</string>
<string name="tools_installer_failure">コマンドラインツールをインストールできません(rootではない?)</string>
<string name="tools_installer_initial">スクリプティングのためのオプションツールのインストール</string>
<string name="tools_installer_initial_magisk">スクリプティングのためのオプションツールを Magisk モジュールとしてインストール</string>
<string name="tools_installer_initial_system">スクリプティングのためのオプションツールをシステムパーティションにインストール</string>
<string name="tools_installer_success_magisk">wg および wg-quick を Magisk モジュールとしてインストール(再起動必須)</string>
<string name="tools_installer_success_system">wg および wg-quick をシステムパーティションにインストール</string>
<string name="tools_installer_title">コマンドラインツールのインストール</string>
<string name="tools_installer_working">wg および wg-quick のインストール</string>
<string name="tools_unavailable_error">リクエストされたツールは利用できません</string>
<string name="transfer">転送</string>
<string name="transfer_rx_tx">受信: %1$s, 送信: %2$s</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_kibibytes">%.2f KiB</string>
<string name="transfer_mibibytes">%.2f MiB</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tun_create_error">tun デバイスを作成できません</string>
<string name="tunnel_config_error">トンネルを設定できません (wg-quick が %d を返却)</string>
<string name="tunnel_create_error">トンネルを作成できません: %s</string>
<string name="tunnel_create_success">トンネル "%s" を作成しました</string>
<string name="tunnel_error_already_exists">トンネル “%s” は存在します</string>
<string name="tunnel_error_invalid_name">不正な名前</string>
<string name="tunnel_list_placeholder">青ボタンでトンネルを追加</string>
<string name="tunnel_name">トンネル名</string>
<string name="tunnel_on_error">トンネルを有効にできません (wgTurnOn が %d を返却)</string>
<string name="tunnel_rename_error">トンネル名を変更できません: %s</string>
<string name="tunnel_rename_success">トンネル名を “%s” に変更しました</string>
<string name="type_name_go_userspace">Go ユーザースペース</string>
<string name="type_name_kernel_module">カーネルモジュール</string>
<string name="unknown_error">未知のエラー</string>
<string name="version_summary">%1$s backend v%2$s</string>
<string name="version_summary_checking">%s バックエンドのバージョンを確認中</string>
<string name="version_summary_unknown">未知の %s バージョン</string>
<string name="version_title">WireGuard for Android v%s</string>
<string name="vpn_not_authorized_error">VPN サービスはユーザに認証されていません</string>
<string name="vpn_start_error">Android VPN サービスを開始できません</string>
<string name="zip_export_error">トンネル設定をエクスポートできません: %s</string>
<string name="zip_export_success">“%s” に保存</string>
<string name="zip_export_summary">Zip ファイルはダウンロードフォルダに保存されます</string>
<string name="zip_export_title">トンネル設定を zip ファイルにエクスポート</string>
<string name="key_length_error">鍵の長さが不正</string>
<string name="key_contents_error">鍵に不正な文字があります</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="light_status_bar">false</bool>
<bool name="light_navigation_bar">false</bool>
</resources>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base palette -->
<color name="primary_color">#ff212121</color>
<color name="primary_light_color">#ff484848</color>
<color name="primary_dark_color">#ff000000</color>
<color name="secondary_color">#ff4285f4</color>
<color name="secondary_light_color">#ff80b4ff</color>
<color name="secondary_dark_color">#ff0059c1</color>
<color name="primary_text_color">#ffffffff</color>
<color name="secondary_text_color">#ffffffff</color>
<!-- Theme variables -->
<color name="list_multiselect_background">#1aeeeeee</color>
<color name="status_bar_color">#21242424</color>
<color name="navigation_bar_color">#aa242424</color>
</resources>
+179
View File
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Не удалось удалить %d туннель: %s</item>
<item quantity="other">Не удалось удалить %d туннелей: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">Успешно удален %d туннель</item>
<item quantity="other">Успешно удалено %d туннелей</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d туннель выбран</item>
<item quantity="other">%d туннелей выбрано</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">Импортирован %d из %d туннелей</item>
<item quantity="other">Импортировано %d из %d туннелей</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">Импортированный %d туннель</item>
<item quantity="other">Импортировано %d туннелей</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d Исключенное приложение</item>
<item quantity="other">%d Исключенных приложений</item>
</plurals>
<string name="add_peer">Добавить пира</string>
<string name="addresses">IP-адреса</string>
<string name="allowed_ips">Разрешенные IP-адреса</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%1$s из %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s в %2$s</string>
<string name="bad_config_explanation_pka">: Должен быть положительный и не больше 65535</string>
<string name="bad_config_explanation_positive_number">: Должен быть положительный</string>
<string name="bad_config_explanation_udp_port">: Должен быть действительный номер порта UDP</string>
<string name="bad_config_reason_invalid_key">Неправильный ключ</string>
<string name="bad_config_reason_invalid_number">Неправильный номер</string>
<string name="bad_config_reason_invalid_value">Неправильное значение</string>
<string name="bad_config_reason_missing_attribute">Несуществующий атрибут</string>
<string name="bad_config_reason_missing_section">Несуществующий раздел</string>
<string name="bad_config_reason_missing_value">Несуществющее значение</string>
<string name="bad_config_reason_syntax_error">Синтаксическая ошибка</string>
<string name="bad_config_reason_unknown_attribute">Неизвестный атрибут</string>
<string name="bad_config_reason_unknown_section">Неизвестный раздел</string>
<string name="bad_config_reason_value_out_of_range">Значение вне диапазона</string>
<string name="bad_extension_error">Файл должен быть .conf или .zip</string>
<string name="cancel">Отмена</string>
<string name="config_delete_error">Не удалось удалить файл конфигурации %s</string>
<string name="config_exists_error">Конфигурация для “%s” уже существует</string>
<string name="config_file_exists_error">Файл конфигурации “%s” уже существует</string>
<string name="config_not_found_error">Файл конфигурации “%s” не найден</string>
<string name="config_rename_error">Не удалось переименовать файл конфигурации “%s”</string>
<string name="config_save_error">Не удается сохранить конфигурацию для “%1$s”: %2$s</string>
<string name="config_save_success">Конфигурация для “%s” успешно сохранена</string>
<string name="create_activity_title">Создать WireGuard туннель</string>
<string name="create_bin_dir_error">Не удалось создать локальный двоичный каталог</string>
<string name="create_empty">Создать вручную</string>
<string name="create_from_file">Создать из файла или архива</string>
<string name="create_from_qr_code">Создать из QR-кода</string>
<string name="create_output_dir_error">Не удалось создать выходной каталог</string>
<string name="create_downloads_file_error">Не удалось создать файл в каталоге загрузок</string>
<string name="create_temp_dir_error">Не удалось создать временный локальный каталог</string>
<string name="create_tunnel">Создать туннель</string>
<string name="dark_theme_summary_off">В данный момент используется светлая (дневная) тема</string>
<string name="dark_theme_summary_on">В данный момент используется темная (ночная) тема</string>
<string name="dark_theme_title">Использовать темную тему</string>
<string name="delete">Удалить</string>
<string name="toggle_all">Инвертировать все</string>
<string name="dns_servers">DNS-серверы</string>
<string name="edit">Редактировать</string>
<string name="endpoint">Конечная точка</string>
<string name="error_down">Ошибка при выходе из туннеля: %s</string>
<string name="error_fetching_apps">Ошибка при получении списка приложений: %s</string>
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
<string name="error_up">Ошибка при запуске туннеля: %s</string>
<string name="exclude_private_ips">Исключить частные IP-адреса</string>
<string name="excluded_applications">Исключенные приложения</string>
<string name="generate">Создать</string>
<string name="generic_error">Неизвестная “%s” ошибка</string>
<string name="hint_automatic">(авто)</string>
<string name="hint_generated">(авто)</string>
<string name="hint_optional">(авто)</string>
<string name="hint_random">(авто)</string>
<string name="illegal_filename_error">Неверное имя файла “%s”</string>
<string name="import_error">Не удалось импортировать туннель: %s</string>
<string name="import_from_qr_code">Импортировать туннель из QR-кода</string>
<string name="import_success">Импортировано “%s”</string>
<string name="interface_title">Интерфейс</string>
<string name="key_length_explanation_base64">: Ключи WireGuard base64 должны содержать 44 символа (32 байта)</string>
<string name="key_length_explanation_binary">: Ключи WireGuard должны быть 32 байта</string>
<string name="key_length_explanation_hex">: HEX ключи WireGuard должны содержать 64 символа (32 байта)</string>
<string name="listen_port">Порт</string>
<string name="log_export_error">Не удалось экспортировать логи: %s</string>
<string name="log_export_success">Сохранено в “%s”</string>
<string name="log_export_summary">Файл логов будет сохранен в папке загрузок</string>
<string name="log_export_title">Экспорт логов в файл</string>
<string name="logcat_error">Не удалось запустить logcat: </string>
<string name="module_version_error">Не удалось определить версию модуля ядра</string>
<string name="module_installer_not_found">Для вашего устройства нет доступных модулей</string>
<string name="module_installer_initial">Экспериментальный модуль ядра может улучшить производительность</string>
<string name="module_installer_success">Успех. Приложение перезапустится через 5 секунд</string>
<string name="module_installer_title">Скачать и установить модуль ядра</string>
<string name="module_installer_working">Скачивание и установка…</string>
<string name="module_installer_error">Что-то пошло не так. Пожалуйста, попробуйте еще раз</string>
<string name="mtu">MTU</string>
<string name="name">Имя</string>
<string name="no_config_error">Попытка поднять туннель без конфигурации</string>
<string name="no_configs_error">Конфигурации не найдены</string>
<string name="no_tunnels_error">Туннелей не существует</string>
<string name="parse_error_generic">строка</string>
<string name="parse_error_inet_address">IP-адрес</string>
<string name="parse_error_inet_endpoint">конечная точка</string>
<string name="parse_error_inet_network">IP-сеть</string>
<string name="parse_error_integer">число</string>
<string name="parse_error_reason">Не могу разобрать %1$s “%2$s”</string>
<string name="peer">Пир</string>
<string name="permission_description">Приложение сможет управлять туннелями WireGuard. Приложения с таким разрешением могут по желанию включать и отключать туннели WireGuard, что может привести к неправильному перенаправлению интернет-трафика.</string>
<string name="permission_label">управлять туннелями WireGuard</string>
<string name="persistent_keepalive">Постоянное соединение</string>
<string name="pre_shared_key">Общий ключ</string>
<string name="private_key">Приватный ключ</string>
<string name="public_key">Публичный ключ</string>
<string name="public_key_description">Публичный ключ</string>
<string name="qr_code_hint">Совет: генерировать с `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_summary">Поднимать ранее выбранные туннели при загрузке</string>
<string name="restore_on_boot_title">Восстанавливать при загрузке</string>
<string name="save">Сохранить</string>
<string name="select_all">Выбрать все</string>
<string name="set_exclusions">ОК</string>
<string name="settings">Настройки</string>
<string name="shell_exit_status_read_error">Shell не может прочитать статус выхода</string>
<string name="shell_marker_count_error">Shell ожидает 4 маркера, получено %d</string>
<string name="shell_start_error">Не удалось запустить Shell: %d</string>
<string name="toggle_error">Ошибка переключения туннеля WireGuard: %s</string>
<string name="tools_installer_already">wg и wg-quick уже установлены</string>
<string name="tools_installer_failure">Не удалось установить инструменты командной строки (нет root?)</string>
<string name="tools_installer_initial">Установите дополнительные инструменты для сценариев</string>
<string name="tools_installer_initial_magisk">Установите дополнительные инструменты для сценариев в качестве модуля Magisk</string>
<string name="tools_installer_initial_system">Установите дополнительные инструменты для сценариев в системный раздел</string>
<string name="tools_installer_success_magisk">wg и wg-quick установлены как модуль Magisk (требуется перезагрузка)</string>
<string name="tools_installer_success_system">wg и wg-quick установлены в системный раздел</string>
<string name="tools_installer_title">Установить инструменты командной строки</string>
<string name="tools_installer_working">Установка wg и wg-quick</string>
<string name="tools_unavailable_error">Необходимые инструменты недоступны</string>
<string name="transfer">Статистика</string>
<string name="transfer_rx_tx">Принято: %1$s, Отдано: %2$s</string>
<string name="transfer_bytes">%d Б</string>
<string name="transfer_kibibytes">%.2f Кб</string>
<string name="transfer_mibibytes">%.2f Мб</string>
<string name="transfer_gibibytes">%.2f Гб</string>
<string name="transfer_tibibytes">%.2f Тб</string>
<string name="tun_create_error">Не удалось создать устройство tun</string>
<string name="tunnel_config_error">Не удалось настроить туннель (wg-quick вернул %d)</string>
<string name="tunnel_create_error">Не удалось создать туннель: %s</string>
<string name="tunnel_create_success">Успешно созданный туннель “%s”</string>
<string name="tunnel_error_already_exists">Туннель “%s” уже существует</string>
<string name="tunnel_error_invalid_name">Неправильное имя</string>
<string name="tunnel_list_placeholder">Добавьте туннель с помощью синей кнопки</string>
<string name="tunnel_name">Название туннеля</string>
<string name="tunnel_on_error">Не удалось включить туннель (wgTurnOn вернул %d)</string>
<string name="tunnel_rename_error">Не удалось переименовать туннель: %s</string>
<string name="tunnel_rename_success">Туннель успешно переименован в “%s”</string>
<string name="type_name_go_userspace">Пользовательское пространство</string>
<string name="type_name_kernel_module">Модуль ядра</string>
<string name="unknown_error">Неизвестная ошибка</string>
<string name="version_summary">%1$s v%2$s</string>
<string name="version_summary_checking">Проверка версии %s</string>
<string name="version_summary_unknown">Неизвестная версия %s</string>
<string name="version_title">WireGuard для Android v%s</string>
<string name="vpn_not_authorized_error">VPN-сервис не авторизован пользователем</string>
<string name="vpn_start_error">Не удалось запустить службу Android VPN</string>
<string name="zip_export_error">Не удалось экспортировать туннели: %s</string>
<string name="zip_export_success">Сохранено в “%s”</string>
<string name="zip_export_summary">Zip-архив будет сохранен в папке загрузок</string>
<string name="zip_export_title">Экспорт туннелей в zip-архив</string>
<string name="key_length_error">Неверная длина ключа</string>
<string name="key_contents_error">Плохие символы в ключе</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More