diff --git a/README.md b/README.md index 3bcf2be2..6b3894b5 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status](https://api.travis-ci.org/shadowsocks/shadowsocks-android.svg)](https://travis-ci.org/shadowsocks/shadowsocks-android) [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) [![Releases](https://img.shields.io/github/downloads/shadowsocks/shadowsocks-android/total.svg)](https://github.com/shadowsocks/shadowsocks-android/releases) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a21d48d466644cdbcb57a1889abea5b)](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin. diff --git a/core/gfwlist/gen.py b/core/gfwlist/gen.py index 22ab9048..ddc250ad 100644 --- a/core/gfwlist/gen.py +++ b/core/gfwlist/gen.py @@ -1,8 +1,6 @@ #!/usr/bin/python # -*- encoding: utf8 -*- -import itertools -import math import sys import IPy diff --git a/detekt.yml b/detekt.yml index c8c71bcb..ef1033e1 100644 --- a/detekt.yml +++ b/detekt.yml @@ -28,8 +28,7 @@ complexity: threshold: 5 ignoreDefaultParameters: true MethodOverloading: - active: true - threshold: 5 + active: false NestedBlockDepth: active: true threshold: 3 diff --git a/mobile/src/main/java/com/github/shadowsocks/AppManager.kt b/mobile/src/main/java/com/github/shadowsocks/AppManager.kt index 9c6c1d5f..598bff2b 100644 --- a/mobile/src/main/java/com/github/shadowsocks/AppManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/AppManager.kt @@ -44,7 +44,6 @@ import android.support.v7.widget.Toolbar import android.view.* import android.widget.ImageView import android.widget.Switch -import android.widget.Toast import com.futuremind.recyclerviewfastscroll.FastScroller import com.futuremind.recyclerviewfastscroll.SectionTitleProvider import com.github.shadowsocks.App.Companion.app @@ -264,11 +263,9 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener { return false } - override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - return if (keyCode == KeyEvent.KEYCODE_MENU) - if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu() - else super.onKeyUp(keyCode, event) - } + override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU) + if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu() + else super.onKeyUp(keyCode, event) override fun onDestroy() { instance = null diff --git a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt index 301490a2..fae6cc75 100644 --- a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt @@ -26,7 +26,6 @@ import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v14.preference.SwitchPreference import android.support.v7.preference.Preference -import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.utils.DirectBoot diff --git a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt index d8842a99..c027ecbf 100644 --- a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt +++ b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt @@ -62,6 +62,7 @@ import com.mikepenz.materialdrawer.Drawer import com.mikepenz.materialdrawer.DrawerBuilder import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem +import java.io.IOException import java.net.HttpURLConnection import java.net.InetSocketAddress import java.net.Proxy @@ -178,8 +179,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe val elapsed = SystemClock.elapsedRealtime() - start if (code == 204 || code == 200 && conn.responseLength == 0L) Pair(true, getString(R.string.connection_test_available, elapsed)) - else throw Exception(getString(R.string.connection_test_error_status_code, code)) - } catch (e: Exception) { + else throw IOException(getString(R.string.connection_test_error_status_code, code)) + } catch (e: IOException) { Pair(false, getString(R.string.connection_test_error, e.message)) } finally { conn.disconnect() diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt index 98817951..f71be5d9 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt @@ -179,27 +179,25 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu } else super.onActivityResult(requestCode, resultCode, data) } - override fun onMenuItemClick(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_delete -> { - val activity = activity!! - AlertDialog.Builder(activity) - .setTitle(R.string.delete_confirm_prompt) - .setPositiveButton(R.string.yes, { _, _ -> - ProfileManager.delProfile(profile.id) - activity.finish() - }) - .setNegativeButton(R.string.no, null) - .create() - .show() - true - } - R.id.action_apply -> { - saveAndExit() - true - } - else -> false + override fun onMenuItemClick(item: MenuItem) = when (item.itemId) { + R.id.action_delete -> { + val activity = activity!! + AlertDialog.Builder(activity) + .setTitle(R.string.delete_confirm_prompt) + .setPositiveButton(R.string.yes, { _, _ -> + ProfileManager.delProfile(profile.id) + activity.finish() + }) + .setNegativeButton(R.string.no, null) + .create() + .show() + true } + R.id.action_apply -> { + saveAndExit() + true + } + else -> false } override fun onDestroy() { diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt index dddf5383..7a62830c 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt @@ -37,7 +37,6 @@ import android.view.* import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView -import android.widget.Toast import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.TrafficMonitor @@ -374,9 +373,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { Snackbar.LENGTH_LONG).show() return true } - } catch (exc: Exception) { - app.track(exc) - } + } catch (_: IndexOutOfBoundsException) { } Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG) .show() true diff --git a/mobile/src/main/java/com/github/shadowsocks/ScannerActivity.kt b/mobile/src/main/java/com/github/shadowsocks/ScannerActivity.kt index d94ca422..11a96842 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ScannerActivity.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ScannerActivity.kt @@ -139,31 +139,29 @@ class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler, Too .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true), if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH) override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { - REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> { - if (resultCode == Activity.RESULT_OK) { - var list = listOfNotNull(data?.data) - val clipData = data?.clipData - if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri } - val resolver = contentResolver - val reader = MultiFormatReader() - var success = false - for (uri in list) try { - val bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri)) - val pixels = IntArray(bitmap.width * bitmap.height) - bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) - Profile.findAll(reader.decode(BinaryBitmap(HybridBinarizer( - RGBLuminanceSource(bitmap.width, bitmap.height, pixels)))).text).forEach { - ProfileManager.createProfile(it) - success = true - } - } catch (e: Exception) { - app.track(e) + REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) { + var list = listOfNotNull(data?.data) + val clipData = data?.clipData + if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri } + val resolver = contentResolver + val reader = MultiFormatReader() + var success = false + for (uri in list) try { + val bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri)) + val pixels = IntArray(bitmap.width * bitmap.height) + bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) + Profile.findAll(reader.decode(BinaryBitmap(HybridBinarizer( + RGBLuminanceSource(bitmap.width, bitmap.height, pixels)))).text).forEach { + ProfileManager.createProfile(it) + success = true } - Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err, - Toast.LENGTH_SHORT).show() - finish() - } else if (requestCode == REQUEST_IMPORT_OR_FINISH) finish() - } + } catch (e: Exception) { + app.track(e) + } + Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err, + Toast.LENGTH_SHORT).show() + finish() + } else if (requestCode == REQUEST_IMPORT_OR_FINISH) finish() else -> super.onActivityResult(requestCode, resultCode, data) } } diff --git a/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt b/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt index 631c9ebe..0c5065f5 100644 --- a/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt @@ -124,7 +124,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } override fun afterTextChanged(s: Editable) = validate(value = s) - override fun onNothingSelected(parent: AdapterView<*>?) = throw IllegalStateException() + override fun onNothingSelected(parent: AdapterView<*>?) = check(false) override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = validate(position) private fun validate(template: Int = templateSelector.selectedItemPosition, value: Editable = editText.text) { @@ -228,16 +228,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, override fun getItemCount(): Int = acl.subnets.size() + acl.hostnames.size() + acl.urls.size() override fun getSectionTitle(i: Int): String { val j = i - acl.subnets.size() - return try { - (if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else { - val k = j - acl.hostnames.size() - if (k < 0) { - val hostname = acl.hostnames[j] - // don't convert IDN yet - PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname - } else acl.urls[k].host - }).substring(0, 1) - } catch (_: IndexOutOfBoundsException) { " " } + return (if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else { + val k = j - acl.hostnames.size() + if (k < 0) { + val hostname = acl.hostnames[j] + // don't convert IDN yet + PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname + } else acl.urls[k].host + }).firstOrNull()?.toString() ?: " " } private fun apply() { diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt index d9416891..6a703f3c 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt @@ -31,7 +31,6 @@ import android.os.RemoteCallbackList import android.support.v4.os.UserManagerCompat import android.util.Base64 import android.util.Log -import android.widget.Toast import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.R import com.github.shadowsocks.acl.Acl @@ -146,18 +145,18 @@ object BaseService { } internal fun updateTrafficTotal(tx: Long, rx: Long) { - val profile = profile ?: return - val p = ProfileManager.getProfile(profile.id) ?: return // profile may have host, etc. modified - p.tx += tx - p.rx += rx - ProfileManager.updateProfile(p) + // this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition) + val profile = ProfileManager.getProfile((profile ?: return).id) ?: return + profile.tx += tx + profile.rx += rx + ProfileManager.updateProfile(profile) app.handler.post { if (bandwidthListeners.isNotEmpty()) { val n = callbacks.beginBroadcast() for (i in 0 until n) { try { val item = callbacks.getBroadcastItem(i) - if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(p.id) + if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(profile.id) } catch (_: Exception) { } // ignore } callbacks.finishBroadcast() @@ -182,9 +181,9 @@ object BaseService { .put("plugin_opts", plugin.toString()) } // sensitive Shadowsocks config is stored in - val file = File((if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) { + val file = File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) { app.deviceContext.noBackupFilesDir // only API 24+ will be in locked state - }), CONFIG_FILE) + }, CONFIG_FILE) shadowsocksConfigFile = file file.writeText(config.toString()) return file @@ -218,9 +217,9 @@ object BaseService { false } else true fun forceLoad() { - val p = app.currentProfile + val profile = app.currentProfile ?: return stopRunner(true, (this as Context).getString(R.string.profile_empty)) - if (!checkProfile(p)) return + if (!checkProfile(profile)) return val s = data.state when (s) { STOPPED -> startRunner() diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/Dns.kt b/mobile/src/main/java/com/github/shadowsocks/bg/Dns.kt index 56d6840d..9bb52093 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/Dns.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/Dns.kt @@ -24,22 +24,19 @@ import android.util.Log import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.BuildConfig import org.xbill.DNS.* -import java.net.Inet6Address -import java.net.InetAddress -import java.net.NetworkInterface -import java.net.UnknownHostException -import okhttp3.Dns as Okdns +import java.io.IOException +import java.net.* object Dns { private const val TAG = "Dns" private val hasIPv6Support get() = try { val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() } - .count { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } > 0 + .any { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected") result - } catch (ex: Exception) { - Log.e(TAG, "Failed to get interfaces' addresses.") + } catch (ex: SocketException) { + Log.e(TAG, "Failed to get interfaces' addresses.", ex) app.track(ex) false } @@ -60,7 +57,7 @@ object Dns { Type.AAAA -> return (r as AAAARecord).address.hostAddress } } - } catch (_: Exception) { } + } catch (_: IOException) { } return null } private fun resolve(host: String): String? = try { diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt index 1d1d9644..ed5925b2 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt @@ -20,7 +20,6 @@ package com.github.shadowsocks.bg -import android.content.Context import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.preference.DataStore @@ -50,10 +49,10 @@ object LocalDnsService { fun makeDns(name: String, address: String, timeout: Int, edns: Boolean = true): JSONObject { val dns = JSONObject() .put("Name", name) - .put("Address", (when (address.parseNumericAddress()) { + .put("Address", when (address.parseNumericAddress()) { is Inet6Address -> "[$address]" else -> address - })) + }) .put("Timeout", timeout) .put("EDNSClientSubnet", JSONObject().put("Policy", "disable")) if (edns) dns diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/ProxyService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/ProxyService.kt index 6f6d0cf1..e1d66872 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/ProxyService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/ProxyService.kt @@ -23,7 +23,6 @@ package com.github.shadowsocks.bg import android.app.Service import android.content.Intent import android.os.IBinder -import com.github.shadowsocks.database.Profile /** * Shadowsocks service at its minimum. diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitor.kt b/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitor.kt index e04dd3e8..8f99a29b 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitor.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitor.kt @@ -44,7 +44,7 @@ object TrafficMonitor { private val units = arrayOf("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB") private val numberFormat = DecimalFormat("@@@") fun formatTraffic(size: Long): String { - var n: Double = size.toDouble() + var n = size.toDouble() var i = -1 while (n >= 999.5) { n /= 1024 diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt b/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt index 70d1b14d..5d738208 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt @@ -38,7 +38,7 @@ class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") { val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN) TrafficMonitor.update(stat.getLong(0), stat.getLong(8)) socket.outputStream.write(0) - } catch (e: Exception) { + } catch (e: IOException) { Log.e(tag, "Error when recv traffic stat", e) app.track(e) } diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt index c1f9470a..cfba6660 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt @@ -38,6 +38,7 @@ import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.parseNumericAddress import java.io.File import java.io.FileDescriptor +import java.io.IOException import java.lang.reflect.Method import java.util.* import android.net.VpnService as BaseVpnService @@ -55,18 +56,24 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { override val socketFile: File = File(app.deviceContext.filesDir, "protect_path") override fun accept(socket: LocalSocket) { + var success = false try { socket.inputStream.read() val fds = socket.ancillaryFileDescriptors if (fds.isEmpty()) return val fd = getInt.invoke(fds.first()) as Int - val ret = protect(fd) + success = protect(fd) JniHelper.close(fd) // Trick to close file decriptor - socket.outputStream.write(if (ret) 0 else 1) } catch (e: Exception) { Log.e(tag, "Error when protect socket", e) app.track(e) } + try { + socket.outputStream.write(if (success) 0 else 1) + } catch (e: IOException) { + Log.e(tag, "Error when returning result in protect", e) + app.track(e) + } } } class NullConnectionException : NullPointerException() @@ -118,7 +125,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { super.startNativeProcesses() val fd = startVpn() - if (!sendFd(fd)) throw Exception("sendFd failed") + if (!sendFd(fd)) throw IOException("sendFd failed") } override fun buildAdditionalArguments(cmd: ArrayList): ArrayList { diff --git a/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt index 61f94686..52215aa4 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt @@ -72,10 +72,10 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;") TableUtils.createTable(connectionSource, Profile::class.java) profileDao.executeRawNoArgs( - "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," + - " udpdns, ipv6, individual) " + - "SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, ipv6," + - " individual FROM `tmp`;") + "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route," + + " proxyApps, bypass, udpdns, ipv6, individual) " + + "SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global," + + " bypass, udpdns, ipv6, individual FROM `tmp`;") profileDao.executeRawNoArgs("DROP TABLE `tmp`;") profileDao.executeRawNoArgs("COMMIT;") } else if (oldVersion < 13) { diff --git a/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt b/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt index a756dedd..8e8e2ea1 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt @@ -36,6 +36,7 @@ import java.util.* class Profile : Serializable { companion object { private const val TAG = "ShadowParser" + private const val serialVersionUID = 0L private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex() private val userInfoPattern = "^(.+?):(.*)$".toRegex() private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex() diff --git a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt index 9ab1c473..15ef7c82 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt @@ -25,79 +25,69 @@ import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.ProfilesFragment import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.utils.DirectBoot +import java.sql.SQLException +/** + * SQLExceptions are not caught (and therefore will cause crash) for insert/update transactions + * to ensure we are in a consistent state. + */ object ProfileManager { private const val TAG = "ProfileManager" + @Throws(SQLException::class) fun createProfile(p: Profile? = null): Profile { val profile = p ?: Profile() profile.id = 0 - try { - val oldProfile = app.currentProfile - if (oldProfile != null) { - // Copy Feature Settings from old profile - profile.route = oldProfile.route - profile.ipv6 = oldProfile.ipv6 - profile.proxyApps = oldProfile.proxyApps - profile.bypass = oldProfile.bypass - profile.individual = oldProfile.individual - profile.udpdns = oldProfile.udpdns - } - val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder() - .selectRaw("MAX(userOrder)").prepareStatementString()).firstResult - if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1 - PrivateDatabase.profileDao.createOrUpdate(profile) - ProfilesFragment.instance?.profilesAdapter?.add(profile) - } catch (ex: Exception) { - Log.e(TAG, "addProfile", ex) - app.track(ex) + val oldProfile = app.currentProfile + if (oldProfile != null) { + // Copy Feature Settings from old profile + profile.route = oldProfile.route + profile.ipv6 = oldProfile.ipv6 + profile.proxyApps = oldProfile.proxyApps + profile.bypass = oldProfile.bypass + profile.individual = oldProfile.individual + profile.udpdns = oldProfile.udpdns } + val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder() + .selectRaw("MAX(userOrder)").prepareStatementString()).firstResult + if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1 + PrivateDatabase.profileDao.createOrUpdate(profile) + ProfilesFragment.instance?.profilesAdapter?.add(profile) return profile } /** * Note: It's caller's responsibility to update DirectBoot profile if necessary. */ - fun updateProfile(profile: Profile): Boolean = try { - PrivateDatabase.profileDao.update(profile) - true - } catch (ex: Exception) { - Log.e(TAG, "updateProfile", ex) - app.track(ex) - false - } + @Throws(SQLException::class) + fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile) fun getProfile(id: Int): Profile? = try { PrivateDatabase.profileDao.queryForId(id) - } catch (ex: Exception) { + } catch (ex: SQLException) { Log.e(TAG, "getProfile", ex) app.track(ex) null } - fun delProfile(id: Int): Boolean = try { + @Throws(SQLException::class) + fun delProfile(id: Int) { PrivateDatabase.profileDao.deleteById(id) ProfilesFragment.instance?.profilesAdapter?.removeId(id) if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean() - true - } catch (ex: Exception) { - Log.e(TAG, "delProfile", ex) - app.track(ex) - false } fun getFirstProfile(): Profile? = try { - val result = PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()) - if (result.size == 1) result[0] else null - } catch (ex: Exception) { - Log.e(TAG, "getAllProfiles", ex) + PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull() + } catch (ex: SQLException) { + Log.e(TAG, "getFirstProfile", ex) app.track(ex) null } fun getAllProfiles(): List? = try { PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare()) - } catch (ex: Exception) { + } catch (ex: SQLException) { Log.e(TAG, "getAllProfiles", ex) app.track(ex) null diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt index 87651719..7a9146b7 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt @@ -52,7 +52,7 @@ object DataStore { /** * Setter is defined in MainActivity.onPreferenceDataStoreChanged. */ - val directBootAware: Boolean get() = BootReceiver.enabled && (publicStore.getBoolean(Key.directBootAware) ?: false) + val directBootAware: Boolean get() = BootReceiver.enabled && publicStore.getBoolean(Key.directBootAware) == true var serviceMode: String get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn set(value) = publicStore.putString(Key.serviceMode, value) diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt b/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt index f1df51f2..7122a2e3 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt @@ -28,11 +28,7 @@ import android.util.AttributeSet class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) { var entryIcons: Array? = null val selectedEntry: Int get() = entryValues.indexOf(value) - val entryIcon: Drawable? get() = try { - entryIcons?.get(selectedEntry) - } catch (_: ArrayIndexOutOfBoundsException) { - null - } + val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry) // fun setEntryIcons(@ArrayRes entryIconsResId: Int) { // val array = getContext().getResources().obtainTypedArray(entryIconsResId) // entryIcons = Array(array.length(), { i -> array.getDrawable(i) }) @@ -49,7 +45,7 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr } init { - super.setOnPreferenceChangeListener({ preference, newValue -> + super.setOnPreferenceChangeListener { preference, newValue -> val listener = listener if (listener == null || listener.onPreferenceChange(preference, newValue)) { value = newValue.toString() @@ -57,7 +53,7 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr if (entryIcons != null) icon = entryIcon true } else false - }) + } // val a = context.obtainStyledAttributes(attrs, R.styleable.IconListPreference) // val entryIconsResId: Int = a.getResourceId(R.styleable.IconListPreference_entryIcons, -1) // if (entryIconsResId != -1) entryIcons = entryIconsResId diff --git a/mobile/src/main/java/com/github/shadowsocks/tasker/ConfigActivity.kt b/mobile/src/main/java/com/github/shadowsocks/tasker/ConfigActivity.kt index 602f75d7..fc9eeecf 100644 --- a/mobile/src/main/java/com/github/shadowsocks/tasker/ConfigActivity.kt +++ b/mobile/src/main/java/com/github/shadowsocks/tasker/ConfigActivity.kt @@ -85,12 +85,12 @@ class ConfigActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - try { - taskerOption = Settings.fromIntent(intent!!) - } catch (_: Exception) { + val intent = intent + if (intent == null) { finish() return } + taskerOption = Settings.fromIntent(intent) setContentView(R.layout.layout_tasker) val toolbar = findViewById(R.id.toolbar) diff --git a/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt b/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt index b231c0cc..ed4e6f9c 100644 --- a/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt +++ b/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt @@ -44,8 +44,8 @@ class Settings(bundle: Bundle?) { 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) + 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.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.kt index 35096d5e..28569627 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.kt @@ -20,9 +20,7 @@ package com.github.shadowsocks.utils -import java.util.ArrayList -import java.util.Arrays -import java.util.StringTokenizer +import java.util.* /** * Commandline objects help handling command lines specifying processes to @@ -86,9 +84,7 @@ object Commandline { * @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 - } + fun toString(args: Array) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable /** * Crack a command line. diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt b/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt index 9bf345fd..228e0c97 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt @@ -7,7 +7,7 @@ import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.preference.DataStore import java.io.File -import java.io.FileNotFoundException +import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -17,7 +17,7 @@ object DirectBoot { fun getDeviceProfile(): Profile? = try { ObjectInputStream(file.inputStream()).use { it.readObject() as Profile } - } catch (_: FileNotFoundException) { null } + } catch (_: IOException) { null } fun clean() { file.delete() diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt index e328fcff..f8d3e66f 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt @@ -28,8 +28,8 @@ fun String.parseNumericAddress(): InetAddress? { } fun parsePort(str: String?, default: Int, min: Int = 1025): Int { - val x = str?.toIntOrNull() ?: default - return if (x < min || x > 65535) default else x + val value = str?.toIntOrNull() ?: default + return if (value < min || value > 65535) default else value } fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() { @@ -72,6 +72,6 @@ private class SortedListIterable(private val list: SortedList) : Itera private class SortedListIterator(private val list: SortedList) : Iterator { private var count = 0 override fun hasNext() = count < list.size() - override fun next(): T = list[count++] + override fun next(): T = if (hasNext()) list[count++] else throw NoSuchElementException() } fun SortedList.asIterable(): Iterable = SortedListIterable(this) diff --git a/mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt b/mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt index 670f68de..963cfaa0 100644 --- a/mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt +++ b/mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt @@ -29,7 +29,6 @@ import android.support.graphics.drawable.AnimatedVectorDrawableCompat import android.support.v7.widget.TooltipCompat import android.util.AttributeSet import android.view.View -import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.R import com.github.shadowsocks.bg.BaseService import java.util.* diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt b/plugin/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt index 104484d7..7aaef405 100644 --- a/plugin/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt +++ b/plugin/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt @@ -70,8 +70,8 @@ abstract class NativePluginProvider : ContentProvider() { /** * Returns executable entry absolute path. This is used if plugin is sharing UID with the host. * - * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the default - * behavior. + * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the + * default behavior. * * @return Absolute path for executable entry. */ diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt b/plugin/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt index 48809668..5497e725 100644 --- a/plugin/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt +++ b/plugin/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt @@ -29,13 +29,11 @@ import android.widget.Toast * Activity that's capable of getting EXTRA_OPTIONS input. */ abstract class OptionsCapableActivity : AppCompatActivity() { - protected fun pluginOptions(intent: Intent = this.intent): PluginOptions { - return try { - PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS)) - } catch (exc: IllegalArgumentException) { - Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show() - PluginOptions() - } + protected fun pluginOptions(intent: Intent = this.intent) = try { + PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS)) + } catch (exc: IllegalArgumentException) { + Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show() + PluginOptions() } /**