Unverified Commit 343ecac8 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge branch 'master' into fix-bab

parents 0cee695f e2662ce9
...@@ -6,17 +6,16 @@ jobs: ...@@ -6,17 +6,16 @@ jobs:
- image: shadowsocks/shadowsocks-android:circleci - image: shadowsocks/shadowsocks-android:circleci
environment: environment:
JVM_OPTS: -Xmx3.5g JVM_OPTS: -Xmx3.5g
GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process"
steps: steps:
- checkout - checkout
- run: git submodule update --init --recursive - run: git submodule update --init --recursive
- run: pushd core/src/overture; ./make.bash 21; popd
- restore_cache: - restore_cache:
key: jars-{{ checksum "build.gradle" }} key: jars-{{ checksum "build.gradle" }}
- run: - run:
name: Run Build name: Run Build and Tests
command: ./gradlew assembleDebug command: ./gradlew assembleDebug check
- run:
name: Run Tests
command: ./gradlew check
- save_cache: - save_cache:
paths: paths:
- ~/.gradle - ~/.gradle
......
sudo: required
addons:
artifacts: true
services:
- docker
before_install:
- docker pull shadowsocks/shadowsocks-android:circleci
- chmod -R a+wrx .
script:
- docker run --rm -v ${PWD}:/build shadowsocks/shadowsocks-android:circleci bash -c "cd /build; ./gradlew assembleDebug check"
## Shadowsocks for Android ## Shadowsocks for Android
[![CircleCI](https://circleci.com/gh/shadowsocks/shadowsocks-android.svg?style=svg)](https://circleci.com/gh/shadowsocks/shadowsocks-android) [![Build Status](https://travis-ci.org/shadowsocks/shadowsocks-android.svg?branch=master)](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) [![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) [![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) [![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)
...@@ -28,6 +28,14 @@ but probably painful. Further contributions regarding building on Windows are al ...@@ -28,6 +28,14 @@ but probably painful. Further contributions regarding building on Windows are al
* Clone the repo using `git clone --recurse-submodules <repo>` or update submodules using `git submodule update --init --recursive` * Clone the repo using `git clone --recurse-submodules <repo>` or update submodules using `git submodule update --init --recursive`
* Build it using Android Studio or gradle script * Build it using Android Studio or gradle script
### BUILD WITH DOCKER
```bash
mkdir build
sudo chown 3434:3434 build
docker run --rm -v ${PWD}/build:/build shadowsocks/shadowsocks-android:circleci bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug"
```
### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30) ### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30)
## OPEN SOURCE LICENSES ## OPEN SOURCE LICENSES
......
...@@ -4,14 +4,13 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,14 +4,13 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
kotlinVersion = '1.3.0' kotlinVersion = '1.3.10'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
buildToolsVersion = '28.0.3' buildToolsVersion = '28.0.3'
androidxVersion = '1.0.0'
roomVersion = '2.0.0' roomVersion = '2.0.0'
workVersion = '1.0.0-alpha10' workVersion = '1.0.0-alpha11'
preferencexVersion = '1.0.0' preferencexVersion = '1.0.0'
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.0' androidTestVersion = '1.1.0'
......
...@@ -71,14 +71,14 @@ tasks.whenTaskAdded { task -> ...@@ -71,14 +71,14 @@ tasks.whenTaskAdded { task ->
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion" api "android.arch.work:work-runtime-ktx:$workVersion"
api "androidx.preference:preference:$androidxVersion" api "androidx.preference:preference:1.0.0"
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.5' api 'com.crashlytics.sdk.android:crashlytics:2.9.6'
api 'com.google.firebase:firebase-config:16.0.0' api 'com.google.firebase:firebase-config:16.0.0'
api 'com.google.firebase:firebase-core:16.0.0' api 'com.google.firebase:firebase-core:16.0.0'
api 'com.squareup.okhttp3:okhttp:3.11.0' api 'com.squareup.okhttp3:okhttp:3.12.0'
api "com.takisoft.preferencex:preferencex:$preferencexVersion" api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'eu.chainfire:libsuperuser:1.0.0.201704021214' api 'eu.chainfire:libsuperuser:1.0.0.201811281328'
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.room:room-testing:$roomVersion" testImplementation "androidx.room:room-testing:$roomVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
......
...@@ -116,7 +116,7 @@ object Core { ...@@ -116,7 +116,7 @@ object Core {
for (dir in arrayOf("acl", "overture")) for (dir in arrayOf("acl", "overture"))
try { try {
for (file in assetManager.list(dir)!!) assetManager.open("$dir/$file").use { input -> for (file in assetManager.list(dir)!!) assetManager.open("$dir/$file").use { input ->
File(deviceStorage.filesDir, file).outputStream().use { output -> input.copyTo(output) } File(deviceStorage.noBackupFilesDir, file).outputStream().use { output -> input.copyTo(output) }
} }
} catch (e: IOException) { } catch (e: IOException) {
printLog(e) printLog(e)
......
...@@ -38,39 +38,17 @@ ...@@ -38,39 +38,17 @@
package com.github.shadowsocks; package com.github.shadowsocks;
import android.os.Build; import android.system.ErrnoException;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.system.ErrnoException;
public class JniHelper { public class JniHelper {
static { static {
System.loadLibrary("jni-helper"); System.loadLibrary("jni-helper");
} }
@Deprecated // Use Process.destroy() since API 24 public static native void sendFd(int fd, @NonNull String path) throws ErrnoException;
public static void sigtermCompat(@NonNull Process process) throws Exception {
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
int errno = sigterm(process);
if (errno != 0) throw new ErrnoException("kill", errno);
}
@Deprecated // only implemented for before API 24
public static boolean waitForCompat(@NonNull Process process, long millis) throws Exception {
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
final Object mutex = getExitValueMutex(process);
synchronized (mutex) {
if (getExitValue(process) == null) mutex.wait(millis);
return getExitValue(process) != null;
}
}
public static native int sigkill(int pid);
private static native int sigterm(Process process);
private static native Integer getExitValue(Process process);
private static native Object getExitValueMutex(Process process);
public static native int sendFd(int fd, @NonNull String path);
public static native void close(int fd);
@Nullable @Nullable
public static native byte[] parseNumericAddress(@NonNull String str); public static native byte[] parseNumericAddress(@NonNull String str);
} }
...@@ -46,7 +46,7 @@ class Acl { ...@@ -46,7 +46,7 @@ class Acl {
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex() val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.filesDir, "$id.acl") fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.noBackupFilesDir, "$id.acl")
var customRules: Acl var customRules: Acl
get() { get() {
...@@ -148,7 +148,7 @@ class Acl { ...@@ -148,7 +148,7 @@ class Acl {
} }
fun fromId(id: String): Acl = try { fun fromId(id: String): Acl = try {
fromReader(Acl.getFile(id).bufferedReader()) fromReader(getFile(id).bufferedReader())
} catch (_: IOException) { this } } catch (_: IOException) { this }
fun flatten(depth: Int): Acl { fun flatten(depth: Int): Acl {
......
...@@ -20,18 +20,14 @@ ...@@ -20,18 +20,14 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.annotation.TargetApi
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.*
import android.os.IBinder
import android.os.RemoteCallbackList
import android.os.SystemClock
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.os.UserManagerCompat import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
...@@ -195,9 +191,8 @@ object BaseService { ...@@ -195,9 +191,8 @@ object BaseService {
.put("plugin_opts", plugin.toString()) .put("plugin_opts", plugin.toString())
} }
// sensitive Shadowsocks config is stored in // sensitive Shadowsocks config is stored in
return File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) { return File((if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()?.isUserUnlocked != true)
Core.deviceStorage.noBackupFilesDir // only API 24+ will be in locked state app else Core.deviceStorage).noBackupFilesDir, CONFIG_FILE).apply {
}, CONFIG_FILE).apply {
shadowsocksConfigFile = this shadowsocksConfigFile = this
writeText(config.toString()) writeText(config.toString())
} }
......
...@@ -20,11 +20,12 @@ ...@@ -20,11 +20,12 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.system.ErrnoException
import android.system.Os
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.JniHelper
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
...@@ -44,11 +45,13 @@ object Executable { ...@@ -44,11 +45,13 @@ object Executable {
} catch (ignore: FileNotFoundException) { } catch (ignore: FileNotFoundException) {
continue continue
}.split(Character.MIN_VALUE, limit = 2).first()) }.split(Character.MIN_VALUE, limit = 2).first())
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) { if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) try {
val errno = JniHelper.sigkill(process.name.toInt()) Os.kill(process.name.toInt(), 9) // SIGKILL
if (errno != 0) { } catch (e: ErrnoException) {
Crashlytics.log(Log.WARN, "kill", if (e.errno != 3) { // ESRCH
"SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno") e.printStackTrace()
Crashlytics.log(Log.WARN, "kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed")
Crashlytics.logException(e)
} }
} }
} }
......
...@@ -22,10 +22,11 @@ package com.github.shadowsocks.bg ...@@ -22,10 +22,11 @@ package com.github.shadowsocks.bg
import android.os.Build import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import android.system.ErrnoException
import android.system.Os
import android.util.Log import android.util.Log
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread import com.github.shadowsocks.utils.thread
import java.io.File import java.io.File
...@@ -37,6 +38,9 @@ import java.util.concurrent.atomic.AtomicReference ...@@ -37,6 +38,9 @@ import java.util.concurrent.atomic.AtomicReference
class GuardedProcessPool { class GuardedProcessPool {
companion object Dummy : IOException("Oopsie the developer has made a no-no") { companion object Dummy : IOException("Oopsie the developer has made a no-no") {
private const val TAG = "GuardedProcessPool" private const val TAG = "GuardedProcessPool"
private val ProcessImpl by lazy { Class.forName("java/lang/ProcessManager\$ProcessImpl") }
private val pid by lazy { ProcessImpl.getField("pid").apply { isAccessible = true } }
private val exitValueMutex by lazy { ProcessImpl.getField("exitValueMutex").apply { isAccessible = true } }
} }
private inner class Guard(private val cmd: List<String>, private val onRestartCallback: (() -> Unit)?) { private inner class Guard(private val cmd: List<String>, private val onRestartCallback: (() -> Unit)?) {
...@@ -66,7 +70,7 @@ class GuardedProcessPool { ...@@ -66,7 +70,7 @@ class GuardedProcessPool {
process = ProcessBuilder(cmd) process = ProcessBuilder(cmd)
.redirectErrorStream(true) .redirectErrorStream(true)
.directory(Core.deviceStorage.filesDir) .directory(Core.deviceStorage.noBackupFilesDir)
.start() .start()
streamLogger(process.inputStream, Log::i) streamLogger(process.inputStream, Log::i)
...@@ -88,9 +92,21 @@ class GuardedProcessPool { ...@@ -88,9 +92,21 @@ class GuardedProcessPool {
pushException(e) pushException(e)
} finally { } finally {
if (process != null) { if (process != null) {
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") { if (Build.VERSION.SDK_INT < 24) {
JniHelper.sigtermCompat(process) val pid = pid.get(process) as Int
JniHelper.waitForCompat(process, 500) try {
Os.kill(pid, 15) // SIGTERM
} catch (e: ErrnoException) {
if (e.errno != 3) throw e // ESRCH
}
val mutex = exitValueMutex.get(process) as Object
synchronized(mutex) {
try {
process.exitValue()
} catch (e: IllegalThreadStateException) {
mutex.wait(500)
}
}
} }
process.destroy() process.destroy()
process.waitFor() // ensure the process is destroyed process.waitFor() // ensure the process is destroyed
......
...@@ -52,7 +52,7 @@ object LocalDnsService { ...@@ -52,7 +52,7 @@ object LocalDnsService {
} }
fun buildOvertureConfig(file: String) = file.also { fun buildOvertureConfig(file: String) = file.also {
File(Core.deviceStorage.filesDir, it).writeText(JSONObject().run { File(Core.deviceStorage.noBackupFilesDir, it).writeText(JSONObject().run {
put("BindAddress", "${DataStore.listenAddress}:${DataStore.portLocalDns}") put("BindAddress", "${DataStore.listenAddress}:${DataStore.portLocalDns}")
put("RedirectIPv6Record", true) put("RedirectIPv6Record", true)
put("DomainBase64Decode", false) put("DomainBase64Decode", false)
......
...@@ -29,7 +29,7 @@ import java.nio.ByteBuffer ...@@ -29,7 +29,7 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") { class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
override val socketFile = File(Core.deviceStorage.filesDir, "stat_path") override val socketFile = File(Core.deviceStorage.noBackupFilesDir, "stat_path")
override fun accept(socket: LocalSocket) { override fun accept(socket: LocalSocket) {
try { try {
......
...@@ -52,7 +52,7 @@ class TransproxyService : Service(), LocalDnsService.Interface { ...@@ -52,7 +52,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
} }
private fun startRedsocksDaemon() { private fun startRedsocksDaemon() {
File(Core.deviceStorage.filesDir, "redsocks.conf").writeText("""base { File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
log_debug = off; log_debug = off;
log_info = off; log_info = off;
log = stderr; log = stderr;
......
...@@ -27,6 +27,8 @@ import android.content.pm.PackageManager ...@@ -27,6 +27,8 @@ import android.content.pm.PackageManager
import android.net.* import android.net.*
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.system.ErrnoException
import android.system.Os
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
...@@ -72,21 +74,24 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -72,21 +74,24 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
} }
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") { private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
override val socketFile: File = File(Core.deviceStorage.filesDir, "protect_path") override val socketFile: File = File(Core.deviceStorage.noBackupFilesDir, "protect_path")
override fun accept(socket: LocalSocket) { override fun accept(socket: LocalSocket) {
try { try {
socket.inputStream.read() socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!! val fd = socket.ancillaryFileDescriptors!!.single()!!
val fdInt = getInt.invoke(fd) as Int
socket.outputStream.write(if (try { socket.outputStream.write(if (try {
val network = underlyingNetwork val network = underlyingNetwork
if (network != null && Build.VERSION.SDK_INT >= 23) { if (network != null && Build.VERSION.SDK_INT >= 23) {
network.bindSocket(fd) network.bindSocket(fd)
true true
} else protect(fdInt) } else protect(getInt.invoke(fd) as Int)
} finally { } finally {
JniHelper.close(fdInt) // Trick to close file decriptor try {
Os.close(fd)
} catch (e: ErrnoException) {
printLog(e)
}
}) 0 else 1) }) 0 else 1)
} catch (e: IOException) { } catch (e: IOException) {
printLog(e) printLog(e)
...@@ -164,8 +169,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -164,8 +169,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
super.startNativeProcesses() super.startNativeProcesses()
val fd = startVpn() sendFd(startVpn())
if (!sendFd(fd)) throw IOException("sendFd failed")
} }
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> { override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
...@@ -242,19 +246,27 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -242,19 +246,27 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
cmd += "--dnsgw" cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}" cmd += "127.0.0.1:${DataStore.portLocalDns}"
} }
data.processes.start(cmd) { sendFd(fd) } data.processes.start(cmd) {
try {
sendFd(fd)
} catch (e: ErrnoException) {
stopRunner(true, e.message)
}
}
return fd return fd
} }
private fun sendFd(fd: Int): Boolean { private fun sendFd(fd: Int) {
if (fd != -1) { if (fd == -1) throw IOException("Invalid fd (-1)")
var tries = 0 var tries = 0
while (tries < 10) { val path = File(Core.deviceStorage.noBackupFilesDir, "sock_path").absolutePath
Thread.sleep(30L shl tries) while (true) try {
if (JniHelper.sendFd(fd, File(Core.deviceStorage.filesDir, "sock_path").absolutePath) != -1) return true Thread.sleep(30L shl tries)
tries += 1 JniHelper.sendFd(fd, path)
} return
} catch (e: ErrnoException) {
if (tries >= 10) throw e
tries += 1
} }
return false
} }
} }
...@@ -44,7 +44,7 @@ class Profile : Serializable { ...@@ -44,7 +44,7 @@ class Profile : Serializable {
companion object { companion object {
private const val TAG = "ShadowParser" private const val TAG = "ShadowParser"
private const val serialVersionUID = 0L private const val serialVersionUID = 0L
private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex() private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
private val userInfoPattern = "^(.+?):(.*)$".toRegex() private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex() private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
......
...@@ -163,7 +163,7 @@ object PluginManager { ...@@ -163,7 +163,7 @@ object PluginManager {
var initialized = false var initialized = false
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found") fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
val list = ArrayList<String>() val list = ArrayList<String>()
val pluginDir = File(Core.deviceStorage.filesDir, "plugin") val pluginDir = File(Core.deviceStorage.noBackupFilesDir, "plugin")
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) (cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
?: return null).use { cursor -> ?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound() if (!cursor.moveToFirst()) entryNotFound()
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
package com.github.shadowsocks.preference package com.github.shadowsocks.preference
import android.os.Binder import android.os.Binder
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.database.PrivateDatabase import com.github.shadowsocks.database.PrivateDatabase
......
#define LOG_TAG "JniHelper"
#include "jni.h" #include "jni.h"
#include <android/log.h>
#include <sys/system_properties.h>
#include <algorithm> #include <algorithm>
#include <cerrno> #include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <unistd.h> #include <unistd.h>
#include <sys/un.h> #include <sys/un.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <ancillary.h> #include <ancillary.h>
using namespace std; using namespace std;
#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0) // Based on: https://android.googlesource.com/platform/libcore/+/564c7e8/luni/src/main/native/libcore_io_Linux.cpp#256
#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0) static void throwException(JNIEnv* env, jclass exceptionClass, jmethodID ctor2, const char* functionName, int error) {
#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0) jstring detailMessage = env->NewStringUTF(functionName);
#define THROW(env, clazz, msg) do { env->ThrowNew(env->FindClass(clazz), msg); } while (0) if (detailMessage == NULL) {
// Not really much we can do here. We're probably dead in the water,
static int sdk_version; // but let's try to stumble on...
static jclass ProcessImpl; env->ExceptionClear();
static jfieldID ProcessImpl_pid, ProcessImpl_exitValue, ProcessImpl_exitValueMutex;
#pragma clang diagnostic ignored "-Wunused-parameter"
extern "C" {
JNIEXPORT jint JNICALL Java_com_github_shadowsocks_JniHelper_sigkill(JNIEnv *env, jobject thiz, jint pid) {
// Suppress "No such process" errors. We just want the process killed. It's fine if it's already killed.
return kill(pid, SIGKILL) == -1 && errno != ESRCH ? errno : 0;
}
JNIEXPORT jint JNICALL Java_com_github_shadowsocks_JniHelper_sigterm(JNIEnv *env, jobject thiz, jobject process) {
if (!env->IsInstanceOf(process, ProcessImpl)) {
THROW(env, "java/lang/ClassCastException",
"Unsupported process object. Only java.lang.ProcessManager$ProcessImpl is accepted.");
return -1;
} }
jint pid = env->GetIntField(process, ProcessImpl_pid); env->Throw(reinterpret_cast<jthrowable>(env->NewObject(exceptionClass, ctor2, detailMessage, error)));
// Suppress "No such process" errors. We just want the process killed. It's fine if it's already killed. env->DeleteLocalRef(detailMessage);
return kill(pid, SIGTERM) == -1 && errno != ESRCH ? errno : 0;
} }
JNIEXPORT jobject JNICALL static void throwErrnoException(JNIEnv* env, const char* functionName) {
Java_com_github_shadowsocks_JniHelper_getExitValue(JNIEnv *env, jobject thiz, jobject process) { int error = errno;
if (!env->IsInstanceOf(process, ProcessImpl)) { static jclass ErrnoException = env->FindClass("android/system/ErrnoException");
THROW(env, "java/lang/ClassCastException", static jmethodID ctor2 = env->GetMethodID(ErrnoException, "<init>", "(Ljava/lang/String;I)V");
"Unsupported process object. Only java.lang.ProcessManager$ProcessImpl is accepted."); throwException(env, ErrnoException, ctor2, functionName, error);
return NULL;
}
return env->GetObjectField(process, ProcessImpl_exitValue);
}
JNIEXPORT jobject JNICALL
Java_com_github_shadowsocks_JniHelper_getExitValueMutex(JNIEnv *env, jobject thiz, jobject process) {
if (!env->IsInstanceOf(process, ProcessImpl)) {
THROW(env, "java/lang/ClassCastException",
"Unsupported process object. Only java.lang.ProcessManager$ProcessImpl is accepted.");
return NULL;
}
return env->GetObjectField(process, ProcessImpl_exitValueMutex);
}
JNIEXPORT void JNICALL Java_com_github_shadowsocks_JniHelper_close(JNIEnv *env, jobject thiz, jint fd) {
close(fd);
} }
JNIEXPORT jint JNICALL #pragma clang diagnostic ignored "-Wunused-parameter"
extern "C" {
JNIEXPORT void JNICALL
Java_com_github_shadowsocks_JniHelper_sendFd(JNIEnv *env, jobject thiz, jint tun_fd, jstring path) { Java_com_github_shadowsocks_JniHelper_sendFd(JNIEnv *env, jobject thiz, jint tun_fd, jstring path) {
int fd; int fd;
struct sockaddr_un addr; struct sockaddr_un addr;
const char *sock_str = env->GetStringUTFChars(path, 0); const char *sock_str = env->GetStringUTFChars(path, 0);
if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
LOGE("socket() failed: %s (socket fd = %d)\n", strerror(errno), fd); throwErrnoException(env, "socket");
return (jint)-1; goto quit2;
} }
memset(&addr, 0, sizeof(addr)); memset(&addr, 0, sizeof(addr));
...@@ -88,20 +47,17 @@ JNIEXPORT jint JNICALL ...@@ -88,20 +47,17 @@ JNIEXPORT jint JNICALL
strncpy(addr.sun_path, sock_str, sizeof(addr.sun_path)-1); strncpy(addr.sun_path, sock_str, sizeof(addr.sun_path)-1);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
LOGE("connect() failed: %s (fd = %d)\n", strerror(errno), fd); throwErrnoException(env, "connect");
close(fd); goto quit;
return (jint)-1;
} }
if (ancil_send_fd(fd, tun_fd)) { if (ancil_send_fd(fd, tun_fd)) throwErrnoException(env, "ancil_send_fd");
LOGE("ancil_send_fd: %s", strerror(errno));
close(fd);
return (jint)-1;
}
quit:
close(fd); close(fd);
quit2:
env->ReleaseStringUTFChars(path, sock_str); env->ReleaseStringUTFChars(path, sock_str);
return 0; return;
} }
JNIEXPORT jbyteArray JNICALL JNIEXPORT jbyteArray JNICALL
...@@ -124,50 +80,7 @@ Java_com_github_shadowsocks_JniHelper_parseNumericAddress(JNIEnv *env, jobject t ...@@ -124,50 +80,7 @@ Java_com_github_shadowsocks_JniHelper_parseNumericAddress(JNIEnv *env, jobject t
/* /*
* This is called by the VM when the shared library is first loaded. * This is called by the VM when the shared library is first loaded.
*/ */
typedef union {
JNIEnv* env;
void* venv;
} UnionJNIEnvToVoid;
#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection" #pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
jint JNI_OnLoad(JavaVM* vm, void* reserved) { jint JNI_OnLoad(JavaVM* vm, void* reserved) {
UnionJNIEnvToVoid uenv; return JNI_VERSION_1_6;
uenv.venv = NULL;
jint result = -1;
JNIEnv* env = NULL;
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) {
THROW(env, "java/lang/RuntimeException", "GetEnv failed");
goto bail;
}
env = uenv.env;
char version[PROP_VALUE_MAX + 1];
__system_property_get("ro.build.version.sdk", version);
sdk_version = atoi(version);
#define FIND_CLASS(out, name) \
if (!(out = env->FindClass(name))) { \
THROW(env, "java/lang/RuntimeException", name " not found"); \
goto bail; \
} \
out = reinterpret_cast<jclass>(env->NewGlobalRef(reinterpret_cast<jobject>(out)))
#define GET_FIELD(out, clazz, name, sig) \
if (!(out = env->GetFieldID(clazz, name, sig))) { \
THROW(env, "java/lang/RuntimeException", "Field " #clazz "." name " with type " sig " not found"); \
goto bail; \
}
if (sdk_version < 24) {
FIND_CLASS(ProcessImpl, "java/lang/ProcessManager$ProcessImpl");
GET_FIELD(ProcessImpl_pid, ProcessImpl, "pid", "I")
GET_FIELD(ProcessImpl_exitValue, ProcessImpl, "exitValue", "Ljava/lang/Integer;")
GET_FIELD(ProcessImpl_exitValueMutex, ProcessImpl, "exitValueMutex", "Ljava/lang/Object;")
}
result = JNI_VERSION_1_6;
bail:
return result;
} }
Subproject commit a9b5a94e215c1beadfe11442994b550e1e81f8d6 Subproject commit ffc00f97fa7663a91011a10e46ae954cfda3837f
# https://github.com/arturbosch/detekt/blob/RC8/detekt-cli/src/main/resources/default-detekt-config.yml # https://github.com/arturbosch/detekt/blob/RC9.2/detekt-cli/src/main/resources/default-detekt-config.yml
autoCorrect: true autoCorrect: true
comments: comments:
...@@ -120,6 +120,7 @@ exceptions: ...@@ -120,6 +120,7 @@ exceptions:
- IndexOutOfBoundsException - IndexOutOfBoundsException
- RuntimeException - RuntimeException
- Throwable - Throwable
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
TooGenericExceptionThrown: TooGenericExceptionThrown:
active: true active: true
exceptionNames: exceptionNames:
...@@ -143,8 +144,7 @@ formatting: ...@@ -143,8 +144,7 @@ formatting:
active: true active: true
autoCorrect: true autoCorrect: true
ImportOrdering: ImportOrdering:
active: true active: false
autoCorrect: false
Indentation: Indentation:
active: true active: true
autoCorrect: true autoCorrect: true
...@@ -217,6 +217,11 @@ naming: ...@@ -217,6 +217,11 @@ naming:
ClassNaming: ClassNaming:
active: true active: true
classPattern: '[A-Z$][a-zA-Z0-9$]*' classPattern: '[A-Z$][a-zA-Z0-9$]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming: EnumNaming:
active: true active: true
enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
...@@ -233,14 +238,20 @@ naming: ...@@ -233,14 +238,20 @@ naming:
active: true active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^' excludeClassPattern: '$^'
ignoreOverridden: true
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
MatchingDeclarationName: MatchingDeclarationName:
active: true active: true
MemberNameEqualsClassName: MemberNameEqualsClassName:
active: false active: false
ObjectPropertyNaming: ObjectPropertyNaming:
active: true active: true
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
constantPattern: '[A-Za-z][_A-Za-z0-9]*' constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*'
PackageNaming: PackageNaming:
active: true active: true
packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$' packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$'
...@@ -259,6 +270,7 @@ naming: ...@@ -259,6 +270,7 @@ naming:
variablePattern: '[a-z][A-Za-z0-9]*' variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^' excludeClassPattern: '$^'
ignoreOverridden: true
performance: performance:
active: true active: true
...@@ -318,6 +330,8 @@ style: ...@@ -318,6 +330,8 @@ style:
ForbiddenImport: ForbiddenImport:
active: true active: true
imports: '' imports: ''
ForbiddenVoid:
active: true
FunctionOnlyReturningConstant: FunctionOnlyReturningConstant:
active: true active: true
ignoreOverridableFunction: true ignoreOverridableFunction: true
...@@ -372,8 +386,11 @@ style: ...@@ -372,8 +386,11 @@ style:
active: true active: true
UnnecessaryAbstractClass: UnnecessaryAbstractClass:
active: true active: true
excludeAnnotatedClasses: "dagger.Module"
UnnecessaryInheritance: UnnecessaryInheritance:
active: true active: true
UnnecessaryLet:
active: true
UnnecessaryParentheses: UnnecessaryParentheses:
active: true active: true
UntilInsteadOfRangeTo: UntilInsteadOfRangeTo:
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx2g
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
......
...@@ -55,7 +55,7 @@ android { ...@@ -55,7 +55,7 @@ android {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "androidx.browser:browser:$androidxVersion" implementation "androidx.browser:browser:1.0.0"
implementation 'com.google.android.gms:play-services-vision:15.0.2' implementation 'com.google.android.gms:play-services-vision:15.0.2'
implementation 'com.google.firebase:firebase-ads:15.0.1' implementation 'com.google.firebase:firebase-ads:15.0.1'
implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion" implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion"
......
...@@ -24,7 +24,6 @@ import android.app.Application ...@@ -24,7 +24,6 @@ import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.IconListPreference import com.github.shadowsocks.preference.IconListPreference
import com.takisoft.preferencex.PreferenceFragmentCompat import com.takisoft.preferencex.PreferenceFragmentCompat
......
...@@ -62,7 +62,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -62,7 +62,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
private const val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS" private const val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
// unescaped: (?<=^(\(\^\|\\\.\)|\^\(\.\*\\\.\)\?)).*(?=\$$) // unescaped: (?<=^(\(\^\|\\\.\)|\^\(\.\*\\\.\)\?)).*(?=\$$)
private val PATTERN_DOMAIN = "(?<=^(\\(\\^\\|\\\\\\.\\)|\\^\\(\\.\\*\\\\\\.\\)\\?)).*(?=\\\$\$)".toRegex() private val domainPattern = "(?<=^(\\(\\^\\|\\\\\\.\\)|\\^\\(\\.\\*\\\\\\.\\)\\?)).*(?=\\\$\$)".toRegex()
} }
private enum class Template { private enum class Template {
...@@ -86,7 +86,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -86,7 +86,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
inputLayout = view.findViewById(R.id.content_layout) inputLayout = view.findViewById(R.id.content_layout)
when (item) { when (item) {
is String -> { is String -> {
val match = PATTERN_DOMAIN.find(item) val match = domainPattern.find(item)
if (match != null) { if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal) templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."), editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
......
...@@ -4,20 +4,17 @@ ...@@ -4,20 +4,17 @@
<item android:id="@+id/action_apply_all" <item android:id="@+id/action_apply_all"
android:title="@string/action_apply_all" android:title="@string/action_apply_all"
android:alphabeticShortcut="a" android:alphabeticShortcut="a"
android:numericShortcut="1"
app:showAsAction="never"/> app:showAsAction="never"/>
<item <item
android:id="@+id/action_export_clipboard" android:id="@+id/action_export_clipboard"
android:alphabeticShortcut="c" android:alphabeticShortcut="c"
android:icon="?attr/actionModeCopyDrawable" android:icon="?attr/actionModeCopyDrawable"
android:numericShortcut="2"
android:title="@string/action_export" android:title="@string/action_export"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_import_clipboard" android:id="@+id/action_import_clipboard"
android:alphabeticShortcut="v" android:alphabeticShortcut="v"
android:icon="?attr/actionModePasteDrawable" android:icon="?attr/actionModePasteDrawable"
android:numericShortcut="3"
android:title="@string/action_import" android:title="@string/action_import"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
</menu> </menu>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
<item android:title="@string/action_add_rule" <item android:title="@string/action_add_rule"
android:icon="@drawable/ic_av_playlist_add" android:icon="@drawable/ic_av_playlist_add"
android:alphabeticShortcut="n" android:alphabeticShortcut="n"
android:numericShortcut="1"
app:showAsAction="always"> app:showAsAction="always">
<menu> <menu>
<item android:id="@+id/action_manual_settings" <item android:id="@+id/action_manual_settings"
......
...@@ -6,28 +6,24 @@ ...@@ -6,28 +6,24 @@
android:id="@+id/action_select_all" android:id="@+id/action_select_all"
android:alphabeticShortcut="a" android:alphabeticShortcut="a"
android:icon="?attr/actionModeSelectAllDrawable" android:icon="?attr/actionModeSelectAllDrawable"
android:numericShortcut="1"
android:title="@android:string/selectAll" android:title="@android:string/selectAll"
android:showAsAction="always"/> android:showAsAction="always"/>
<item <item
android:id="@+id/action_cut" android:id="@+id/action_cut"
android:alphabeticShortcut="x" android:alphabeticShortcut="x"
android:icon="?attr/actionModeCutDrawable" android:icon="?attr/actionModeCutDrawable"
android:numericShortcut="2"
android:title="@android:string/cut" android:title="@android:string/cut"
android:showAsAction="always"/> android:showAsAction="always"/>
<item <item
android:id="@+id/action_copy" android:id="@+id/action_copy"
android:alphabeticShortcut="c" android:alphabeticShortcut="c"
android:icon="?attr/actionModeCopyDrawable" android:icon="?attr/actionModeCopyDrawable"
android:numericShortcut="3"
android:title="@android:string/copy" android:title="@android:string/copy"
android:showAsAction="always"/> android:showAsAction="always"/>
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:alphabeticShortcut="d" android:alphabeticShortcut="d"
android:icon="@drawable/ic_action_delete" android:icon="@drawable/ic_action_delete"
android:numericShortcut="4"
android:title="@string/delete" android:title="@string/delete"
android:showAsAction="always"/> android:showAsAction="always"/>
</menu> </menu>
...@@ -5,12 +5,10 @@ ...@@ -5,12 +5,10 @@
android:title="@string/delete" android:title="@string/delete"
android:icon="@drawable/ic_action_delete" android:icon="@drawable/ic_action_delete"
android:alphabeticShortcut="d" android:alphabeticShortcut="d"
android:numericShortcut="1"
app:showAsAction="always"/> app:showAsAction="always"/>
<item android:id="@+id/action_apply" <item android:id="@+id/action_apply"
android:title="@string/apply" android:title="@string/apply"
android:icon="@drawable/ic_action_done" android:icon="@drawable/ic_action_done"
android:alphabeticShortcut="s" android:alphabeticShortcut="s"
android:numericShortcut="2"
app:showAsAction="always"/> app:showAsAction="always"/>
</menu> </menu>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
<item <item
android:alphabeticShortcut="n" android:alphabeticShortcut="n"
android:icon="@drawable/ic_action_note_add" android:icon="@drawable/ic_action_note_add"
android:numericShortcut="1"
android:title="@string/add_profile" android:title="@string/add_profile"
app:showAsAction="always"> app:showAsAction="always">
<menu> <menu>
...@@ -28,7 +27,6 @@ ...@@ -28,7 +27,6 @@
android:alphabeticShortcut="s" android:alphabeticShortcut="s"
android:icon="@drawable/ic_file_file_upload" android:icon="@drawable/ic_file_file_upload"
android:title="@string/action_export_more" android:title="@string/action_export_more"
android:numericShortcut="2"
app:showAsAction="always"> app:showAsAction="always">
<menu> <menu>
<item <item
......
...@@ -5,6 +5,5 @@ ...@@ -5,6 +5,5 @@
android:title="@string/action_import_file" android:title="@string/action_import_file"
android:icon="@drawable/ic_image_photo" android:icon="@drawable/ic_image_photo"
android:alphabeticShortcut="o" android:alphabeticShortcut="o"
android:numericShortcut="1"
app:showAsAction="always"/> app:showAsAction="always"/>
</menu> </menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>
## Privacy Policy
Shadowsocks built the Shadowsocks-android app as an Open Source app. This SERVICE is provided by Shadowsocks at no cost and is intended for use as is.
This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.
If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.
The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Shadowsocks-android unless otherwise defined in this Privacy Policy.
**Information Collection and Use**
For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way.
The app does use third party services that may collect information used to identify you.
Link to privacy policy of third party service providers used by the app
* [Google Play Services](https://www.google.com/policies/privacy/)
**Log Data**
I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.
**Cookies**
Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.
**Service Providers**
I may employ third-party companies and individuals due to the following reasons:
* To facilitate our Service;
* To provide the Service on our behalf;
* To perform Service-related services; or
* To assist us in analyzing how our Service is used.
I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.
**Security**
I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.
**Links to Other Sites**
This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
**Children’s Privacy**
These Services do not address anyone under the age of 13\. I do not knowingly collect personally identifiable information from children under 13\. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions.
**Changes to This Privacy Policy**
I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
**Contact Us**
If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.firebaseapp.com/)
\ No newline at end of file
...@@ -56,7 +56,7 @@ android { ...@@ -56,7 +56,7 @@ android {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.leanback:leanback-preference:$androidxVersion" implementation "androidx.leanback:leanback-preference:1.0.0"
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
......
...@@ -294,6 +294,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -294,6 +294,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
} }
} catch (e: Exception) { } catch (e: Exception) {
printLog(e) printLog(e)
Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show()
} }
populateProfiles() populateProfiles()
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment