diff --git a/app/src/main/java/com/github/kr328/clash/Constants.kt b/app/src/main/java/com/github/kr328/clash/Constants.kt index 391d6c5..cd98ccc 100644 --- a/app/src/main/java/com/github/kr328/clash/Constants.kt +++ b/app/src/main/java/com/github/kr328/clash/Constants.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash object Constants { + const val CLASH_DIR = "clash" const val PROFILES_DIR = "profiles" } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index df0d808..531c706 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -10,6 +10,7 @@ import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.core.transact.DoneCallbackImpl import com.github.kr328.clash.core.transact.ProxyCollectionImpl import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl +import java.io.File import java.io.InputStream import java.lang.IllegalStateException import java.util.concurrent.BrokenBarrierException @@ -55,27 +56,27 @@ object Clash{ Bridge.stopTunDevice() } - fun loadProfile(path: String, baseDir: String): CompletableFuture { + fun loadProfile(path: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.loadProfileFile(path, baseDir, this) + Bridge.loadProfileFile(path.absolutePath, baseDir.absolutePath, this) } } - fun downloadProfile(url: String, output: String, baseDir: String): CompletableFuture { + fun downloadProfile(url: String, output: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.downloadProfileAndCheck(url, output, baseDir, this) + Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath, this) } } - fun saveProfile(data: ByteArray, output: String, baseDir: String): CompletableFuture { + fun saveProfile(data: ByteArray, output: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.saveProfileAndCheck(data, output, baseDir, this) + Bridge.saveProfileAndCheck(data, output.absolutePath, baseDir.absolutePath, this) } } - fun moveProfile(source: String, target: String, baseDir: String): CompletableFuture { + fun moveProfile(source: File, target: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.moveProfileAndCheck(source, target, baseDir, this) + Bridge.moveProfileAndCheck(source.absolutePath, target.absolutePath, baseDir.absolutePath, this) } } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl index 63d05ab..9a45840 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl @@ -1,9 +1,10 @@ package com.github.kr328.clash.service; import com.github.kr328.clash.service.ipc.IPCParcelables; +import com.github.kr328.clash.service.data.ClashProfileEntity; interface IClashProfileManager { - ParcelableCompletedFuture addProfile(String url); + ParcelableCompletedFuture addProfile(String name, int type, String uri); ParcelableCompletedFuture updateProfile(int id); - ParcelableCompletedFuture queryAllProfiles(); + ClashProfileEntity[] queryAllProfiles(); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index 428bbe7..495b92d 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -1,24 +1,114 @@ package com.github.kr328.clash.service +import android.content.Context +import android.net.Uri import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture +import com.github.kr328.clash.service.util.DefaultThreadPool +import com.github.kr328.clash.service.util.FileUtils + +class ClashProfileManager(private val context: Context, private val database: ClashDatabase) : + IClashProfileManager.Stub() { + private val clashDir = context.filesDir.resolve(Constants.CLASH_DIR) + private val profileDir = context.filesDir.resolve(Constants.PROFILES_DIR) -class ClashProfileManager(private val database: ClashDatabase): IClashProfileManager.Stub() { override fun updateProfile(id: Int): ParcelableCompletedFuture { val entity = database.openClashProfileDao().queryProfileById(id) - require(entity != null && entity.type == ClashProfileEntity.Type.URL) + require(entity != null && (entity.type == ClashProfileEntity.Type.URL || + entity.type == ClashProfileEntity.Type.FILE)) val result = ParcelableCompletedFuture() + + downloadProfile(Uri.parse(entity.uri), entity.file, entity.base, + onSuccess = { + result.complete(null) + + database.openClashProfileDao().touchProfile(id) + }, + onFailure = { + result.completeExceptionally(it) + }) + + return result } - override fun queryAllProfiles(): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun queryAllProfiles(): Array { + return database.openClashProfileDao().queryProfiles() } - override fun addProfile(url: String?): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun addProfile(name: String, type: Int, uri: String?): ParcelableCompletedFuture { + require(uri != null && (uri.startsWith("http") || uri.startsWith("content"))) + + val result = ParcelableCompletedFuture() + val fileName = FileUtils.generateRandomFileName(profileDir, ".yaml") + val baseDirName = FileUtils.generateRandomFileName(clashDir) + + downloadProfile(Uri.parse(uri), fileName, baseDirName, + onSuccess = { + database.openClashProfileDao().addProfile( + ClashProfileEntity( + name, + ClashProfileEntity.intToType(type), + uri, + fileName, + baseDirName, + false, + System.currentTimeMillis() + ) + ) + + result.complete(null) + }, + onFailure = { + result.completeExceptionally(it) + }) + + return result + } + + private fun downloadProfile( + uri: Uri?, + fileName: String, + baseDirName: String, + onSuccess: () -> Unit, + onFailure: (Throwable) -> Unit + ) { + require(uri != null && uri != Uri.EMPTY) + + if (uri.scheme == "http" || uri.scheme == "https") { + Clash.downloadProfile( + uri.toString(), + profileDir.resolve(fileName), + clashDir.resolve(baseDirName) + ) + .whenComplete { _, u -> + if (u != null) + onFailure(u) + else + onSuccess() + } + } else { + DefaultThreadPool.submit { + try { + val input = context.contentResolver.openInputStream(uri) + ?: throw NullPointerException("Unable to open profile") + + input.use { + Clash.saveProfile( + it.readBytes(), + profileDir.resolve(fileName), + clashDir.resolve(baseDirName) + ) + } + + onSuccess() + } catch (e: Exception) { + onFailure(e) + } + } + } } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index 925e530..8e6b4ae 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -3,4 +3,7 @@ package com.github.kr328.clash.service object Constants { const val CLASH_PROCESS_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ClashProcessEvent" const val CLASH_RELOAD_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ProfileReloadEvent" + + const val CLASH_DIR = "clash" + const val PROFILES_DIR = "profiles" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt index 70c9560..56488c5 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt @@ -5,8 +5,9 @@ import android.os.Parcelable import android.os.RemoteException import java.util.concurrent.CompletableFuture -class ParcelableCompletedFuture(private val pipe: ParcelablePipe = ParcelablePipe()) : +class ParcelableCompletedFuture private constructor(private val pipe: ParcelablePipe) : CompletableFuture(), Parcelable { + constructor(): this(ParcelablePipe()) constructor(parcel: Parcel) : this( parcel.readParcelable( ParcelableCompletedFuture::class.java.classLoader diff --git a/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt b/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt new file mode 100644 index 0000000..85a7783 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt @@ -0,0 +1,6 @@ +package com.github.kr328.clash.service.util + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +val DefaultThreadPool: ExecutorService = Executors.newCachedThreadPool() \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt new file mode 100644 index 0000000..aecba39 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.service.util + +import java.io.File +import java.security.SecureRandom +import kotlin.math.absoluteValue + +object FileUtils { + private val random = SecureRandom() + + fun generateRandomFileName(dir: File, suffix: String = ""): String { + dir.mkdirs() + + var fileName: String + + do { + fileName = random.nextLong().absoluteValue.toString() + suffix + } while (dir.resolve(fileName).exists()) + + return fileName + } +} \ No newline at end of file