diff options
Diffstat (limited to 'app/src/main/java/com/wireguard/android')
57 files changed, 0 insertions, 7452 deletions
diff --git a/app/src/main/java/com/wireguard/android/Application.java b/app/src/main/java/com/wireguard/android/Application.java deleted file mode 100644 index 744986e8..00000000 --- a/app/src/main/java/com/wireguard/android/Application.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import 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.ModuleLoader; -import com.wireguard.android.util.RootShell; -import com.wireguard.android.util.ToolsInstaller; - -import java.io.File; -import java.lang.ref.WeakReference; - -import java9.util.concurrent.CompletableFuture; - -public class Application extends android.app.Application { - @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 (!app.moduleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) { - try { - app.rootShell.start(); - didStartRootShell = true; - app.moduleLoader.loadModule(); - } catch (final Exception ignored) { - } - } - if (app.moduleLoader.isModuleLoaded()) { - try { - if (!didStartRootShell) - app.rootShell.start(); - backend = new WgQuickBackend(app.getApplicationContext()); - } catch (final Exception ignored) { - } - } - if (backend == null) - backend = new GoBackend(app.getApplicationContext()); - 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); - } - } - - @Override - public void onCreate() { - super.onCreate(); - - asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper())); - rootShell = new RootShell(getApplicationContext()); - toolsInstaller = new ToolsInstaller(getApplicationContext()); - moduleLoader = new ModuleLoader(getApplicationContext()); - - 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); - } -} diff --git a/app/src/main/java/com/wireguard/android/BootShutdownReceiver.java b/app/src/main/java/com/wireguard/android/BootShutdownReceiver.java deleted file mode 100644 index e3ffce7a..00000000 --- a/app/src/main/java/com/wireguard/android/BootShutdownReceiver.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import com.wireguard.android.backend.WgQuickBackend; -import com.wireguard.android.model.TunnelManager; -import com.wireguard.android.util.ExceptionLoggers; - -public class BootShutdownReceiver extends BroadcastReceiver { - private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName(); - - @Override - public void onReceive(final Context context, final Intent intent) { - Application.getBackendAsync().thenAccept(backend -> { - if (!(backend instanceof WgQuickBackend)) - return; - final String action = intent.getAction(); - if (action == null) - return; - final TunnelManager tunnelManager = Application.getTunnelManager(); - if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - Log.i(TAG, "Broadcast receiver restoring state (boot)"); - tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D); - } else if (Intent.ACTION_SHUTDOWN.equals(action)) { - Log.i(TAG, "Broadcast receiver saving state (shutdown)"); - tunnelManager.saveState(); - } - }); - } -} diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java deleted file mode 100644 index 425738ef..00000000 --- a/app/src/main/java/com/wireguard/android/QuickTileService.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android; - -import android.annotation.TargetApi; -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 android.util.Log; -import android.widget.Toast; - -import com.wireguard.android.activity.MainActivity; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.util.ErrorMessages; -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. - */ - -@TargetApi(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 Tunnel 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) { - final Tile tile = getQsTile(); - if (tile != null) { - tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn); - tile.updateTile(); - } - tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished); - } 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 onToggleFinished(@SuppressWarnings("unused") final State state, - @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(); - } - - private void updateTile() { - // Update the tunnel. - final Tunnel 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() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; - } else { - label = getString(R.string.app_name); - state = Tile.STATE_INACTIVE; - } - if (tile == null) - return; - tile.setLabel(label); - if (tile.getState() != state) { - tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff); - tile.setState(state); - } - tile.updateTile(); - } - - private final class OnStateChangedCallback extends OnPropertyChangedCallback { - @Override - public void onPropertyChanged(final Observable sender, final int propertyId) { - if (!Objects.equals(sender, tunnel)) { - sender.removeOnPropertyChangedCallback(this); - return; - } - if (propertyId != 0 && propertyId != BR.state) - return; - updateTile(); - } - } - - private final class OnTunnelChangedCallback extends OnPropertyChangedCallback { - @Override - public void onPropertyChanged(final Observable sender, final int propertyId) { - if (propertyId != 0 && propertyId != BR.lastUsedTunnel) - return; - updateTile(); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/activity/BaseActivity.java b/app/src/main/java/com/wireguard/android/activity/BaseActivity.java deleted file mode 100644 index b27ca6dd..00000000 --- a/app/src/main/java/com/wireguard/android/activity/BaseActivity.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.Tunnel; - -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 Tunnel selectedTunnel; - - public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) { - selectionChangeRegistry.add(listener); - } - - @Nullable - public Tunnel 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 Tunnel oldTunnel, @Nullable Tunnel newTunnel); - - public void removeOnSelectedTunnelChangedListener( - final OnSelectedTunnelChangedListener listener) { - selectionChangeRegistry.remove(listener); - } - - public void setSelectedTunnel(@Nullable final Tunnel tunnel) { - final Tunnel oldTunnel = selectedTunnel; - if (Objects.equals(oldTunnel, tunnel)) - return; - selectedTunnel = tunnel; - onSelectedTunnelChanged(oldTunnel, tunnel); - selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel); - } - - public interface OnSelectedTunnelChangedListener { - void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel); - } - - private static final class SelectionChangeNotifier - extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> { - @Override - public void onNotifyCallback(final OnSelectedTunnelChangedListener listener, - final Tunnel oldTunnel, final int ignored, - final Tunnel newTunnel) { - listener.onSelectedTunnelChanged(oldTunnel, newTunnel); - } - } - - private static final class SelectionChangeRegistry - extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> { - private SelectionChangeRegistry() { - super(new SelectionChangeNotifier()); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/activity/MainActivity.java b/app/src/main/java/com/wireguard/android/activity/MainActivity.java deleted file mode 100644 index ab9a4f8a..00000000 --- a/app/src/main/java/com/wireguard/android/activity/MainActivity.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.ActionBar; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -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.fragment.TunnelListFragment; -import com.wireguard.android.model.Tunnel; - -/** - * 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; - @Nullable private TunnelListFragment listFragment; - - @Override - public void onBackPressed() { - final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount(); - // If the action menu is visible and expanded, collapse it instead of navigating back. - if (isTwoPaneLayout || backStackEntries == 0) { - if (listFragment != null && listFragment.collapseActionMenu()) - return; - } - // 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) { - 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; - listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST"); - getSupportFragmentManager().addOnBackStackChangedListener(this); - onBackStackChanged(); - final View actionBarView = findViewById(R.id.action_bar); - if (actionBarView != null) - actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu()); - } - - @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 Tunnel oldTunnel, - @Nullable final Tunnel 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(); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java b/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java deleted file mode 100644 index 442c93e6..00000000 --- a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 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 (Application.getModuleLoader().isModuleLoaded()) { - screen.removePreference(moduleInstaller); - } else { - Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> { - if (e == null) - moduleInstaller.setVisible(true); - else - screen.removePreference(moduleInstaller); - }); - } - } - } -} diff --git a/app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java b/app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java deleted file mode 100644 index 977f64ea..00000000 --- a/app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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(); - private static boolean lastDarkMode; - @Nullable private static Resources lastResources; - - private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) { - if (resources == lastResources && darkMode == lastDarkMode) - return; - - try { - Field f; - Object o = resources; - try { - f = o.getClass().getDeclaredField("mResourcesImpl"); - f.setAccessible(true); - o = f.get(o); - } catch (final Exception ignored) { - } - f = o.getClass().getDeclaredField("mDrawableCache"); - f.setAccessible(true); - o = f.get(o); - try { - o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1); - } catch (final Exception ignored) { - o.getClass().getMethod("clear").invoke(o); - } - } catch (final Exception e) { - Log.e(TAG, "Failed to flush drawable cache", e); - } - - lastResources = resources; - lastDarkMode = darkMode; - } - - - @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)) { - final boolean darkMode = sharedPreferences.getBoolean(key, false); - AppCompatDelegate.setDefaultNightMode( - sharedPreferences.getBoolean(key, false) ? - AppCompatDelegate.MODE_NIGHT_YES : - AppCompatDelegate.MODE_NIGHT_NO); - invalidateDrawableCache(getResources(), darkMode); - recreate(); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java b/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java deleted file mode 100644 index 08ea8e7e..00000000 --- a/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.Tunnel; - -/** - * 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 Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { - finish(); - } -} diff --git a/app/src/main/java/com/wireguard/android/backend/Backend.java b/app/src/main/java/com/wireguard/android/backend/Backend.java deleted file mode 100644 index a25bfd04..00000000 --- a/app/src/main/java/com/wireguard/android/backend/Backend.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.backend; - -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; -import com.wireguard.config.Config; - -import java.util.Set; - -/** - * Interface for implementations of the WireGuard secure network tunnel. - */ - -public interface Backend { - /** - * Update the volatile configuration of a running tunnel and return the resulting configuration. - * If the tunnel is not up, return the configuration that would result (if known), or else - * simply return the given configuration. - * - * @param tunnel The tunnel to apply the configuration to. - * @param config The new configuration for this tunnel. - * @return The updated configuration of the tunnel. - */ - Config applyConfig(Tunnel tunnel, Config config) throws Exception; - - /** - * Enumerate the names of currently-running tunnels. - * - * @return The set of running tunnel names. - */ - Set<String> enumerate(); - - /** - * Get the actual state of a tunnel. - * - * @param tunnel The tunnel to examine the state of. - * @return The state of the tunnel. - */ - State getState(Tunnel tunnel) throws Exception; - - /** - * Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the - * statistics object will be filled with zero values. - * - * @param tunnel The tunnel to retrieve statistics for. - * @return The statistics for the tunnel. - */ - Statistics getStatistics(Tunnel tunnel) throws Exception; - - /** - * Determine type name of underlying backend. - * - * @return Type name - */ - String getTypePrettyName(); - - /** - * Determine version of underlying backend. - * - * @return The version of the backend. - * @throws Exception - */ - String getVersion() throws Exception; - - /** - * Set the state of a tunnel. - * - * @param tunnel The tunnel to control the state of. - * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or - * {@code TOGGLE}. - * @return The updated state of the tunnel. - */ - State setState(Tunnel tunnel, State state) throws Exception; -} diff --git a/app/src/main/java/com/wireguard/android/backend/GoBackend.java b/app/src/main/java/com/wireguard/android/backend/GoBackend.java deleted file mode 100644 index e85f2b0d..00000000 --- a/app/src/main/java/com/wireguard/android/backend/GoBackend.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.backend; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.ParcelFileDescriptor; -import androidx.annotation.Nullable; -import androidx.collection.ArraySet; -import android.util.Log; - -import com.wireguard.android.Application; -import com.wireguard.android.R; -import com.wireguard.android.activity.MainActivity; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; -import com.wireguard.android.util.ExceptionLoggers; -import com.wireguard.android.util.SharedLibraryLoader; -import com.wireguard.config.Config; -import com.wireguard.config.InetNetwork; -import com.wireguard.config.Peer; - -import java.net.InetAddress; -import java.util.Collections; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import java9.util.concurrent.CompletableFuture; - -public final class GoBackend implements Backend { - private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName(); - private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>(); - - private final Context context; - @Nullable private Tunnel currentTunnel; - private int currentTunnelHandle = -1; - - public GoBackend(final Context context) { - SharedLibraryLoader.loadSharedLibrary(context, "wg-go"); - this.context = context; - } - - private static native int wgGetSocketV4(int handle); - - private static native int wgGetSocketV6(int handle); - - private static native void wgTurnOff(int handle); - - private static native int wgTurnOn(String ifName, int tunFd, String settings); - - private static native String wgVersion(); - - @Override - public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception { - if (tunnel.getState() == State.UP) { - // Restart the tunnel to apply the new config. - setStateInternal(tunnel, tunnel.getConfig(), State.DOWN); - try { - setStateInternal(tunnel, config, State.UP); - } catch (final Exception e) { - // The new configuration didn't work, so try to go back to the old one. - setStateInternal(tunnel, tunnel.getConfig(), State.UP); - throw e; - } - } - return config; - } - - @Override - public Set<String> enumerate() { - if (currentTunnel != null) { - final Set<String> runningTunnels = new ArraySet<>(); - runningTunnels.add(currentTunnel.getName()); - return runningTunnels; - } - return Collections.emptySet(); - } - - @Override - public State getState(final Tunnel tunnel) { - return currentTunnel == tunnel ? State.UP : State.DOWN; - } - - @Override - public Statistics getStatistics(final Tunnel tunnel) { - return new Statistics(); - } - - @Override - public String getTypePrettyName() { - return context.getString(R.string.type_name_go_userspace); - } - - @Override - public String getVersion() { - return wgVersion(); - } - - @Override - public State setState(final Tunnel tunnel, State state) throws Exception { - final State originalState = getState(tunnel); - if (state == State.TOGGLE) - state = originalState == State.UP ? State.DOWN : State.UP; - if (state == originalState) - return originalState; - if (state == State.UP && currentTunnel != null) - throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error)); - Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); - setStateInternal(tunnel, tunnel.getConfig(), state); - return getState(tunnel); - } - - private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) - throws Exception { - - if (state == State.UP) { - Log.i(TAG, "Bringing tunnel up"); - - Objects.requireNonNull(config, context.getString(R.string.no_config_error)); - - if (VpnService.prepare(context) != null) - throw new Exception(context.getString(R.string.vpn_not_authorized_error)); - - final VpnService service; - if (!vpnService.isDone()) - startVpnService(); - - try { - service = vpnService.get(2, TimeUnit.SECONDS); - } catch (final TimeoutException e) { - throw new Exception(context.getString(R.string.vpn_start_error), e); - } - - if (currentTunnelHandle != -1) { - Log.w(TAG, "Tunnel already up"); - return; - } - - // Build config - final String goConfig = config.toWgUserspaceString(); - - // Create the vpn tunnel with android API - final VpnService.Builder builder = service.getBuilder(); - builder.setSession(tunnel.getName()); - - final Intent configureIntent = new Intent(context, MainActivity.class); - configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0)); - - for (final String excludedApplication : config.getInterface().getExcludedApplications()) - builder.addDisallowedApplication(excludedApplication); - - for (final InetNetwork addr : config.getInterface().getAddresses()) - builder.addAddress(addr.getAddress(), addr.getMask()); - - for (final InetAddress addr : config.getInterface().getDnsServers()) - builder.addDnsServer(addr.getHostAddress()); - - for (final Peer peer : config.getPeers()) { - for (final InetNetwork addr : peer.getAllowedIps()) - builder.addRoute(addr.getAddress(), addr.getMask()); - } - - builder.setMtu(config.getInterface().getMtu().orElse(1280)); - - builder.setBlocking(true); - try (final ParcelFileDescriptor tun = builder.establish()) { - if (tun == null) - throw new Exception(context.getString(R.string.tun_create_error)); - Log.d(TAG, "Go backend v" + wgVersion()); - currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig); - } - if (currentTunnelHandle < 0) - throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle)); - - currentTunnel = tunnel; - - service.protect(wgGetSocketV4(currentTunnelHandle)); - service.protect(wgGetSocketV6(currentTunnelHandle)); - } else { - Log.i(TAG, "Bringing tunnel down"); - - if (currentTunnelHandle == -1) { - Log.w(TAG, "Tunnel already down"); - return; - } - - wgTurnOff(currentTunnelHandle); - currentTunnel = null; - currentTunnelHandle = -1; - } - } - - private void startVpnService() { - Log.d(TAG, "Requesting to start VpnService"); - context.startService(new Intent(context, VpnService.class)); - } - - public static class VpnService extends android.net.VpnService { - public Builder getBuilder() { - return new Builder(); - } - - @Override - public void onCreate() { - vpnService.complete(this); - super.onCreate(); - } - - @Override - public void onDestroy() { - Application.getTunnelManager().getTunnels().thenAccept(tunnels -> { - for (final Tunnel tunnel : tunnels) { - if (tunnel != null && tunnel.getState() != State.DOWN) - tunnel.setState(State.DOWN); - } - }); - - vpnService = vpnService.newIncompleteFuture(); - super.onDestroy(); - } - - @Override - public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) { - vpnService.complete(this); - if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) { - Log.d(TAG, "Service started by Always-on VPN feature"); - Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D); - } - return super.onStartCommand(intent, flags, startId); - } - - } -} diff --git a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java deleted file mode 100644 index 71427d8a..00000000 --- a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.backend; - -import android.content.Context; -import androidx.annotation.Nullable; -import android.util.Log; - -import com.wireguard.android.Application; -import com.wireguard.android.R; -import com.wireguard.android.model.Tunnel; -import com.wireguard.android.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.Statistics; -import com.wireguard.config.Config; - -import java.io.File; -import java.io.FileOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.Set; - -import java9.util.stream.Collectors; -import java9.util.stream.Stream; - -/** - * WireGuard backend that uses {@code wg-quick} to implement tunnel configuration. - */ - -public final class WgQuickBackend implements Backend { - private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName(); - - private final File localTemporaryDir; - private final Context context; - - public WgQuickBackend(final Context context) { - localTemporaryDir = new File(context.getCacheDir(), "tmp"); - this.context = context; - } - - @Override - public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception { - if (tunnel.getState() == State.UP) { - // Restart the tunnel to apply the new config. - setStateInternal(tunnel, tunnel.getConfig(), State.DOWN); - try { - setStateInternal(tunnel, config, State.UP); - } catch (final Exception e) { - // The new configuration didn't work, so try to go back to the old one. - setStateInternal(tunnel, tunnel.getConfig(), State.UP); - throw e; - } - } - return config; - } - - @Override - public Set<String> enumerate() { - final List<String> output = new ArrayList<>(); - // Don't throw an exception here or nothing will show up in the UI. - try { - Application.getToolsInstaller().ensureToolsAvailable(); - if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty()) - return Collections.emptySet(); - } catch (final Exception e) { - Log.w(TAG, "Unable to enumerate running tunnels", e); - return Collections.emptySet(); - } - // wg puts all interface names on the same line. Split them into separate elements. - return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet()); - } - - @Override - public State getState(final Tunnel tunnel) { - return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN; - } - - @Override - public Statistics getStatistics(final Tunnel tunnel) { - return new Statistics(); - } - - @Override - public String getTypePrettyName() { - return context.getString(R.string.type_name_kernel_module); - } - - @Override - public String getVersion() throws Exception { - final List<String> output = new ArrayList<>(); - if (Application.getRootShell() - .run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty()) - throw new Exception(context.getString(R.string.module_version_error)); - return output.get(0); - } - - @Override - public State setState(final Tunnel tunnel, State state) throws Exception { - final State originalState = getState(tunnel); - if (state == State.TOGGLE) - state = originalState == State.UP ? State.DOWN : State.UP; - if (state == originalState) - return originalState; - Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); - Application.getToolsInstaller().ensureToolsAvailable(); - setStateInternal(tunnel, tunnel.getConfig(), state); - return getState(tunnel); - } - - private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception { - Objects.requireNonNull(config, "Trying to set state with a null config"); - - final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf"); - try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) { - stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8)); - } - String command = String.format("wg-quick %s '%s'", - state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath()); - if (state == State.UP) - command = "cat /sys/module/wireguard/version && " + command; - final int result = Application.getRootShell().run(null, command); - // noinspection ResultOfMethodCallIgnored - tempFile.delete(); - if (result != 0) - throw new Exception(context.getString(R.string.tunnel_config_error, result)); - } -} diff --git a/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java deleted file mode 100644 index d4761464..00000000 --- a/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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; -} diff --git a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java deleted file mode 100644 index 45f2f759..00000000 --- a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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; - } -} diff --git a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java deleted file mode 100644 index ee216d4c..00000000 --- a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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) : ""); - } -} diff --git a/app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java b/app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java deleted file mode 100644 index e7303eae..00000000 --- a/app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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); - } - } - } -} diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java deleted file mode 100644 index 8b40dd91..00000000 --- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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; - } - } - -} diff --git a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java deleted file mode 100644 index 67059c73..00000000 --- a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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; - -public class AppListDialogFragment extends DialogFragment { - - private static final String KEY_EXCLUDED_APPS = "excludedApps"; - private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>(); - @Nullable private List<String> currentlyExcludedApps; - - 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> appData = new ArrayList<>(); - for (ResolveInfo resolveInfo : resolveInfos) { - String packageName = resolveInfo.activityInfo.packageName; - appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName))); - } - - Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER)); - return appData; - }).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); - currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS); - } - - @Override - public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { - final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); - alertDialogBuilder.setTitle(R.string.excluded_applications); - - final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().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.deselect_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 -> { - for (final ApplicationData app : appData) - app.setExcludedFromTunnel(false); - })); - return dialog; - } - - 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); - } - -} diff --git a/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java b/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java deleted file mode 100644 index 00e26348..00000000 --- a/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.Tunnel; -import com.wireguard.android.model.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 Tunnel pendingTunnel; - @Nullable private Boolean pendingTunnelUp; - - @Nullable - protected Tunnel 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 Tunnel tunnel) { - if (activity != null) - activity.setSelectedTunnel(tunnel); - } - - public void setTunnelState(final View view, final boolean checked) { - final ViewDataBinding binding = DataBindingUtil.findBinding(view); - final Tunnel 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 Tunnel 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 = getContext().getString(messageResId, error); - final View view = getView(); - if (view != null) - Snackbar.make(view, message, Snackbar.LENGTH_LONG).show(); - else - Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); - Log.e(TAG, message, throwable); - }); - } - -} diff --git a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java deleted file mode 100644 index e8f0419c..00000000 --- a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 = Objects.requireNonNull(getActivity()); - - 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); - } - } - -} diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java deleted file mode 100644 index 8d2be476..00000000 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 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.model.Tunnel; - -/** - * Fragment that shows details about a specific tunnel. - */ - -public class TunnelDetailFragment extends BaseFragment { - @Nullable private TunnelDetailFragmentBinding binding; - - @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 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(); - return binding.getRoot(); - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { - if (binding == null) - return; - binding.setTunnel(newTunnel); - if (newTunnel == null) - binding.setConfig(null); - else - newTunnel.getConfigAsync().thenAccept(binding::setConfig); - } - - @Override - public void onViewStateRestored(@Nullable final Bundle savedInstanceState) { - if (binding == null) { - return; - } - - binding.setFragment(this); - onSelectedTunnelChanged(null, getSelectedTunnel()); - super.onViewStateRestored(savedInstanceState); - } - -} diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java deleted file mode 100644 index 4990e69f..00000000 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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 androidx.fragment.app.FragmentManager; -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.Tunnel; -import com.wireguard.android.model.TunnelManager; -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 Tunnel tunnel; - - private void onConfigLoaded(final Config config) { - if (binding != null) { - binding.setConfig(new ConfigProxy(config)); - } - } - - private void onConfigSaved(final Tunnel 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(getContext(), 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(); - 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 Tunnel 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) { - final FragmentManager fragmentManager = getFragmentManager(); - if (fragmentManager != null && binding != null) { - final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications()); - final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this); - fragment.show(fragmentManager, 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 Tunnel oldTunnel, - @Nullable final Tunnel 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 Tunnel 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(getContext(), 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 Tunnel 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); - } - -} diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java deleted file mode 100644 index 59260500..00000000 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java +++ /dev/null @@ -1,479 +0,0 @@ -/* - * 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.fragment.app.FragmentManager; -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.Tunnel; -import com.wireguard.android.util.ErrorMessages; -import com.wireguard.android.widget.MultiselectableRelativeLayout; -import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener; -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 { - private static final int REQUEST_IMPORT = 1; - private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName(); - - private final ActionModeListener actionModeListener = new ActionModeListener(); - @Nullable private ActionMode actionMode; - @Nullable private TunnelListFragmentBinding binding; - - public boolean collapseActionMenu() { - if (binding != null && binding.createMenu.isExpanded()) { - binding.createMenu.collapse(); - return true; - } - return false; - } - - 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… - final FragmentManager fragmentManager = getFragmentManager(); - if (fragmentManager != null) - ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, 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<Tunnel>> 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<Tunnel> tunnels = new ArrayList<>(futureTunnels.size()); - for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) { - Tunnel 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); - } - } - - @SuppressWarnings("deprecation") - @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.tunnelList.setOnTouchListener((view, motionEvent) -> { - if (binding != null) { - binding.createMenu.collapse(); - } - return false; - }); - binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu)); - binding.executePendingBindings(); - return binding.getRoot(); - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void onPause() { - if (binding != null) { - binding.createMenu.collapse(); - } - super.onPause(); - } - - public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) { - startActivity(new Intent(getActivity(), TunnelCreatorActivity.class)); - if (binding != null) - binding.createMenu.collapse(); - } - - public void onRequestImportConfig(@SuppressWarnings("unused") final View view) { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, REQUEST_IMPORT); - if (binding != null) - binding.createMenu.collapse(); - } - - public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) { - final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this); - intentIntegrator.setOrientationLocked(false); - intentIntegrator.setBeepEnabled(false); - intentIntegrator.setPrompt(getString(R.string.qr_code_hint)); - intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE)); - - if (binding != null) - binding.createMenu.collapse(); - } - - @Override - public void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems()); - } - - @Override - public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel 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 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); - } - if (binding != null) { - Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show(); - } - } - - private void onTunnelImportFinished(final List<Tunnel> 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()); - - if (binding != null) - Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show(); - } - - @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, Tunnel>) (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 Tunnel 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<Tunnel> tunnelsToDelete = new ArrayList<>(); - for (final Integer position : copyCheckedItems) - tunnelsToDelete.add(tunnels.get(position)); - - final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete) - .map(Tunnel::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)); - } - } - } - -} diff --git a/app/src/main/java/com/wireguard/android/model/ApplicationData.java b/app/src/main/java/com/wireguard/android/model/ApplicationData.java deleted file mode 100644 index 65edff90..00000000 --- a/app/src/main/java/com/wireguard/android/model/ApplicationData.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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); - } -} diff --git a/app/src/main/java/com/wireguard/android/model/Tunnel.java b/app/src/main/java/com/wireguard/android/model/Tunnel.java deleted file mode 100644 index 49e78a22..00000000 --- a/app/src/main/java/com/wireguard/android/model/Tunnel.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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.util.ExceptionLoggers; -import com.wireguard.config.Config; -import com.wireguard.util.Keyed; - -import java.util.regex.Pattern; - -import java9.util.concurrent.CompletableFuture; -import java9.util.concurrent.CompletionStage; - -/** - * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel. - */ - -public class Tunnel extends BaseObservable implements Keyed<String> { - public static final int NAME_MAX_LENGTH = 15; - private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}"); - - private final TunnelManager manager; - @Nullable private Config config; - private String name; - private State state; - @Nullable private Statistics statistics; - - Tunnel(final TunnelManager manager, final String name, - @Nullable final Config config, final State state) { - this.manager = manager; - this.name = name; - this.config = config; - this.state = state; - } - - public static boolean isNameInvalid(final CharSequence name) { - return !NAME_PATTERN.matcher(name).matches(); - } - - 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; - } - - @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() { - // FIXME: Check age of statistics. - if (statistics == null) - TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E); - return statistics; - } - - public CompletionStage<Statistics> getStatisticsAsync() { - // FIXME: Check age of statistics. - if (statistics == null) - return TunnelManager.getTunnelStatistics(this); - return CompletableFuture.completedFuture(statistics); - } - - Config onConfigChanged(final Config config) { - this.config = config; - notifyPropertyChanged(BR.config); - return config; - } - - public 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; - } - - @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); - } - - public enum State { - DOWN, - TOGGLE, - UP; - - public static State of(final boolean running) { - return running ? UP : DOWN; - } - } - - public static class Statistics extends BaseObservable { - } -} diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java deleted file mode 100644 index 2deea6df..00000000 --- a/app/src/main/java/com/wireguard/android/model/TunnelManager.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * 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.model.Tunnel.State; -import com.wireguard.android.model.Tunnel.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, Tunnel>> 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, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR); - private boolean haveLoaded; - @Nullable private Tunnel lastUsedTunnel; - - public TunnelManager(final ConfigStore configStore) { - this.configStore = configStore; - } - - static CompletionStage<State> getTunnelState(final Tunnel tunnel) { - return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel)) - .thenApply(tunnel::onStateChanged); - } - - static CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) { - return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel)) - .thenApply(tunnel::onStatisticsChanged); - } - - private Tunnel addToList(final String name, @Nullable final Config config, final State state) { - final Tunnel tunnel = new Tunnel(this, name, config, state); - tunnels.add(tunnel); - return tunnel; - } - - public CompletionStage<Tunnel> 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 Tunnel 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); - try { - configStore.delete(tunnel.getName()); - } catch (final Exception e) { - if (originalState == State.UP) - Application.getBackend().setState(tunnel, State.UP); - // 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 Tunnel getLastUsedTunnel() { - return lastUsedTunnel; - } - - CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) { - return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName())) - .thenApply(tunnel::onConfigChanged); - } - - public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() { - return completableTunnels; - } - - public void onCreate() { - Application.getAsyncWorker().supplyAsync(configStore::enumerate) - .thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), 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().enumerate()) - .thenAccept(running -> { - for (final Tunnel 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(Tunnel::getName) - .collect(Collectors.toUnmodifiableSet()); - Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply(); - } - - private void setLastUsedTunnel(@Nullable final Tunnel 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 Tunnel tunnel, final Config config) { - return Application.getAsyncWorker().supplyAsync(() -> { - final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config); - return configStore.save(tunnel.getName(), appliedConfig); - }).thenApply(tunnel::onConfigChanged); - } - - CompletionStage<String> setTunnelName(final Tunnel 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); - configStore.rename(tunnel.getName(), name); - final String newName = tunnel.onNameChanged(name); - if (originalState == State.UP) - Application.getBackend().setState(tunnel, State.UP); - 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 Tunnel tunnel, final State state) { - // Ensure the configuration is loaded before trying to use it. - return tunnel.getConfigAsync().thenCompose(x -> - Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state)) - ).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 Tunnel tunnel = tunnels.get(tunnelName); - if (tunnel == null) - return; - manager.setTunnelState(tunnel, state); - }); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java deleted file mode 100644 index 565854b4..00000000 --- a/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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(); - } - }); - } - -} diff --git a/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java b/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java deleted file mode 100644 index a04bed76..00000000 --- a/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.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(), throwable.getMessage(), 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; - } - } -} diff --git a/app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java b/app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java deleted file mode 100644 index 78a7497b..00000000 --- a/app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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; - } - } -} diff --git a/app/src/main/java/com/wireguard/android/preference/VersionPreference.java b/app/src/main/java/com/wireguard/android/preference/VersionPreference.java deleted file mode 100644 index a0a5d1ff..00000000 --- a/app/src/main/java/com/wireguard/android/preference/VersionPreference.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 java.util.Locale; - -public class VersionPreference extends Preference { - @Nullable private String versionSummary; - - public VersionPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - - Application.getBackendAsync().thenAccept(backend -> { - versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH)); - Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> { - versionSummary = exception == null - ? getContext().getString(R.string.version_summary, backend.getTypePrettyName(), version) - : getContext().getString(R.string.version_summary_unknown, backend.getTypePrettyName().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) { - } - } - -} diff --git a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java deleted file mode 100644 index efda91bb..00000000 --- a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.Tunnel; -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<Tunnel> tunnels) { - final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size()); - for (final Tunnel 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(); - } - }); - } - -} diff --git a/app/src/main/java/com/wireguard/android/util/AsyncWorker.java b/app/src/main/java/com/wireguard/android/util/AsyncWorker.java deleted file mode 100644 index 1d041851..00000000 --- a/app/src/main/java/com/wireguard/android/util/AsyncWorker.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.util; - -import android.os.Handler; - -import java.util.concurrent.Executor; - -import java9.util.concurrent.CompletableFuture; -import java9.util.concurrent.CompletionStage; - -/** - * Helper class for running asynchronous tasks and ensuring they are completed on the main thread. - */ - -public class AsyncWorker { - private final Executor executor; - private final Handler handler; - - public AsyncWorker(final Executor executor, final Handler handler) { - this.executor = executor; - this.handler = handler; - } - - public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) { - final CompletableFuture<Void> future = new CompletableFuture<>(); - executor.execute(() -> { - try { - runnable.run(); - handler.post(() -> future.complete(null)); - } catch (final Throwable t) { - handler.post(() -> future.completeExceptionally(t)); - } - }); - return future; - } - - public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) { - final CompletableFuture<T> future = new CompletableFuture<>(); - executor.execute(() -> { - try { - final T result = supplier.get(); - handler.post(() -> future.complete(result)); - } catch (final Throwable t) { - handler.post(() -> future.completeExceptionally(t)); - } - }); - return future; - } - - @FunctionalInterface - public interface AsyncRunnable<E extends Throwable> { - void run() throws E; - } - - @FunctionalInterface - public interface AsyncSupplier<T, E extends Throwable> { - T get() throws E; - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ClipboardUtils.java b/app/src/main/java/com/wireguard/android/util/ClipboardUtils.java deleted file mode 100644 index 0df5e96a..00000000 --- a/app/src/main/java/com/wireguard/android/util/ClipboardUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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(); - } -} diff --git a/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java b/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java deleted file mode 100644 index efe09f3f..00000000 --- a/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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)); - 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 { - 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); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ErrorMessages.java b/app/src/main/java/com/wireguard/android/util/ErrorMessages.java deleted file mode 100644 index ee9cb12a..00000000 --- a/app/src/main/java/com/wireguard/android/util/ErrorMessages.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 androidx.annotation.Nullable; - -import com.wireguard.android.Application; -import com.wireguard.android.R; -import com.wireguard.config.BadConfigException; -import com.wireguard.config.BadConfigException.Location; -import com.wireguard.config.BadConfigException.Reason; -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<Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of( - Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key, - Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number, - Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value, - Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute, - Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section, - Reason.MISSING_VALUE, R.string.bad_config_reason_missing_value, - Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error, - Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute, - Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section - )); - 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.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) - break; - cause = cause.getCause(); - } - return cause; - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java b/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java deleted file mode 100644 index 5c7a38c0..00000000 --- a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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"); - } -} diff --git a/app/src/main/java/com/wireguard/android/util/FragmentUtils.java b/app/src/main/java/com/wireguard/android/util/FragmentUtils.java deleted file mode 100644 index 5fb9a3bc..00000000 --- a/app/src/main/java/com/wireguard/android/util/FragmentUtils.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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; - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ModuleLoader.java b/app/src/main/java/com/wireguard/android/util/ModuleLoader.java deleted file mode 100644 index 21ff9c77..00000000 --- a/app/src/main/java/com/wireguard/android/util/ModuleLoader.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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.Application; -import com.wireguard.android.BuildConfig; -import com.wireguard.android.util.RootShell.NoRootException; - -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.io.OutputStream; -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 File moduleDir; - private final File tmpDir; - - public ModuleLoader(final Context context) { - moduleDir = new File(context.getCacheDir(), "kmod"); - tmpDir = new File(context.getCacheDir(), "tmp"); - } - - public boolean moduleMightExist() { - return moduleDir.exists() && moduleDir.isDirectory(); - } - - public void loadModule() throws IOException, NoRootException { - Application.getRootShell().run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath())); - } - - public 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, NoRootException, NoSuchAlgorithmException { - final List<String> output = new ArrayList<>(); - Application.getRootShell().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)); - final String userAgent = String.format("WireGuard/%s (Android)", BuildConfig.VERSION_NAME); //TODO: expand a bit - - 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 OutputStream 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); - } - } - 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; - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java b/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java deleted file mode 100644 index 0ba02184..00000000 --- a/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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); - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java b/app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java deleted file mode 100644 index be8ceb9b..00000000 --- a/app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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> { -} diff --git a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java b/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java deleted file mode 100644 index 1d585856..00000000 --- a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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(); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java b/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java deleted file mode 100644 index d796704e..00000000 --- a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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> { -} diff --git a/app/src/main/java/com/wireguard/android/util/RootShell.java b/app/src/main/java/com/wireguard/android/util/RootShell.java deleted file mode 100644 index 1fe5667a..00000000 --- a/app/src/main/java/com/wireguard/android/util/RootShell.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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.annotation.Nullable; -import android.util.Log; - -import com.wireguard.android.BuildConfig; -import com.wireguard.android.R; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.UUID; - -/** - * Helper class for running commands as root. - */ - -public class RootShell { - private static final String SU = "su"; - private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName(); - - private final Context context; - private final String deviceNotRootedMessage; - private final File localBinaryDir; - private final File localTemporaryDir; - private final Object lock = new Object(); - private final String preamble; - @Nullable private Process process; - @Nullable private BufferedReader stderr; - @Nullable private OutputStreamWriter stdin; - @Nullable private BufferedReader stdout; - - public RootShell(final Context context) { - deviceNotRootedMessage = context.getString(R.string.error_root); - localBinaryDir = new File(context.getCodeCacheDir(), "bin"); - localTemporaryDir = new File(context.getCacheDir(), "tmp"); - preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n", - BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir); - this.context = context; - } - - private static boolean isExecutableInPath(final String name) { - final String path = System.getenv("PATH"); - if (path == null) - return false; - for (final String dir : path.split(":")) - if (new File(dir, name).canExecute()) - return true; - return false; - } - - private boolean isRunning() { - synchronized (lock) { - try { - // Throws an exception if the process hasn't finished yet. - if (process != null) - process.exitValue(); - return false; - } catch (final IllegalThreadStateException ignored) { - // The existing process is still running. - return true; - } - } - } - - /** - * Run a command in a root shell. - * - * @param output Lines read from stdout are appended to this list. Pass null if the - * output from the shell is not important. - * @param command Command to run as root. - * @return The exit value of the command. - */ - public int run(@Nullable final Collection<String> output, final String command) - throws IOException, NoRootException { - synchronized (lock) { - /* Start inside synchronized block to prevent a concurrent call to stop(). */ - start(); - final String marker = UUID.randomUUID().toString(); - final String script = "echo " + marker + "; echo " + marker + " >&2; (" + command + - "); ret=$?; echo " + marker + " $ret; echo " + marker + " $ret >&2\n"; - Log.v(TAG, "executing: " + command); - stdin.write(script); - stdin.flush(); - String line; - int errnoStdout = Integer.MIN_VALUE; - int errnoStderr = Integer.MAX_VALUE; - int markersSeen = 0; - while ((line = stdout.readLine()) != null) { - if (line.startsWith(marker)) { - ++markersSeen; - if (line.length() > marker.length() + 1) { - errnoStdout = Integer.valueOf(line.substring(marker.length() + 1)); - break; - } - } else if (markersSeen > 0) { - if (output != null) - output.add(line); - Log.v(TAG, "stdout: " + line); - } - } - while ((line = stderr.readLine()) != null) { - if (line.startsWith(marker)) { - ++markersSeen; - if (line.length() > marker.length() + 1) { - errnoStderr = Integer.valueOf(line.substring(marker.length() + 1)); - break; - } - } else if (markersSeen > 2) { - Log.v(TAG, "stderr: " + line); - } - } - if (markersSeen != 4) - throw new IOException(context.getString(R.string.shell_marker_count_error, markersSeen)); - if (errnoStdout != errnoStderr) - throw new IOException(context.getString(R.string.shell_exit_status_read_error)); - Log.v(TAG, "exit: " + errnoStdout); - return errnoStdout; - } - } - - public void start() throws IOException, NoRootException { - if (!isExecutableInPath(SU)) - throw new NoRootException(deviceNotRootedMessage); - synchronized (lock) { - if (isRunning()) - return; - if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs()) - throw new FileNotFoundException(context.getString(R.string.create_bin_dir_error)); - if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs()) - throw new FileNotFoundException(context.getString(R.string.create_temp_dir_error)); - try { - final ProcessBuilder builder = new ProcessBuilder().command(SU); - builder.environment().put("LC_ALL", "C"); - try { - process = builder.start(); - } catch (final IOException e) { - // A failure at this stage means the device isn't rooted. - throw new NoRootException(deviceNotRootedMessage, e); - } - stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8); - stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), - StandardCharsets.UTF_8)); - stderr = new BufferedReader(new InputStreamReader(process.getErrorStream(), - StandardCharsets.UTF_8)); - stdin.write(preamble); - stdin.flush(); - // Check that the shell started successfully. - final String uid = stdout.readLine(); - if (!"0".equals(uid)) { - Log.w(TAG, "Root check did not return correct UID: " + uid); - throw new NoRootException(deviceNotRootedMessage); - } - if (!isRunning()) { - String line; - while ((line = stderr.readLine()) != null) { - Log.w(TAG, "Root check returned an error: " + line); - if (line.contains("Permission denied")) - throw new NoRootException(deviceNotRootedMessage); - } - throw new IOException(context.getString(R.string.shell_start_error, process.exitValue())); - } - } catch (final IOException | NoRootException e) { - stop(); - throw e; - } - } - } - - public void stop() { - synchronized (lock) { - if (process != null) { - process.destroy(); - process = null; - } - } - } - - public static class NoRootException extends Exception { - public NoRootException(final String message, final Throwable cause) { - super(message, cause); - } - - public NoRootException(final String message) { - super(message); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java b/app/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java deleted file mode 100644 index e3923d19..00000000 --- a/app/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.util; - -import android.content.Context; -import android.os.Build; -import android.util.Log; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public final class SharedLibraryLoader { - private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName(); - - private SharedLibraryLoader() { - } - - public static boolean extractLibrary(final Context context, final String libName, final File destination) throws IOException { - final Collection<String> apks = new HashSet<>(); - if (context.getApplicationInfo().sourceDir != null) - apks.add(context.getApplicationInfo().sourceDir); - if (context.getApplicationInfo().splitSourceDirs != null) - apks.addAll(Arrays.asList(context.getApplicationInfo().splitSourceDirs)); - - for (final String abi : Build.SUPPORTED_ABIS) { - for (final String apk : apks) { - final ZipFile zipFile; - try { - zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ); - } catch (final IOException e) { - throw new RuntimeException(e); - } - - final String mappedLibName = System.mapLibraryName(libName); - final byte[] buffer = new byte[1024 * 32]; - final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName; - final ZipEntry zipEntry = zipFile.getEntry(libZipPath); - if (zipEntry == null) - continue; - Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath()); - try (final FileOutputStream out = new FileOutputStream(destination); - final InputStream in = zipFile.getInputStream(zipEntry)) { - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - } - return true; - } - } - return false; - } - - public static void loadSharedLibrary(final Context context, final String libName) { - Throwable noAbiException; - try { - System.loadLibrary(libName); - return; - } catch (final UnsatisfiedLinkError e) { - Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e); - noAbiException = e; - } - File f = null; - try { - f = File.createTempFile("lib", ".so", context.getCodeCacheDir()); - if (extractLibrary(context, libName, f)) { - System.load(f.getAbsolutePath()); - return; - } - } catch (final Exception e) { - Log.d(TAG, "Failed to load library apk:/" + libName, e); - noAbiException = e; - } finally { - if (f != null) - // noinspection ResultOfMethodCallIgnored - f.delete(); - } - if (noAbiException instanceof RuntimeException) - throw (RuntimeException) noAbiException; - throw new RuntimeException(noAbiException); - } -} diff --git a/app/src/main/java/com/wireguard/android/util/ToolsInstaller.java b/app/src/main/java/com/wireguard/android/util/ToolsInstaller.java deleted file mode 100644 index defdefd8..00000000 --- a/app/src/main/java/com/wireguard/android/util/ToolsInstaller.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.util; - -import android.content.Context; -import androidx.annotation.Nullable; -import android.system.OsConstants; -import android.util.Log; - -import com.wireguard.android.Application; -import com.wireguard.android.BuildConfig; -import com.wireguard.android.R; -import com.wireguard.android.util.RootShell.NoRootException; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -/** - * Helper to install WireGuard tools to the system partition. - */ - -public final class ToolsInstaller { - public static final int ERROR = 0x0; - public static final int MAGISK = 0x4; - public static final int NO = 0x2; - public static final int SYSTEM = 0x8; - public static final int YES = 0x1; - private static final String[] EXECUTABLES = {"wg", "wg-quick"}; - private static final File[] INSTALL_DIRS = { - new File("/system/xbin"), - new File("/system/bin"), - }; - @Nullable private static final File INSTALL_DIR = getInstallDir(); - private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName(); - - private final Context context; - private final File localBinaryDir; - private final Object lock = new Object(); - @Nullable private Boolean areToolsAvailable; - @Nullable private Boolean installAsMagiskModule; - - public ToolsInstaller(final Context context) { - localBinaryDir = new File(context.getCodeCacheDir(), "bin"); - this.context = context; - } - - @Nullable - private static File getInstallDir() { - final String path = System.getenv("PATH"); - if (path == null) - return INSTALL_DIRS[0]; - final List<String> paths = Arrays.asList(path.split(":")); - for (final File dir : INSTALL_DIRS) { - if (paths.contains(dir.getPath()) && dir.isDirectory()) - return dir; - } - return null; - } - - public int areInstalled() throws NoRootException { - if (INSTALL_DIR == null) - return ERROR; - final StringBuilder script = new StringBuilder(); - for (final String name : EXECUTABLES) { - script.append(String.format("cmp -s '%s' '%s' && ", - new File(localBinaryDir, name).getAbsolutePath(), - new File(INSTALL_DIR, name).getAbsolutePath())); - } - script.append("exit ").append(OsConstants.EALREADY).append(';'); - try { - final int ret = Application.getRootShell().run(null, script.toString()); - if (ret == OsConstants.EALREADY) - return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM; - else - return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM; - } catch (final IOException ignored) { - return ERROR; - } - } - - public void ensureToolsAvailable() throws FileNotFoundException { - synchronized (lock) { - if (areToolsAvailable == null) { - try { - Log.d(TAG, extract() ? "Tools are now extracted into our private binary dir" : - "Tools were already extracted into our private binary dir"); - areToolsAvailable = true; - } catch (final IOException e) { - Log.e(TAG, "The wg and wg-quick tools are not available", e); - areToolsAvailable = false; - } - } - if (!areToolsAvailable) - throw new FileNotFoundException( - context.getString(R.string.tools_unavailable_error)); - } - } - - public int install() throws NoRootException, IOException { - return willInstallAsMagiskModule() ? installMagisk() : installSystem(); - } - - private int installMagisk() throws NoRootException, IOException { - extract(); - final StringBuilder script = new StringBuilder("set -ex; "); - - script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; "); - script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR)); - script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - script.append("touch /sbin/.magisk/img/wireguard/auto_mount; "); - for (final String name : EXECUTABLES) { - final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name); - script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ", - new File(localBinaryDir, name), destination, destination, destination)); - } - script.append("trap - INT TERM EXIT;"); - - try { - return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR; - } catch (final IOException ignored) { - return ERROR; - } - } - - private int installSystem() throws NoRootException, IOException { - if (INSTALL_DIR == null) - return OsConstants.ENOENT; - extract(); - final StringBuilder script = new StringBuilder("set -ex; "); - script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; "); - for (final String name : EXECUTABLES) { - final File destination = new File(INSTALL_DIR, name); - script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ", - new File(localBinaryDir, name), destination, destination, destination)); - } - try { - return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR; - } catch (final IOException ignored) { - return ERROR; - } - } - - public boolean extract() throws IOException { - localBinaryDir.mkdirs(); - final File files[] = new File[EXECUTABLES.length]; - boolean allExist = true; - for (int i = 0; i < files.length; ++i) { - files[i] = new File(localBinaryDir, EXECUTABLES[i]); - allExist &= files[i].exists(); - } - if (allExist) - return false; - for (int i = 0; i < files.length; ++i) { - if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], files[i])) - throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]); - if (!files[i].setExecutable(true, false)) - throw new IOException("Unable to mark " + files[i].getAbsolutePath() + " as executable"); - } - return true; - } - - private boolean willInstallAsMagiskModule() { - synchronized (lock) { - if (installAsMagiskModule == null) { - try { - installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS; - } catch (final Exception ignored) { - installAsMagiskModule = false; - } - } - return installAsMagiskModule; - } - } -} diff --git a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java deleted file mode 100644 index bcfe14e3..00000000 --- a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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]; - } - } -} diff --git a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java deleted file mode 100644 index cc9f2dd8..00000000 --- a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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]; - } - } -} diff --git a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java deleted file mode 100644 index 7dc50f09..00000000 --- a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * 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]; - } - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java b/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java deleted file mode 100644 index 79572aa3..00000000 --- a/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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; - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java b/app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java deleted file mode 100644 index 2fe9c924..00000000 --- a/app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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); - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java b/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java deleted file mode 100644 index 07759a18..00000000 --- a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.model.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; - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java b/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java deleted file mode 100644 index 61ce1362..00000000 --- a/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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.annotation.TargetApi; -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 android.util.FloatProperty; - -@TargetApi(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(); - } - - @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; - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java b/app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java deleted file mode 100644 index dcb9aceb..00000000 --- a/app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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); - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java deleted file mode 100644 index 616e176e..00000000 --- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.widget.fab; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.content.Context; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import com.google.android.material.snackbar.Snackbar; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; -import android.util.AttributeSet; -import android.view.View; - -public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> { - - private static final long ANIMATION_DURATION = 250; - private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); - - public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) { - final float origin = child.getBehaviorYTranslation(); - if (Math.abs(destination - origin) < fullSpan / 2) { - child.setBehaviorYTranslation(destination); - return; - } - final ValueAnimator animator = new ValueAnimator(); - animator.setFloatValues(origin, destination); - animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); - animator.setDuration((long) (ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan))); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator a) { - child.setBehaviorYTranslation(destination); - } - }); - animator.addUpdateListener(a -> child.setBehaviorYTranslation((float) a.getAnimatedValue())); - animator.start(); - } - - @Override - public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child, - final View dependency) { - return dependency instanceof Snackbar.SnackbarLayout; - } - - @Override - public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child, - final View dependency) { - animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight()); - return true; - } - - @Override - public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child, - final View dependency) { - animateChange(child, 0, dependency.getMeasuredHeight()); - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java deleted file mode 100644 index 9704081b..00000000 --- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java +++ /dev/null @@ -1,629 +0,0 @@ -/* - * Copyright © 2014 Jerzy Chalupski - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.widget.fab; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.Keep; -import androidx.annotation.Nullable; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import androidx.core.content.res.ResourcesCompat; -import androidx.appcompat.widget.AppCompatTextView; -import android.util.AttributeSet; -import android.view.ContextThemeWrapper; -import android.view.TouchDelegate; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.OvershootInterpolator; -import android.widget.TextView; - -import com.wireguard.android.R; - -public class FloatingActionsMenu extends ViewGroup { - public static final int EXPAND_DOWN = 1; - public static final int EXPAND_LEFT = 2; - public static final int EXPAND_RIGHT = 3; - public static final int EXPAND_UP = 0; - public static final int LABELS_ON_LEFT_SIDE = 0; - public static final int LABELS_ON_RIGHT_SIDE = 1; - private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator(); - private static final int ANIMATION_DURATION = 300; - private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equals("ASUS"); - private static final float COLLAPSED_PLUS_ROTATION = 0f; - private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f); - private static final float EXPANDED_PLUS_ROTATION = 90f + 45f; - private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator(); - private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION); - private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION); - private final Rect touchArea = new Rect(0, 0, 0, 0); - private float behaviorYTranslation; - @Nullable private FloatingActionButton mAddButton; - private int mButtonSpacing; - private int mButtonsCount; - private int mExpandDirection; - private boolean mExpanded; - private int mLabelsMargin; - private int mLabelsPosition; - private int mLabelsStyle; - private int mLabelsVerticalOffset; - @Nullable private OnFloatingActionsMenuUpdateListener mListener; - private int mMaxButtonHeight; - private int mMaxButtonWidth; - @Nullable private RotatingDrawable mRotatingDrawable; - @Nullable private TouchDelegateGroup mTouchDelegateGroup; - private float scrollYTranslation; - - public FloatingActionsMenu(final Context context) { - this(context, null); - } - - public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - init(context, attrs); - } - - private static int adjustForOvershoot(final int dimension) { - return dimension * 12 / 10; - } - - public void addButton(final LabeledFloatingActionButton button) { - addView(button, mButtonsCount - 1); - mButtonsCount++; - - if (mLabelsStyle != 0) { - createLabels(); - } - } - - public void collapse() { - collapse(false); - } - - private void collapse(final boolean immediately) { - if (mExpanded) { - mExpanded = false; - mTouchDelegateGroup.setEnabled(false); - mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION); - mCollapseAnimation.start(); - mExpandAnimation.cancel(); - - if (mListener != null) { - mListener.onMenuCollapsed(); - } - } - } - - public void collapseImmediately() { - collapse(true); - } - - private void createAddButton(final Context context) { - final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme())); - mRotatingDrawable = rotatingDrawable; - - final TimeInterpolator interpolator = new OvershootInterpolator(); - - final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION); - final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION); - - collapseAnimator.setInterpolator(interpolator); - expandAnimator.setInterpolator(interpolator); - - mExpandAnimation.play(expandAnimator); - mCollapseAnimation.play(collapseAnimator); - - mAddButton = new FloatingActionButton(context); - mAddButton.setImageDrawable(rotatingDrawable); - mAddButton.setId(R.id.fab_expand_menu_button); - mAddButton.setOnClickListener(v -> toggle()); - - addView(mAddButton, super.generateDefaultLayoutParams()); - mButtonsCount++; - } - - private void createLabels() { - final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle); - - for (int i = 0; i < mButtonsCount; i++) { - final FloatingActionButton button = (FloatingActionButton) getChildAt(i); - - if (button instanceof LabeledFloatingActionButton) { - final String title = ((LabeledFloatingActionButton) button).getTitle(); - - final AppCompatTextView label = new AppCompatTextView(context); - if (!BROKEN_LABEL_STYLE) - label.setTextAppearance(context, mLabelsStyle); - label.setText(title); - addView(label); - - button.setTag(R.id.fab_label, label); - } - } - } - - public void expand() { - if (!mExpanded) { - mExpanded = true; - mTouchDelegateGroup.setEnabled(true); - mCollapseAnimation.cancel(); - mExpandAnimation.start(); - - if (mListener != null) { - mListener.onMenuExpanded(); - } - } - } - - private boolean expandsHorizontally() { - return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT; - } - - @Override - protected ViewGroup.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(super.generateDefaultLayoutParams()); - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) { - return new LayoutParams(super.generateLayoutParams(attrs)); - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) { - return new LayoutParams(super.generateLayoutParams(p)); - } - - public float getBehaviorYTranslation() { - return behaviorYTranslation; - } - - public float getScrollYTranslation() { - return scrollYTranslation; - } - - private void init(final Context context, @Nullable final AttributeSet attributeSet) { - mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing)); - mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin); - mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset); - - mTouchDelegateGroup = new TouchDelegateGroup(this); - setTouchDelegate(mTouchDelegateGroup); - - final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0); - mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP); - mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0); - mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE); - attr.recycle(); - - if (mLabelsStyle != 0 && expandsHorizontally()) { - throw new IllegalStateException("Action labels in horizontal expand orientation are not supported"); - } - - createAddButton(context); - } - - public boolean isExpanded() { - return mExpanded; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - bringChildToFront(mAddButton); - mButtonsCount = getChildCount(); - - if (mLabelsStyle != 0) { - createLabels(); - } - } - - @Override - protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) { - switch (mExpandDirection) { - case EXPAND_UP: - case EXPAND_DOWN: - final boolean expandUp = mExpandDirection == EXPAND_UP; - - if (changed) { - mTouchDelegateGroup.clearTouchDelegates(); - } - - final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0; - // Ensure mAddButton is centered on the line where the buttons should be - final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE - ? r - l - mMaxButtonWidth / 2 - : mMaxButtonWidth / 2); - final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2; - mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight()); - - final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin; - final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE - ? buttonsHorizontalCenter - labelsOffset - : buttonsHorizontalCenter + labelsOffset; - - int nextY = expandUp ? - addButtonY - mButtonSpacing : - addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing; - - for (int i = mButtonsCount - 1; i >= 0; i--) { - final View child = getChildAt(i); - - if (child == mAddButton || child.getVisibility() == GONE) continue; - - final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2; - final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY; - child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight()); - - final float collapsedTranslation = addButtonY - childY; - final float expandedTranslation = 0f; - - child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation); - child.setAlpha(mExpanded ? 1f : 0f); - - final LayoutParams params = (LayoutParams) child.getLayoutParams(); - params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation); - params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation); - params.setAnimationsTarget(child); - - final View label = (View) child.getTag(R.id.fab_label); - if (label != null) { - final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE - ? labelsXNearButton - label.getMeasuredWidth() - : labelsXNearButton + label.getMeasuredWidth(); - - final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE - ? labelXAwayFromButton - : labelsXNearButton; - - final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE - ? labelsXNearButton - : labelXAwayFromButton; - - final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2; - - label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight()); - - touchArea.set(Math.min(childX, labelLeft), - childY - mButtonSpacing / 2, - Math.max(childX + child.getMeasuredWidth(), labelRight), - childY + child.getMeasuredHeight() + mButtonSpacing / 2); - mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child)); - - label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation); - label.setAlpha(mExpanded ? 1f : 0f); - - final LayoutParams labelParams = (LayoutParams) label.getLayoutParams(); - labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation); - labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation); - labelParams.setAnimationsTarget(label); - } - - nextY = expandUp ? - childY - mButtonSpacing : - childY + child.getMeasuredHeight() + mButtonSpacing; - } - break; - - case EXPAND_LEFT: - case EXPAND_RIGHT: - final boolean expandLeft = mExpandDirection == EXPAND_LEFT; - - final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0; - // Ensure mAddButton is centered on the line where the buttons should be - final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2; - mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight()); - - int nextX = expandLeft ? - addButtonX - mButtonSpacing : - addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing; - - for (int i = mButtonsCount - 1; i >= 0; i--) { - final View child = getChildAt(i); - - if (child == mAddButton || child.getVisibility() == GONE) continue; - - final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX; - final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2; - child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight()); - - final float collapsedTranslation = addButtonX - childX; - final float expandedTranslation = 0f; - - child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation); - child.setAlpha(mExpanded ? 1f : 0f); - - final LayoutParams params = (LayoutParams) child.getLayoutParams(); - params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation); - params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation); - params.setAnimationsTarget(child); - - nextX = expandLeft ? - childX - mButtonSpacing : - childX + child.getMeasuredWidth() + mButtonSpacing; - } - - break; - } - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - measureChildren(widthMeasureSpec, heightMeasureSpec); - - int width = 0; - int height = 0; - - mMaxButtonWidth = 0; - mMaxButtonHeight = 0; - int maxLabelWidth = 0; - - for (int i = 0; i < mButtonsCount; i++) { - final View child = getChildAt(i); - - if (child.getVisibility() == GONE) { - continue; - } - - switch (mExpandDirection) { - case EXPAND_UP: - case EXPAND_DOWN: - mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth()); - height += child.getMeasuredHeight(); - break; - case EXPAND_LEFT: - case EXPAND_RIGHT: - width += child.getMeasuredWidth(); - mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight()); - break; - } - - if (!expandsHorizontally()) { - final TextView label = (TextView) child.getTag(R.id.fab_label); - if (label != null) { - maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth()); - } - } - } - - if (expandsHorizontally()) { - height = mMaxButtonHeight; - } else { - width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0); - } - - switch (mExpandDirection) { - case EXPAND_UP: - case EXPAND_DOWN: - height += mButtonSpacing * (mButtonsCount - 1); - height = adjustForOvershoot(height); - break; - case EXPAND_LEFT: - case EXPAND_RIGHT: - width += mButtonSpacing * (mButtonsCount - 1); - width = adjustForOvershoot(width); - break; - } - - setMeasuredDimension(width, height); - } - - @Override - public void onRestoreInstanceState(final Parcelable state) { - if (state instanceof SavedState) { - final SavedState savedState = (SavedState) state; - mExpanded = savedState.mExpanded; - mTouchDelegateGroup.setEnabled(mExpanded); - - if (mRotatingDrawable != null) { - mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION); - } - - super.onRestoreInstanceState(savedState.getSuperState()); - } else { - super.onRestoreInstanceState(state); - } - } - - @Override - public Parcelable onSaveInstanceState() { - final Parcelable superState = super.onSaveInstanceState(); - final SavedState savedState = new SavedState(superState); - savedState.mExpanded = mExpanded; - - return savedState; - } - - public void removeButton(final LabeledFloatingActionButton button) { - removeView(button.getLabelView()); - removeView(button); - button.setTag(R.id.fab_label, null); - mButtonsCount--; - } - - public void setBehaviorYTranslation(final float behaviorYTranslation) { - this.behaviorYTranslation = behaviorYTranslation; - setTranslationY(behaviorYTranslation + scrollYTranslation); - } - - @Override - public void setEnabled(final boolean enabled) { - super.setEnabled(enabled); - - mAddButton.setEnabled(enabled); - } - - public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) { - mListener = listener; - } - - public void setScrollYTranslation(final float scrollYTranslation) { - this.scrollYTranslation = scrollYTranslation; - setTranslationY(behaviorYTranslation + scrollYTranslation); - } - - public void toggle() { - if (mExpanded) { - collapse(); - } else { - expand(); - } - } - - public interface OnFloatingActionsMenuUpdateListener { - void onMenuCollapsed(); - - void onMenuExpanded(); - } - - private static class RotatingDrawable extends LayerDrawable { - private float mRotation; - - RotatingDrawable(final Drawable drawable) { - super(new Drawable[]{drawable}); - } - - @Override - public void draw(final Canvas canvas) { - canvas.save(); - canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY()); - super.draw(canvas); - canvas.restore(); - } - - @SuppressWarnings("UnusedDeclaration") - public float getRotation() { - return mRotation; - } - - @Keep - @SuppressWarnings("UnusedDeclaration") - public void setRotation(final float rotation) { - mRotation = rotation; - invalidateSelf(); - } - } - - public static class SavedState extends BaseSavedState { - public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { - - @Override - public SavedState createFromParcel(final Parcel in) { - return new SavedState(in); - } - - @Override - public SavedState[] newArray(final int size) { - return new SavedState[size]; - } - }; - private boolean mExpanded; - - public SavedState(final Parcelable parcel) { - super(parcel); - } - - private SavedState(final Parcel in) { - super(in); - mExpanded = in.readInt() == 1; - } - - @Override - public void writeToParcel(final Parcel out, final int flags) { - super.writeToParcel(out, flags); - out.writeInt(mExpanded ? 1 : 0); - } - } - - private class LayoutParams extends ViewGroup.LayoutParams { - - private final ObjectAnimator mCollapseAlpha = new ObjectAnimator(); - private final ObjectAnimator mCollapseDir = new ObjectAnimator(); - private final ObjectAnimator mExpandAlpha = new ObjectAnimator(); - private final ObjectAnimator mExpandDir = new ObjectAnimator(); - private boolean animationsSetToPlay; - - LayoutParams(final ViewGroup.LayoutParams source) { - super(source); - - mExpandDir.setInterpolator(EXPAND_INTERPOLATOR); - mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR); - mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR); - mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR); - - mCollapseAlpha.setProperty(View.ALPHA); - mCollapseAlpha.setFloatValues(1f, 0f); - - mExpandAlpha.setProperty(View.ALPHA); - mExpandAlpha.setFloatValues(0f, 1f); - - switch (mExpandDirection) { - case EXPAND_UP: - case EXPAND_DOWN: - mCollapseDir.setProperty(View.TRANSLATION_Y); - mExpandDir.setProperty(View.TRANSLATION_Y); - break; - case EXPAND_LEFT: - case EXPAND_RIGHT: - mCollapseDir.setProperty(View.TRANSLATION_X); - mExpandDir.setProperty(View.TRANSLATION_X); - break; - } - } - - private void addLayerTypeListener(final Animator animator, final View view) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - view.setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void onAnimationStart(final Animator animation) { - view.setLayerType(LAYER_TYPE_HARDWARE, null); - } - }); - } - - public void setAnimationsTarget(final View view) { - mCollapseAlpha.setTarget(view); - mCollapseDir.setTarget(view); - mExpandAlpha.setTarget(view); - mExpandDir.setTarget(view); - - // Now that the animations have targets, set them to be played - if (!animationsSetToPlay) { - addLayerTypeListener(mExpandDir, view); - addLayerTypeListener(mCollapseDir, view); - - mCollapseAnimation.play(mCollapseAlpha); - mCollapseAnimation.play(mCollapseDir); - mExpandAnimation.play(mExpandAlpha); - mExpandAnimation.play(mExpandDir); - animationsSetToPlay = true; - } - } - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java deleted file mode 100644 index e1af4484..00000000 --- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.widget.fab; - -import androidx.recyclerview.widget.RecyclerView; - -public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener { - private static final float SCALE_FACTOR = 1.5f; - private final FloatingActionsMenu menu; - - public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) { - this.menu = menu; - } - - private static float bound(final float min, final float proposal, final float max) { - return Math.min(max, Math.max(min, proposal)); - } - - @Override - public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) { - super.onScrolled(recyclerView, dx, dy); - menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY())); - } -} diff --git a/app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java b/app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java deleted file mode 100644 index be94a6e5..00000000 --- a/app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright © 2014 Jerzy Chalupski - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.widget.fab; - -import android.content.Context; -import android.content.res.TypedArray; -import androidx.annotation.Nullable; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import android.util.AttributeSet; -import android.widget.TextView; - -import com.wireguard.android.R; - -public class LabeledFloatingActionButton extends FloatingActionButton { - - @Nullable private final String title; - - public LabeledFloatingActionButton(final Context context) { - this(context, null); - } - - public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) { - this(context, attrs, 0); - } - - public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - - final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0); - title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title); - attr.recycle(); - } - - @Nullable - TextView getLabelView() { - return (TextView) getTag(R.id.fab_label); - } - - @Nullable - public String getTitle() { - return title; - } - - @Override - public void setVisibility(final int visibility) { - final TextView label = getLabelView(); - if (label != null) { - label.setVisibility(visibility); - } - - super.setVisibility(visibility); - } - -} diff --git a/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java b/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java deleted file mode 100644 index e16d1d3e..00000000 --- a/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2014 Jerzy Chalupski - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.widget.fab; - -import android.graphics.Rect; -import androidx.annotation.Nullable; -import android.view.MotionEvent; -import android.view.TouchDelegate; -import android.view.View; - -import java.util.ArrayList; -import java.util.Collection; - -public class TouchDelegateGroup extends TouchDelegate { - private static final Rect USELESS_HACKY_RECT = new Rect(); - private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>(); - @Nullable private TouchDelegate mCurrentTouchDelegate; - private boolean mEnabled; - - public TouchDelegateGroup(final View uselessHackyView) { - super(USELESS_HACKY_RECT, uselessHackyView); - } - - public void addTouchDelegate(final TouchDelegate touchDelegate) { - mTouchDelegates.add(touchDelegate); - } - - public void clearTouchDelegates() { - mTouchDelegates.clear(); - mCurrentTouchDelegate = null; - } - - @Override - public boolean onTouchEvent(final MotionEvent event) { - if (!mEnabled) - return false; - - TouchDelegate delegate = null; - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - for (final TouchDelegate touchDelegate : mTouchDelegates) { - if (touchDelegate.onTouchEvent(event)) { - mCurrentTouchDelegate = touchDelegate; - return true; - } - } - break; - - case MotionEvent.ACTION_MOVE: - delegate = mCurrentTouchDelegate; - break; - - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - delegate = mCurrentTouchDelegate; - mCurrentTouchDelegate = null; - break; - } - - return delegate != null && delegate.onTouchEvent(event); - } - - public void removeTouchDelegate(final TouchDelegate touchDelegate) { - mTouchDelegates.remove(touchDelegate); - if (mCurrentTouchDelegate == touchDelegate) { - mCurrentTouchDelegate = null; - } - } - - public void setEnabled(final boolean enabled) { - mEnabled = enabled; - } -} |