From 7d04980e309152284022d6ca5e2ed7727a7bb413 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 21 Nov 2019 23:40:35 +0800 Subject: [PATCH] main activity --- app/build.gradle | 19 ++- .../com/github/kr328/clash/MainActivity.kt | 115 ++++++++++++++++++ .../res/layout/activity_main_clash_status.xml | 6 +- .../res/layout/activity_main_profiles.xml | 2 +- .../res/layout/activity_main_proxy_manage.xml | 2 +- app/src/main/res/values/strings.xml | 5 +- core/build.gradle | 9 +- core/src/main/golang/main.go | 1 + core/src/main/golang/tun/tun.go | 2 +- .../github/kr328/clash/core/ClashProcess.kt | 114 ++++++++--------- .../kr328/clash/core/ClashProcessStatus.kt | 6 +- service/build.gradle | 3 +- service/src/main/AndroidManifest.xml | 4 - .../kr328/clash/service/ClashService.kt | 34 ++++-- .../github/kr328/clash/service/TunService.kt | 9 +- .../kr328/clash/service/VpnRequestActivity.kt | 32 ----- .../clash/service/data/ClashProfileDao.kt | 6 +- .../clash/service/data/ClashProfileEntity.kt | 46 +++++-- 18 files changed, 276 insertions(+), 139 deletions(-) delete mode 100644 service/src/main/java/com/github/kr328/clash/service/VpnRequestActivity.kt diff --git a/app/build.gradle b/app/build.gradle index 0fd02f1..cd0ff9e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,6 +19,13 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { @@ -26,12 +33,16 @@ dependencies { implementation project(":core") implementation project(":service") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.core:core-ktx:1.1.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.lifecycle:lifecycle-livedata:2.1.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" - implementation "androidx.constraintlayout:constraintlayout:1.1.3" + implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0-rc01' + implementation 'androidx.core:core-ktx:1.2.0-beta02' + implementation 'androidx.fragment:fragment-ktx:1.2.0-rc02' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3' implementation "androidx.room:room-runtime:$room_version" implementation "com.google.android.material:material:1.2.0-alpha02" } diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 666a703..2dd8542 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -1,10 +1,56 @@ package com.github.kr328.clash +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.net.VpnService import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.observe +import com.github.kr328.clash.core.ClashProcessStatus +import com.github.kr328.clash.service.ClashService +import com.github.kr328.clash.service.IClashObserver +import com.github.kr328.clash.service.IClashService +import com.github.kr328.clash.service.TunService +import com.github.kr328.clash.service.data.ClashDatabase import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.activity_main_clash_status.* +import kotlinx.android.synthetic.main.activity_main_profiles.* +import kotlinx.android.synthetic.main.activity_main_proxy_manage.* class MainActivity : AppCompatActivity() { + companion object { + const val VPN_REQUEST_CODE = 233 + } + + private val handler = Handler() + private var clash: IClashService? = null + private val connection = object: ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + finish() + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + clash = IClashService.Stub.asInterface(service) + + clash?.apply { + registerObserver("main_activity",true , object: IClashObserver.Stub() { + override fun onStatusChanged(status: ClashProcessStatus?) { + clashStatus.postValue(status ?: ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED_INT)) + } + }) + } + } + } + + private val clashStatus = MutableLiveData(ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED_INT)) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -12,5 +58,74 @@ class MainActivity : AppCompatActivity() { setSupportActionBar(activity_main_toolbar.apply { setLogo(R.mipmap.ic_launcher_foreground) }) + + clashStatus.observe(this) { + handler.removeMessages(0) + handler.postDelayed({ + when ( it.status ) { + ClashProcessStatus.STATUS_STARTED_INT -> { + activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_started) + activity_main_clash_status_title.text = getString(R.string.clash_status_started) + activity_main_clash_status_summary.text = getString(R.string.clash_status_forwarded_traffic, "1.4GiB") + activity_main_clash_proxies.visibility = View.VISIBLE + activity_main_clash_logs.visibility = View.VISIBLE + } + else -> { + activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped) + activity_main_clash_status_title.text = getString(R.string.clash_status_stopped) + activity_main_clash_status_summary.text = getString(R.string.clash_status_click_to_start) + activity_main_clash_proxies.visibility = View.GONE + activity_main_clash_logs.visibility = View.GONE + } + } + }, 300) + } + + activity_main_clash_status.setOnClickListener { + when ( clashStatus.value?.status ?: ClashProcessStatus.STATUS_STOPPED_INT ) { + ClashProcessStatus.STATUS_STARTED_INT -> {} + ClashProcessStatus.STATUS_STOPPED_INT -> { + VpnService.prepare(this)?.apply { + startActivityForResult(this, VPN_REQUEST_CODE) + } ?: startService(Intent(this, TunService::class.java)) + } + } + } + + ClashDatabase.getInstance(this) + .openClashProfileDao() + .observeDefaultProfileUrl() + .observe(this) { + activity_main_clash_profiles_summary.text = + if ( it == null ) + getString(R.string.clash_profiles_summary_unselected) + else + getString(R.string.clash_profiles_summary_selected, it) + } + } + + override fun onStart() { + super.onStart() + + bindService(Intent(this, ClashService::class.java), + connection, + Context.BIND_AUTO_CREATE) + } + + override fun onStop() { + super.onStop() + + unbindService(connection) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when ( requestCode ) { + VPN_REQUEST_CODE -> { + if ( resultCode == Activity.RESULT_OK ) + startService(Intent(this, TunService::class.java)) + } + } + + super.onActivityResult(requestCode, resultCode, data) } } diff --git a/app/src/main/res/layout/activity_main_clash_status.xml b/app/src/main/res/layout/activity_main_clash_status.xml index a6365a0..fff47dd 100644 --- a/app/src/main/res/layout/activity_main_clash_status.xml +++ b/app/src/main/res/layout/activity_main_clash_status.xml @@ -16,8 +16,8 @@ Clash Stopped Click to start - Clash Started + Clash Started %s Traffic Forwarded Proxy Manage %d Proxies Profiles - %s Selected + %s Selected + Unselected Logs Settings diff --git a/core/build.gradle b/core/build.gradle index 726680b..045f1c6 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -28,11 +28,15 @@ android { jniLibs.srcDirs += ["$buildDir/intermediates/native_output"] } } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } } dependencies { implementation "androidx.core:core-ktx:1.1.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } afterEvaluate { @@ -52,7 +56,7 @@ afterEvaluate { def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { onlyIf { - System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 30 * 24 * 3600L + System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 24 * 3600L } output = "$buildDir/intermediates/cache/Country.tar.gz" @@ -69,6 +73,7 @@ afterEvaluate { into "$buildDir/intermediates/cache/mmdb" doLast { + file("$buildDir/intermediates/dynamic_assets").mkdirs() fileTree("$buildDir/intermediates/cache/mmdb").visit { FileVisitDetails details -> if ( details.path.endsWith("Country.mmdb") ) details.file.renameTo(file("$buildDir/intermediates/dynamic_assets/Country.mmdb")) diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go index 522d062..dc46393 100644 --- a/core/src/main/golang/main.go +++ b/core/src/main/golang/main.go @@ -29,6 +29,7 @@ func main() { return } + fmt.Println("[PID]", os.Getpid()) fmt.Println("[CONTROLLER] STARTED") profile.LoadDefault() diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index ad79754..ebe8bfb 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -87,7 +87,7 @@ func newTunProxy(fd int, mtu int) (*tun, error) { // IPv6 [::]/0 subnet, _ = tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 16)), tcpip.AddressMask(strings.Repeat("\x00", 16))) - ipstack.AddAddressRange(1, ipv4.ProtocolNumber, subnet) + ipstack.AddAddressRange(1, ipv6.ProtocolNumber, subnet) // TCP handler tcpFwd := tcp.NewForwarder(ipstack, 0, 16, func(r *tcp.ForwarderRequest) { diff --git a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt index 49b21e5..413d033 100644 --- a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt +++ b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt @@ -5,6 +5,7 @@ import android.util.Log import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.lang.Exception import kotlin.concurrent.thread abstract class ClashProcess(private val context: Context, @@ -14,11 +15,9 @@ abstract class ClashProcess(private val context: Context, companion object { const val TAG = "ClashForAndroid" - private const val CLASH_COMMAND = "\"{CLASH}\" \"{CONTROLLER}\" 2>&1" - private const val CONTROLLER_STATUS_PREFIX = "[CONTROLLER]" - private val PID_PATTERN = Regex("PID=(\\d+)") + private val PID_PATTERN = Regex("\\[PID]\\s*(\\d+)") private val CONTROLLER_ERROR_PATTERN = Regex("\\[CONTROLLER] ERROR=\\{(.+)\\}") } @@ -30,71 +29,72 @@ abstract class ClashProcess(private val context: Context, if ( pid > 0 ) return - extractMMDB() + try { + clashDir.mkdirs() + controllerPath.parentFile?.mkdirs() + controllerPath.delete() - clashDir.mkdirs() - controllerPath.parentFile?.mkdirs() - controllerPath.delete() + extractMMDB() - val p = ProcessBuilder().apply { - command("sh") - directory(clashDir) - }.start() + val clashPath = File(context.applicationInfo.nativeLibraryDir,"libclash.so").absolutePath - val clashPath = File(context.applicationInfo.nativeLibraryDir,"libclash.so").absolutePath - val command = CLASH_COMMAND - .replace("{CLASH}", clashPath) - .replace("{CONTROLLER}", controllerPath.absolutePath) + val p = ProcessBuilder().apply { + command(clashPath, controllerPath.absolutePath) + directory(clashDir) + }.start() - Log.i(TAG, "Starting clash [$command]") + Log.i(TAG, "Starting clash [$clashPath]") - p.outputStream.use { - it.write("echo PID=$$\n".toByteArray()) - it.write("exec $command\n".toByteArray()) - it.write("exit\n".toByteArray()) - it.flush() - } + val reader = p.inputStream.bufferedReader() + var line = "" - var line = "" + // Parse pid + var currentPid = 0 + while ( reader.readLine()?.apply { line = this.trim() } != null ) { + Log.d(TAG, line) - // Parse pid - var currentPid = 0 - while ( p.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) { - if ( PID_PATTERN.matchEntire(line)?.apply { currentPid = groups[1]!!.value.toInt() } != null ) - break - } - - while ( p.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) { - if ( line.startsWith(CONTROLLER_STATUS_PREFIX) ) { - val error = CONTROLLER_ERROR_PATTERN.matchEntire(line)?.groups?.get(1)?.value - - if ( error != null ) - throw IOException("Controller: $error") - - break - } - } - - process = p - pid = currentPid - - listener(ClashProcessStatus(ClashProcessStatus.STATUS_STARTED)) - - Log.i(TAG, "Clash started pid = $pid") - - thread { - // Redirect stdout to log - while ( p.inputStream.bufferedReader().readLine()?.apply { line = this } != null ) { - Log.i(TAG, line.trim()) + if ( PID_PATTERN.matchEntire(line)?.apply { currentPid = groups[1]!!.value.toInt() } != null ) + break } - synchronized(this@ClashProcess) { - p.destroy() - process = null - pid = -1 + while ( reader.readLine()?.apply { line = this.trim() } != null ) { + Log.d(TAG, line) + + if ( line.startsWith(CONTROLLER_STATUS_PREFIX) ) { + val error = CONTROLLER_ERROR_PATTERN.matchEntire(line)?.groups?.get(1)?.value + + if ( error != null ) + throw IOException("Controller: $error") + + break + } } - listener(ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED)) + process = p + pid = currentPid + + listener(ClashProcessStatus.STATUS_STARTED) + + Log.i(TAG, "Clash started pid = $pid") + + thread { + // Redirect stdout to log + while ( reader.readLine()?.apply { line = this } != null ) { + Log.i(TAG, line.trim()) + } + + synchronized(this@ClashProcess) { + p.destroy() + process = null + pid = -1 + } + + listener(ClashProcessStatus.STATUS_STOPPED) + } + } + catch (e: Exception) { + listener(ClashProcessStatus.STATUS_STOPPED) + throw e } } diff --git a/core/src/main/java/com/github/kr328/clash/core/ClashProcessStatus.kt b/core/src/main/java/com/github/kr328/clash/core/ClashProcessStatus.kt index 563c0d2..28b00ae 100644 --- a/core/src/main/java/com/github/kr328/clash/core/ClashProcessStatus.kt +++ b/core/src/main/java/com/github/kr328/clash/core/ClashProcessStatus.kt @@ -5,8 +5,10 @@ import android.os.Parcelable data class ClashProcessStatus(val status: Int) : Parcelable { companion object { - const val STATUS_STOPPED = 0 - const val STATUS_STARTED = 1 + const val STATUS_STOPPED_INT = 0 + const val STATUS_STARTED_INT = 1 + val STATUS_STOPPED = ClashProcessStatus(STATUS_STOPPED_INT) + val STATUS_STARTED = ClashProcessStatus(STATUS_STARTED_INT) @JvmField val CREATOR = object: Parcelable.Creator { diff --git a/service/build.gradle b/service/build.gradle index 8a5c61d..d787e82 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -36,7 +36,6 @@ dependencies { implementation project(":core") implementation "androidx.lifecycle:lifecycle-livedata:2.1.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.core:core-ktx:1.1.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.room:room-runtime:$room_version" } diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 1fff614..7c99c44 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -1,10 +1,6 @@ - { override fun onChanged(url: String?) { executor.submit { + Log.i(TAG, "Loading profile $url") + if (url == null) clash.stop() if (url == lastProfile) @@ -55,7 +59,7 @@ class ClashService : Service() { lastProfile = null ClashDatabase.getInstance(this@ClashService) .openClashProfileDao() - .setDefaultProfile(p ?: "") + .setSelectedProfile(p ?: "") } } } @@ -97,6 +101,8 @@ class ClashService : Service() { try { clash.start() } catch (e: IOException) { + Log.e(TAG, "Start failure", e) + throw RemoteException(e.message) } } @@ -128,12 +134,19 @@ class ClashService : Service() { ) { this.status = it - when (it.status) { - ClashProcessStatus.STATUS_STARTED -> - ClashDatabase.getInstance(this) - .openClashProfileDao() - .observeDefaultProfileUrl() - .observeForever(defaultProfileObserver) + handler.post { + when (it.status) { + ClashProcessStatus.STATUS_STARTED_INT -> + ClashDatabase.getInstance(this) + .openClashProfileDao() + .observeDefaultProfileUrl() + .observeForever(defaultProfileObserver) + ClashProcessStatus.STATUS_STOPPED_INT -> + ClashDatabase.getInstance(this) + .openClashProfileDao() + .observeDefaultProfileUrl() + .removeObserver(defaultProfileObserver) + } } } } @@ -154,11 +167,6 @@ class ClashService : Service() { clash.stop() executor.shutdown() - ClashDatabase.getInstance(this) - .openClashProfileDao() - .observeDefaultProfileUrl() - .removeObserver(defaultProfileObserver) - super.onDestroy() } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 43c5069..bc2a57a 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -20,7 +20,7 @@ class TunService : VpnService() { private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2" } - private lateinit var fileDescriptor: ParcelFileDescriptor + private var fileDescriptor: ParcelFileDescriptor? = null private var clash: IClashService? = null private val connection = object: ServiceConnection { override fun onServiceDisconnected(name: ComponentName?) { @@ -40,9 +40,9 @@ class TunService : VpnService() { return when ( status.status ) { - ClashProcessStatus.STATUS_STOPPED -> + ClashProcessStatus.STATUS_STOPPED_INT -> stopSelf() - ClashProcessStatus.STATUS_STARTED -> + ClashProcessStatus.STATUS_STARTED_INT -> clash.startTunDevice(fileDescriptor, VPN_MTU) } } @@ -57,7 +57,6 @@ class TunService : VpnService() { super.onCreate() if ( prepare(this) != null ) { - startActivity(Intent(this, VpnRequestActivity::class.java)) stopSelf() return } @@ -84,7 +83,7 @@ class TunService : VpnService() { } override fun onDestroy() { - fileDescriptor.close() + fileDescriptor?.close() clash?.stopTunDevice() clash?.stop() diff --git a/service/src/main/java/com/github/kr328/clash/service/VpnRequestActivity.kt b/service/src/main/java/com/github/kr328/clash/service/VpnRequestActivity.kt deleted file mode 100644 index 358de3c..0000000 --- a/service/src/main/java/com/github/kr328/clash/service/VpnRequestActivity.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.kr328.clash.service - -import android.app.Activity -import android.content.Intent -import android.net.VpnService -import android.os.Bundle - -class VpnRequestActivity : Activity() { - companion object { - const val REQUEST_CODE = 124 - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val intent = VpnService.prepare(this) - - if ( intent == null ) - finish() - else - startActivityForResult(intent, REQUEST_CODE) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if ( requestCode == REQUEST_CODE && resultCode == RESULT_OK ) { - startService(Intent(this, TunService::class.java)) - } - else { - super.onActivityResult(requestCode, resultCode, data) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt index 071b86e..8679070 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt @@ -6,9 +6,9 @@ import androidx.room.Query @Dao interface ClashProfileDao { - @Query("UPDATE profiles SET `default` = CASE WHEN url = :url THEN 1 ELSE 0 END") - fun setDefaultProfile(url: String) + @Query("UPDATE profiles SET selected = CASE WHEN url = :url THEN 1 ELSE 0 END") + fun setSelectedProfile(url: String) - @Query("SELECT url FROM profiles WHERE `default` = 1 LIMIT 1") + @Query("SELECT url FROM profiles WHERE selected = 1 LIMIT 1") fun observeDefaultProfileUrl(): LiveData } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index dafabf2..78e7745 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -1,10 +1,42 @@ package com.github.kr328.clash.service.data -import androidx.room.ColumnInfo -import androidx.room.Entity +import androidx.room.* -@Entity(tableName = "profiles", primaryKeys = ["url"]) -data class ClashProfileEntity(@ColumnInfo(name = "url") val url: String, - @ColumnInfo(name = "default") val defaultProfile: Boolean, - @ColumnInfo(name = "writable") val writable: Boolean, - @ColumnInfo(name = "last_update") val lastUpdate: Long) \ No newline at end of file +@Entity(tableName = "profiles") +@TypeConverters(ClashProfileEntity.Converters::class) +data class ClashProfileEntity( + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "url") val url: String, + @ColumnInfo(name = "selected") val selected: Boolean, + @ColumnInfo(name = "type") val writable: Type, + @ColumnInfo(name = "last_update") val lastUpdate: Long, + @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0) { + + enum class Type { + UNKNOWN, URL, FILE + } + + class Converters { + @TypeConverter + fun typeToInt(type: Type?): Int? { + return type?.run { + when ( this ) { + Type.URL -> 1 + Type.FILE -> 2 + Type.UNKNOWN -> 0 + } + } + } + + @TypeConverter + fun intToType(i: Int?): Type? { + return i?.run { + when (this) { + 1 -> Type.URL + 2 -> Type.FILE + else -> Type.UNKNOWN + } + } + } + } +} \ No newline at end of file