*
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.tasker
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.github.shadowsocks.R
+import com.github.shadowsocks.database.ProfileManager
+import com.twofortyfouram.locale.api.Intent as ApiIntent
+
+class Settings(bundle: Bundle) {
+ companion object {
+ private val KEY_SWITCH_ON = "switch_on"
+ private val KEY_PROFILE_ID = "profile_id"
+
+ fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
+ }
+
+ var switchOn: Boolean = bundle.getBoolean(KEY_SWITCH_ON, true)
+ var profileId: Int = bundle.getInt(KEY_PROFILE_ID, -1)
+
+ fun toIntent(context: Context): Intent {
+ val bundle = Bundle()
+ if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false)
+ if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
+ val profile = ProfileManager.getProfile(profileId)
+ return Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
+ if (profile != null)
+ context.getString(if (switchOn) R.string.start_service else R.string.stop_service, profile.formattedName)
+ else context.getString(if (switchOn) R.string.start_service_default else R.string.stop))
+ }
+}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java
deleted file mode 100644
index e364c567..00000000
--- a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package com.github.shadowsocks.utils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.StringTokenizer;
-
-/**
- * Commandline objects help handling command lines specifying processes to
- * execute.
- *
- * The class can be used to define a command line as nested elements or as a
- * helper to define a command line by an application.
- *
- *
- * <someelement>
- * <acommandline executable="/executable/to/run">
- * <argument value="argument 1" />
- * <argument line="argument_1 argument_2 argument_3" />
- * <argument value="argument 4" />
- * </acommandline>
- * </someelement>
- *
- *
- * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
- *
- * Adds support for escape character '\'.
- *
- * @author Mygod
- */
-public final class Commandline {
- private Commandline() { }
-
- /**
- * Quote the parts of the given array in way that makes them
- * usable as command line arguments.
- * @param args the list of arguments to quote.
- * @return empty string for null or no command, else every argument split
- * by spaces and quoted by quoting rules.
- */
- public static String toString(Iterable args) {
- // empty path return empty string
- if (args == null) {
- return "";
- }
- // path containing one or more elements
- final StringBuilder result = new StringBuilder();
- for (String arg : args) {
- if (result.length() > 0) result.append(' ');
- for (int j = 0; j < arg.length(); ++j) {
- char ch = arg.charAt(j);
- switch (ch) {
- case ' ': case '\\': case '"': case '\'': result.append('\\'); // intentionally no break
- default: result.append(ch);
- }
- }
- }
- return result.toString();
- }
- /**
- * Quote the parts of the given array in way that makes them
- * usable as command line arguments.
- * @param args the list of arguments to quote.
- * @return empty string for null or no command, else every argument split
- * by spaces and quoted by quoting rules.
- */
- public static String toString(String[] args) {
- return toString(Arrays.asList(args)); // thanks to Java, arrays aren't iterable
- }
-
- /**
- * Crack a command line.
- * @param toProcess the command line to process.
- * @return the command line broken into strings.
- * An empty or null toProcess parameter results in a zero sized array.
- */
- public static String[] translateCommandline(String toProcess) {
- if (toProcess == null || toProcess.length() == 0) {
- //no command? no string
- return new String[0];
- }
- // parse with a simple finite state machine
-
- final int normal = 0;
- final int inQuote = 1;
- final int inDoubleQuote = 2;
- int state = normal;
- final StringTokenizer tok = new StringTokenizer(toProcess, "\\\"\' ", true);
- final ArrayList result = new ArrayList<>();
- final StringBuilder current = new StringBuilder();
- boolean lastTokenHasBeenQuoted = false;
- boolean lastTokenIsSlash = false;
-
- while (tok.hasMoreTokens()) {
- String nextTok = tok.nextToken();
- switch (state) {
- case inQuote:
- if ("\'".equals(nextTok)) {
- lastTokenHasBeenQuoted = true;
- state = normal;
- } else {
- current.append(nextTok);
- }
- break;
- case inDoubleQuote:
- if ("\"".equals(nextTok)) {
- if (lastTokenIsSlash) {
- current.append(nextTok);
- lastTokenIsSlash = false;
- } else {
- lastTokenHasBeenQuoted = true;
- state = normal;
- }
- } else if ("\\".equals(nextTok)) {
- if (lastTokenIsSlash) {
- current.append(nextTok);
- lastTokenIsSlash = false;
- } else lastTokenIsSlash = true;
- } else {
- if (lastTokenIsSlash) {
- current.append("\\"); // unescaped
- lastTokenIsSlash = false;
- }
- current.append(nextTok);
- }
- break;
- default:
- if (lastTokenIsSlash) {
- current.append(nextTok);
- lastTokenIsSlash = false;
- } else if ("\\".equals(nextTok)) lastTokenIsSlash = true; else if ("\'".equals(nextTok)) {
- state = inQuote;
- } else if ("\"".equals(nextTok)) {
- state = inDoubleQuote;
- } else if (" ".equals(nextTok)) {
- if (lastTokenHasBeenQuoted || current.length() != 0) {
- result.add(current.toString());
- current.setLength(0);
- }
- } else {
- current.append(nextTok);
- }
- lastTokenHasBeenQuoted = false;
- break;
- }
- }
- if (lastTokenHasBeenQuoted || current.length() != 0) {
- result.add(current.toString());
- }
- if (state == inQuote || state == inDoubleQuote) {
- throw new IllegalArgumentException("unbalanced quotes in " + toProcess);
- }
- if (lastTokenIsSlash) throw new IllegalArgumentException("escape character following nothing in " + toProcess);
- return result.toArray(new String[result.size()]);
- }
-}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.kt
new file mode 100644
index 00000000..35096d5e
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.kt
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.utils
+
+import java.util.ArrayList
+import java.util.Arrays
+import java.util.StringTokenizer
+
+/**
+ * Commandline objects help handling command lines specifying processes to
+ * execute.
+ *
+ * The class can be used to define a command line as nested elements or as a
+ * helper to define a command line by an application.
+ *
+ *
+ * `
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+` *
+ *
+ * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
+ *
+ * Adds support for escape character '\'.
+ */
+object Commandline {
+
+ /**
+ * Quote the parts of the given array in way that makes them
+ * usable as command line arguments.
+ * @param args the list of arguments to quote.
+ * @return empty string for null or no command, else every argument split
+ * by spaces and quoted by quoting rules.
+ */
+ fun toString(args: Iterable?): String {
+ // empty path return empty string
+ if (args == null) {
+ return ""
+ }
+ // path containing one or more elements
+ val result = StringBuilder()
+ for (arg in args) {
+ if (result.isNotEmpty()) result.append(' ')
+ (0 until arg.length)
+ .map { arg[it] }
+ .forEach {
+ when (it) {
+ ' ', '\\', '"', '\'' -> {
+ result.append('\\') // intentionally no break
+ result.append(it)
+ }
+ else -> result.append(it)
+ }
+ }
+ }
+ return result.toString()
+ }
+
+ /**
+ * Quote the parts of the given array in way that makes them
+ * usable as command line arguments.
+ * @param args the list of arguments to quote.
+ * @return empty string for null or no command, else every argument split
+ * by spaces and quoted by quoting rules.
+ */
+ fun toString(args: Array): String {
+ return toString(Arrays.asList(*args)) // thanks to Java, arrays aren't iterable
+ }
+
+ /**
+ * Crack a command line.
+ * @param toProcess the command line to process.
+ * @return the command line broken into strings.
+ * An empty or null toProcess parameter results in a zero sized array.
+ */
+ fun translateCommandline(toProcess: String?): Array {
+ if (toProcess == null || toProcess.isEmpty()) {
+ //no command? no string
+ return arrayOf()
+ }
+ // parse with a simple finite state machine
+
+ val normal = 0
+ val inQuote = 1
+ val inDoubleQuote = 2
+ var state = normal
+ val tok = StringTokenizer(toProcess, "\\\"\' ", true)
+ val result = ArrayList()
+ val current = StringBuilder()
+ var lastTokenHasBeenQuoted = false
+ var lastTokenIsSlash = false
+
+ while (tok.hasMoreTokens()) {
+ val nextTok = tok.nextToken()
+ when (state) {
+ inQuote -> if ("\'" == nextTok) {
+ lastTokenHasBeenQuoted = true
+ state = normal
+ } else {
+ current.append(nextTok)
+ }
+ inDoubleQuote -> if ("\"" == nextTok) {
+ if (lastTokenIsSlash) {
+ current.append(nextTok)
+ lastTokenIsSlash = false
+ } else {
+ lastTokenHasBeenQuoted = true
+ state = normal
+ }
+ } else if ("\\" == nextTok) {
+ lastTokenIsSlash = if (lastTokenIsSlash) {
+ current.append(nextTok)
+ false
+ } else
+ true
+ } else {
+ if (lastTokenIsSlash) {
+ current.append("\\") // unescaped
+ lastTokenIsSlash = false
+ }
+ current.append(nextTok)
+ }
+ else -> {
+ if (lastTokenIsSlash) {
+ current.append(nextTok)
+ lastTokenIsSlash = false
+ } else if ("\\" == nextTok)
+ lastTokenIsSlash = true
+ else if ("\'" == nextTok) {
+ state = inQuote
+ } else if ("\"" == nextTok) {
+ state = inDoubleQuote
+ } else if (" " == nextTok) {
+ if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
+ result.add(current.toString())
+ current.setLength(0)
+ }
+ } else {
+ current.append(nextTok)
+ }
+ lastTokenHasBeenQuoted = false
+ }
+ }
+ }
+ if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
+ result.add(current.toString())
+ }
+ if (state == inQuote || state == inDoubleQuote) {
+ throw IllegalArgumentException("unbalanced quotes in " + toProcess)
+ }
+ if (lastTokenIsSlash) throw IllegalArgumentException("escape character following nothing in " + toProcess)
+ return result.toTypedArray()
+ }
+}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt
new file mode 100644
index 00000000..b8f04e22
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.utils
+
+object Key {
+ const val PROFILE = "profile.db"
+
+ const val id = "profileId"
+ const val name = "profileName"
+
+ const val individual = "Proxyed"
+
+ const val serviceMode = "serviceMode"
+ const val modeProxy = "proxy"
+ const val modeVpn = "vpn"
+ const val modeTransproxy = "transproxy"
+ const val portProxy = "portProxy"
+ const val portLocalDns = "portLocalDns"
+ const val portTransproxy = "portTransproxy"
+
+ const val route = "route"
+
+ const val isAutoConnect = "isAutoConnect"
+
+ const val proxyApps = "isProxyApps"
+ const val bypass = "isBypassApps"
+ const val udpdns = "isUdpDns"
+ const val ipv6 = "isIpv6"
+
+ const val host = "proxy"
+ const val password = "sitekey"
+ const val method = "encMethod"
+ const val remotePort = "remotePortNum"
+ const val remoteDns = "remoteDns"
+
+ const val plugin = "plugin"
+ const val pluginConfigure = "plugin.configure"
+
+ const val dirty = "profileDirty"
+
+ const val tfo = "tcp_fastopen"
+ const val assetUpdateTime = "assetUpdateTime"
+}
+
+object Action {
+ const val SERVICE = "com.github.shadowsocks.SERVICE"
+ const val CLOSE = "com.github.shadowsocks.CLOSE"
+ const val RELOAD = "com.github.shadowsocks.RELOAD"
+
+ const val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
+}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
deleted file mode 100644
index 921c652d..00000000
--- a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2010 Brave New Software Project, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.github.shadowsocks.utils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.LinkProperties;
-import android.os.Build;
-
-import java.lang.reflect.Method;
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.Collection;
-
-public class Dns {
- public static String getDnsResolver(Context context) throws Exception {
- Collection dnsResolvers = getDnsResolvers(context);
- if (dnsResolvers.isEmpty()) {
- throw new Exception("Couldn't find an active DNS resolver");
- }
- String dnsResolver = dnsResolvers.iterator().next().toString();
- if (dnsResolver.startsWith("/")) {
- dnsResolver = dnsResolver.substring(1);
- }
- return dnsResolver;
- }
-
- private static Collection getDnsResolvers(Context context) throws Exception {
- ArrayList addresses = new ArrayList();
- ConnectivityManager connectivityManager =
- (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
- Class> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
- Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties");
- Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager);
- if (linkProperties != null) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses");
- Collection> dnses = (Collection>)getDnsesMethod.invoke(linkProperties);
- for (Object dns : dnses) {
- addresses.add((InetAddress)dns);
- }
- } else {
- addresses.addAll(((LinkProperties) linkProperties).getDnsServers());
- }
- }
-
- return addresses;
- }
-}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt
new file mode 100644
index 00000000..e2948430
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.utils
+
+import java.net.InetAddress
+import java.util.*
+
+class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable {
+ companion object {
+ @Throws(IllegalArgumentException::class)
+ fun fromString(value: String): Subnet {
+ val parts = value.split('/')
+ val addr = parts[0].parseNumericAddress()
+ return when (parts.size) {
+ 1 -> Subnet(addr, addr.address.size shl 3)
+ 2 -> Subnet(addr, parts[1].toInt())
+ else -> throw IllegalArgumentException()
+ }
+ }
+ }
+
+ private val addressLength get() = address.address.size shl 3
+
+ init {
+ if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException()
+ }
+
+ override fun toString(): String =
+ if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
+
+ private fun Byte.unsigned() = toInt() and 0xFF
+ override fun compareTo(other: Subnet): Int {
+ val addrThis = address.address
+ val addrThat = other.address.address
+ var result = addrThis.size.compareTo(addrThat.size) // IPv4 address goes first
+ if (result != 0) return result
+ for ((x, y) in addrThis zip addrThat) {
+ result = x.unsigned().compareTo(y.unsigned()) // undo sign extension of signed byte
+ if (result != 0) return result
+ }
+ return prefixSize.compareTo(other.prefixSize)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ val that = other as? Subnet
+ return address == that?.address && prefixSize == that.prefixSize
+ }
+ override fun hashCode(): Int = Objects.hash(address, prefixSize)
+}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt b/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt
new file mode 100644
index 00000000..bd38cf62
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.utils
+
+import eu.chainfire.libsuperuser.Shell
+import java.io.File
+
+object TcpFastOpen {
+ /**
+ * Is kernel version >= 3.7.1.
+ */
+ val supported by lazy {
+ val match = """^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version"))
+ if (match == null) false else when (match.groupValues[1].toInt()) {
+ in Int.MIN_VALUE .. 2 -> false
+ 3 -> when (match.groupValues[2].toInt()) {
+ in Int.MIN_VALUE .. 6 -> false
+ 7 -> match.groupValues[3].toInt() >= 1
+ else -> true
+ }
+ else -> true
+ }
+ }
+
+ val sendEnabled: Boolean get() {
+ val file = File("/proc/sys/net/ipv4/tcp_fastopen")
+ return file.canRead() && file.bufferedReader().use { it.readText() }.trim().toInt() and 1 > 0
+ }
+
+ fun enabled(value: Boolean): String? = if (sendEnabled == value) null else Shell.run("su", arrayOf(
+ "if echo " + (if (value) 3 else 0) + " > /proc/sys/net/ipv4/tcp_fastopen; then",
+ " echo Success.",
+ "else",
+ " echo Failed.",
+ "fi"), null, true)?.joinToString("\n")
+}
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt
new file mode 100644
index 00000000..4b5decbc
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt
@@ -0,0 +1,95 @@
+package com.github.shadowsocks.utils
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Build
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentManager
+import android.support.v7.util.SortedList
+import android.util.DisplayMetrics
+import android.view.Gravity
+import android.view.View
+import android.view.Window
+import android.widget.Toast
+import com.github.shadowsocks.App.Companion.app
+import java.lang.reflect.InvocationTargetException
+import java.net.InetAddress
+import java.net.URLConnection
+
+private val isNumericMethod by lazy { InetAddress::class.java.getMethod("isNumeric", String::class.java) }
+private val parseNumericAddressMethod by lazy {
+ InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
+}
+private val fieldChildFragmentManager by lazy {
+ val field = Fragment::class.java.getDeclaredField("mChildFragmentManager")
+ field.isAccessible = true
+ field
+}
+
+fun String.isNumericAddress(): Boolean = isNumericMethod.invoke(null, this) as Boolean
+fun String.parseNumericAddress(): InetAddress = try {
+ parseNumericAddressMethod.invoke(null, this) as InetAddress
+} catch (exc: InvocationTargetException) {
+ throw exc.cause ?: exc
+}
+
+fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
+ val x = str?.toIntOrNull() ?: default
+ return if (x < min || x > 65535) default else x
+}
+
+fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
+}
+
+/**
+ * Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
+ */
+fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null,
+ name: String? = null, priority: Int = -1, block: () -> Unit): Thread {
+ val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
+ thread.setUncaughtExceptionHandler(app::track)
+ if (start) thread.start()
+ return thread
+}
+
+val URLConnection.responseLength: Long
+ get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
+
+/**
+ * Based on: http://stackoverflow.com/a/21026866/2245107
+ */
+fun Toast.position(view: View, window: Window, offsetX: Int = 0, offsetY: Int = 0): Toast {
+ val rect = Rect()
+ window.decorView.getWindowVisibleDisplayFrame(rect)
+ val viewLocation = IntArray(2)
+ view.getLocationInWindow(viewLocation)
+ val metrics = DisplayMetrics()
+ window.windowManager.defaultDisplay.getMetrics(metrics)
+ val toastView = this.view
+ toastView.measure(View.MeasureSpec.makeMeasureSpec(metrics.widthPixels, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(metrics.heightPixels, View.MeasureSpec.UNSPECIFIED))
+ setGravity(Gravity.START or Gravity.TOP,
+ viewLocation[0] - rect.left + (view.width - toastView.measuredWidth) / 2 + offsetX,
+ viewLocation[1] - rect.top + view.height + offsetY)
+ return this
+}
+
+/**
+ * Based on: https://stackoverflow.com/a/15656428/2245107
+ */
+var Fragment.childFragManager: FragmentManager?
+ get() = childFragmentManager
+ set(value) = fieldChildFragmentManager.set(this, value)
+
+private class SortedListIterable(private val list: SortedList) : Iterable {
+ override fun iterator(): Iterator = SortedListIterator(list)
+}
+private class SortedListIterator(private val list: SortedList) : Iterator {
+ private var count = 0
+ override fun hasNext() = count < list.size()
+ override fun next(): T = list[count++]
+}
+fun SortedList.asIterable(): Iterable = SortedListIterable(this)
diff --git a/mobile/src/main/java/com/github/shadowsocks/widget/BoundedGridLayout.kt b/mobile/src/main/java/com/github/shadowsocks/widget/BoundedGridLayout.kt
new file mode 100644
index 00000000..19a6efab
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/widget/BoundedGridLayout.kt
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.GridLayout
+import com.github.shadowsocks.R
+
+/**
+ * Based on: http://stackoverflow.com/a/6212120/2245107
+ */
+class BoundedGridLayout(context: Context, attrs: AttributeSet) : GridLayout(context, attrs) {
+ private val boundedWidth: Int
+ private val boundedHeight: Int
+
+ init {
+ val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedGridLayout)
+ boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_width, 0)
+ boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_height, 0)
+ arr.recycle()
+ }
+
+ override fun onMeasure(widthSpec: Int, heightSpec: Int) = super.onMeasure(
+ if (boundedWidth <= 0 || boundedWidth >= MeasureSpec.getSize(widthSpec)) widthSpec
+ else MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthSpec)),
+ if (boundedHeight <= 0 || boundedHeight >= MeasureSpec.getSize(heightSpec)) heightSpec
+ else MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightSpec)))
+}
diff --git a/mobile/src/main/java/com/github/shadowsocks/widget/UndoSnackbarManager.kt b/mobile/src/main/java/com/github/shadowsocks/widget/UndoSnackbarManager.kt
new file mode 100644
index 00000000..98bf8c99
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/widget/UndoSnackbarManager.kt
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package com.github.shadowsocks.widget
+
+import android.support.design.widget.Snackbar
+import android.view.View
+import com.github.shadowsocks.R
+
+/**
+ * @param view The view to find a parent from.
+ * @param undo Callback for undoing removals.
+ * @param commit Callback for committing removals.
+ * @tparam T Item type.
+ */
+class UndoSnackbarManager(private val view: View, private val undo: (List>) -> Unit,
+ commit: ((List>) -> Unit)? = null) {
+ private val recycleBin = ArrayList>()
+ private val removedCallback = object : Snackbar.Callback() {
+ override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
+ when (event) {
+ DISMISS_EVENT_SWIPE, DISMISS_EVENT_MANUAL, DISMISS_EVENT_TIMEOUT -> {
+ commit?.invoke(recycleBin)
+ recycleBin.clear()
+ }
+ }
+ last = null
+ }
+ }
+ private var last: Snackbar? = null
+
+ fun remove(items: Collection>) {
+ recycleBin.addAll(items)
+ val count = recycleBin.size
+ val snackbar = Snackbar
+ .make(view, view.resources.getQuantityString(R.plurals.removed, count, count), Snackbar.LENGTH_LONG)
+ .addCallback(removedCallback)
+ .setAction(R.string.undo, {
+ undo(recycleBin.reversed())
+ recycleBin.clear()
+ })
+ snackbar.show()
+ last = snackbar
+ }
+ fun remove(vararg items: Pair) = remove(items.toList())
+
+ fun flush() {
+ last?.dismiss()
+ last = null
+ }
+}
diff --git a/mobile/src/main/jni/Application.mk b/mobile/src/main/jni/Application.mk
deleted file mode 100644
index f80bef09..00000000
--- a/mobile/src/main/jni/Application.mk
+++ /dev/null
@@ -1,4 +0,0 @@
-APP_ABI := armeabi-v7a arm64-v8a x86
-APP_PLATFORM := android-19
-APP_STL := c++_static
-NDK_TOOLCHAIN_VERSION := clang
diff --git a/mobile/src/main/res/font/iceland.xml b/mobile/src/main/res/font/iceland.xml
new file mode 100644
index 00000000..b887eb61
--- /dev/null
+++ b/mobile/src/main/res/font/iceland.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/mobile/src/main/res/layout/layout_header.xml b/mobile/src/main/res/layout/layout_header.xml
index d9fd41f5..0488a489 100644
--- a/mobile/src/main/res/layout/layout_header.xml
+++ b/mobile/src/main/res/layout/layout_header.xml
@@ -1,7 +1,8 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ android:id="@+id/drawer_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:fontFamily="@font/iceland"
+ android:gravity="center"
+ android:text="shadowsocks"
+ android:textColor="@android:color/white"
+ android:textSize="32sp"
+ tools:ignore="HardcodedText"/>
diff --git a/mobile/src/main/res/layout/layout_main.xml b/mobile/src/main/res/layout/layout_main.xml
index b2bf4ebb..e995094d 100644
--- a/mobile/src/main/res/layout/layout_main.xml
+++ b/mobile/src/main/res/layout/layout_main.xml
@@ -30,40 +30,40 @@
-
+
-
-
+
-
-
+
-
-
+
-
+ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_columnWeight="1"
+ android:layout_gravity="fill_horizontal" android:gravity="end"/>