Unverified Commit 4126b313 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #2402 from Mygod/subscription

Refactor subscriptions
parents 0154cfda 56e788a8
...@@ -61,6 +61,11 @@ ...@@ -61,6 +61,11 @@
android:exported="false"> android:exported="false">
</service> </service>
<service
android:name="com.github.shadowsocks.subscription.SubscriptionService"
android:exported="false">
</service>
<activity <activity
android:name="com.github.shadowsocks.UrlImportActivity" android:name="com.github.shadowsocks.UrlImportActivity"
android:theme="@style/Theme.AppCompat.Translucent" android:theme="@style/Theme.AppCompat.Translucent"
......
...@@ -42,6 +42,7 @@ import com.github.shadowsocks.database.Profile ...@@ -42,6 +42,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.TcpFastOpen import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.subscription.SubscriptionService
import com.github.shadowsocks.utils.* import com.github.shadowsocks.utils.*
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
...@@ -61,6 +62,7 @@ object Core { ...@@ -61,6 +62,7 @@ object Core {
lateinit var configureIntent: (Context) -> PendingIntent lateinit var configureIntent: (Context) -> PendingIntent
val activity by lazy { app.getSystemService<ActivityManager>()!! } val activity by lazy { app.getSystemService<ActivityManager>()!! }
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! } val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
val notification by lazy { app.getSystemService<NotificationManager>()!! }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) } val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) } val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) } val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) }
...@@ -128,16 +130,16 @@ object Core { ...@@ -128,16 +130,16 @@ object Core {
fun updateNotificationChannels() { fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) { if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = app.getSystemService<NotificationManager>()!! notification.createNotificationChannels(listOf(
nm.createNotificationChannels(listOf(
NotificationChannel("service-vpn", app.getText(R.string.service_vpn), NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
else NotificationManager.IMPORTANCE_LOW), // #1355 else NotificationManager.IMPORTANCE_LOW), // #1355
NotificationChannel("service-proxy", app.getText(R.string.service_proxy), NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW), NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy), NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW))) NotificationManager.IMPORTANCE_LOW),
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good SubscriptionService.notificationChannel))
notification.deleteNotificationChannel("service-nat") // NAT mode is gone for good
} }
} }
......
...@@ -53,10 +53,12 @@ data class Profile( ...@@ -53,10 +53,12 @@ data class Profile(
// user configurable fields // user configurable fields
var name: String? = "", var name: String? = "",
var host: String = sponsored, var host: String = sponsored,
var remotePort: Int = 8388, var remotePort: Int = 8388,
var password: String = "u1rRWTssNv0p", var password: String = "u1rRWTssNv0p",
var method: String = "aes-256-cfb", var method: String = "aes-256-cfb",
var route: String = "all", var route: String = "all",
var remoteDns: String = "dns.google", var remoteDns: String = "dns.google",
var proxyApps: Boolean = false, var proxyApps: Boolean = false,
...@@ -252,8 +254,11 @@ data class Profile( ...@@ -252,8 +254,11 @@ data class Profile(
@Query("SELECT * FROM `Profile` WHERE `id` = :id") @Query("SELECT * FROM `Profile` WHERE `id` = :id")
operator fun get(id: Long): Profile? operator fun get(id: Long): Profile?
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`") @Query("SELECT * FROM `Profile` WHERE `Subscription` != 2 ORDER BY `userOrder`")
fun list(): List<Profile> fun listActive(): List<Profile>
@Query("SELECT * FROM `Profile`")
fun listAll(): List<Profile>
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`") @Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
fun nextOrder(): Long? fun nextOrder(): Long?
......
...@@ -54,38 +54,6 @@ object ProfileManager { ...@@ -54,38 +54,6 @@ object ProfileManager {
return profile return profile
} }
fun createProfilesFromSubscription(jsons: Sequence<InputStream>, replace: Boolean,
oldProfiles: List<Profile>?) {
val profiles = oldProfiles?.associateBy { it.formattedAddress }
val feature = profiles?.values?.singleOrNull { it.id == DataStore.profileId }
val lazyClear = lazy { clear() }
jsons.asIterable().forEachTry { json ->
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) {
if (replace) {
lazyClear.value
}
// if two profiles has the same address, treat them as the same profile and copy settings over
profiles?.get(it.formattedAddress)?.apply {
it.tx = tx
it.rx = rx
it.individual = individual
it.route = route
it.bypass = bypass
it.ipv6 = ipv6
it.metered = metered
it.proxyApps = proxyApps
it.remoteDns = remoteDns
it.udpdns = udpdns
it.udpFallback = udpFallback
}
it.subscription = Profile.SubscriptionStatus.Active
createProfile(it)
}
}
}
fun createProfilesFromJson(jsons: Sequence<InputStream>, replace: Boolean = false) { fun createProfilesFromJson(jsons: Sequence<InputStream>, replace: Boolean = false) {
val profiles = if (replace) getAllProfiles()?.associateBy { it.formattedAddress } else null val profiles = if (replace) getAllProfiles()?.associateBy { it.formattedAddress } else null
val feature = if (replace) { val feature = if (replace) {
...@@ -107,7 +75,7 @@ object ProfileManager { ...@@ -107,7 +75,7 @@ object ProfileManager {
} }
} }
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? { fun serializeToJson(profiles: List<Profile>? = getActiveProfiles()): JSONArray? {
if (profiles == null) return null if (profiles == null) return null
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } } val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
return JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray()) return JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray())
...@@ -159,9 +127,19 @@ object ProfileManager { ...@@ -159,9 +127,19 @@ object ProfileManager {
if (!nonEmpty) DataStore.profileId = createProfile().id if (!nonEmpty) DataStore.profileId = createProfile().id
} }
@Throws(IOException::class)
fun getActiveProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.listActive()
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
printLog(ex)
null
}
@Throws(IOException::class) @Throws(IOException::class)
fun getAllProfiles(): List<Profile>? = try { fun getAllProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.list() PrivateDatabase.profileDao.listAll()
} catch (ex: SQLiteCantOpenDatabaseException) { } catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex) throw IOException(ex)
} catch (ex: SQLException) { } catch (ex: SQLException) {
......
/*******************************************************************************
* *
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.subscription
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import com.google.gson.JsonStreamParser
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
class SubscriptionService : Service() {
companion object {
private const val NOTIFICATION_CHANNEL = "service-subscription"
private const val NOTIFICATION_ID = 2
private var worker: Job? = null
val idle = MutableLiveData<Boolean>(true)
val notificationChannel @RequiresApi(26) get() = NotificationChannel(NOTIFICATION_CHANNEL,
app.getText(R.string.service_subscription), NotificationManager.IMPORTANCE_LOW)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (worker == null) {
idle.value = false
worker = GlobalScope.launch {
val urls = Subscription.instance.urls
val notification = NotificationCompat.Builder(this@SubscriptionService, NOTIFICATION_CHANNEL).apply {
color = ContextCompat.getColor(this@SubscriptionService, R.color.material_primary_500)
priority = NotificationCompat.PRIORITY_LOW
setCategory(NotificationCompat.CATEGORY_PROGRESS)
setContentTitle(getString(R.string.service_subscription_working))
setOngoing(true)
setProgress(urls.size(), 0, false)
setSmallIcon(R.drawable.ic_file_cloud_download)
setWhen(0)
}
Core.notification.notify(NOTIFICATION_ID, notification.build())
counter = 0
val workers = urls.asIterable().map { url ->
async(Dispatchers.IO) { work(url, urls.size(), notification) }
}
try {
val localJsons = workers.awaitAll()
withContext(Dispatchers.Main) {
Core.notification.notify(NOTIFICATION_ID, notification.apply {
setProgress(0, 0, true)
}.build())
createProfilesFromSubscription(localJsons.asSequence().filterNotNull().map { it.inputStream() })
}
} finally {
for (worker in workers) {
worker.cancel()
try {
worker.getCompleted()?.apply { if (!delete()) deleteOnExit() }
} catch (_: Exception) { }
}
GlobalScope.launch(Dispatchers.Main) {
Core.notification.cancel(NOTIFICATION_ID)
idle.value = true
}
check(worker != null)
worker = null
stopSelf(startId)
}
}
} else stopSelf(startId)
return START_NOT_STICKY
}
private var counter = 0
private suspend fun work(url: URL, max: Int, notification: NotificationCompat.Builder): File? {
val tempFile = File.createTempFile("subscription-", ".json", cacheDir)
try {
(url.openConnection() as HttpURLConnection).useCancellable {
tempFile.outputStream().use { out -> inputStream.copyTo(out) }
}
return tempFile
} catch (e: IOException) {
e.printStackTrace()
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(this@SubscriptionService, e.readableMessage, Toast.LENGTH_LONG).show()
}
if (!tempFile.delete()) tempFile.deleteOnExit()
return null
} finally {
withContext(Dispatchers.Main) {
counter += 1
Core.notification.notify(NOTIFICATION_ID, notification.apply {
setProgress(max, counter, false)
}.build())
}
}
}
private fun createProfilesFromSubscription(jsons: Sequence<InputStream>) {
val currentId = DataStore.profileId
val profiles = ProfileManager.getAllProfiles()
val subscriptions = mutableMapOf<Pair<String?, String>, Profile>()
val toUpdate = mutableSetOf<Long>()
var feature: Profile? = null
profiles?.forEach { profile -> // preprocessing phase
if (currentId == profile.id) feature = profile
if (profile.subscription == Profile.SubscriptionStatus.UserConfigured) return@forEach
if (subscriptions.putIfAbsentCompat(profile.name to profile.formattedAddress, profile) != null) {
ProfileManager.delProfile(profile.id)
if (currentId == profile.id) DataStore.profileId = 0
} else if (profile.subscription == Profile.SubscriptionStatus.Active) {
toUpdate.add(profile.id)
profile.subscription = Profile.SubscriptionStatus.Obsolete
}
}
for (json in jsons.asIterable()) try {
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) {
subscriptions.computeCompat(it.name to it.formattedAddress) { _, oldProfile ->
when (oldProfile?.subscription) {
Profile.SubscriptionStatus.Active -> oldProfile // skip dup subscription
Profile.SubscriptionStatus.Obsolete -> {
oldProfile.password = it.password
oldProfile.method = it.method
oldProfile.plugin = it.plugin
oldProfile.udpFallback = it.udpFallback
oldProfile.subscription = Profile.SubscriptionStatus.Active
oldProfile
}
else -> ProfileManager.createProfile(it.apply {
subscription = Profile.SubscriptionStatus.Active
})
}
}
}
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, e.readableMessage, Toast.LENGTH_LONG).show()
}
profiles?.forEach { profile -> if (toUpdate.contains(profile.id)) ProfileManager.updateProfile(profile) }
}
override fun onDestroy() {
worker?.cancel()
super.onDestroy()
}
}
...@@ -89,8 +89,18 @@ fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_IN ...@@ -89,8 +89,18 @@ fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_IN
if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null, this) as InetAddress if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null, this) as InetAddress
} }
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT >= 24) fun <K, V> MutableMap<K, V>.computeCompat(key: K, remappingFunction: (K, V?) -> V?) = if (Build.VERSION.SDK_INT < 24) {
computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) } val oldValue = get(key)
remappingFunction(key, oldValue).also { newValue ->
if (newValue != null) put(key, newValue) else if (oldValue != null || containsKey(key)) remove(key)
}
} else compute(key) { k, oldValue -> remappingFunction(k, oldValue) }
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT < 24) {
this[key] ?: value().also { put(key, it) }
} else computeIfAbsent(key) { value() }
fun <K, V> MutableMap<K, V>.putIfAbsentCompat(key: K, value: V) = if (Build.VERSION.SDK_INT < 24) {
this[key] ?: put(key, value)
} else putIfAbsent(key, value)
suspend fun <T> HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T { suspend fun <T> HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T {
return suspendCancellableCoroutine { cont -> return suspendCancellableCoroutine { cont ->
......
...@@ -140,6 +140,9 @@ ...@@ -140,6 +140,9 @@
<string name="add_subscription">Add a subscription</string> <string name="add_subscription">Add a subscription</string>
<string name="edit_subscription">Edit subscription</string> <string name="edit_subscription">Edit subscription</string>
<string name="update_subscription">Refresh servers from subscription</string> <string name="update_subscription">Refresh servers from subscription</string>
<string name="service_subscription">Subscription Service</string>
<string name="service_subscription_working">Syncing subscriptions… (%d of %d)</string>
<string name="service_subscription_finishing">Finishing up…</string>
<!-- acl --> <!-- acl -->
<string name="custom_rules">Custom rules</string> <string name="custom_rules">Custom rules</string>
......
...@@ -70,7 +70,6 @@ dependencies { ...@@ -70,7 +70,6 @@ dependencies {
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'me.zhanghai.android.fastscroll:library:1.1.0' implementation 'me.zhanghai.android.fastscroll:library:1.1.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
......
...@@ -126,9 +126,9 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -126,9 +126,9 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
findPreference<Preference>(Key.password)!!.isEnabled = false findPreference<Preference>(Key.password)!!.isEnabled = false
findPreference<Preference>(Key.method)!!.isEnabled = false findPreference<Preference>(Key.method)!!.isEnabled = false
findPreference<Preference>(Key.remotePort)!!.isEnabled = false findPreference<Preference>(Key.remotePort)!!.isEnabled = false
findPreference<Preference>(Key.plugin)!!.isEnabled = false plugin.isEnabled = false
findPreference<Preference>(Key.pluginConfigure)!!.isEnabled = false pluginConfigure.isEnabled = false
udpFallback.isEnabled = false
} }
} }
......
...@@ -344,7 +344,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -344,7 +344,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>(), ProfileManager.Listener { inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>(), ProfileManager.Listener {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf() internal val profiles = ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf()
private val updated = HashSet<Profile>() private val updated = HashSet<Profile>()
init { init {
...@@ -543,7 +543,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -543,7 +543,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
true true
} }
R.id.action_export_clipboard -> { R.id.action_export_clipboard -> {
val profiles = ProfileManager.getAllProfiles() val profiles = ProfileManager.getActiveProfiles()
(activity as MainActivity).snackbar().setText(if (profiles != null) { (activity as MainActivity).snackbar().setText(if (profiles != null) {
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.joinToString("\n"))) clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.joinToString("\n")))
R.string.action_export_msg R.string.action_export_msg
......
...@@ -64,7 +64,7 @@ class UdpFallbackProfileActivity : AppCompatActivity() { ...@@ -64,7 +64,7 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
} }
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() { inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = (ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()) internal val profiles = (ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf())
.filter { it.id != editingId && PluginConfiguration(it.plugin ?: "").selected.isEmpty() } .filter { it.id != editingId && PluginConfiguration(it.plugin ?: "").selected.isEmpty() }
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
......
...@@ -33,6 +33,7 @@ import android.widget.EditText ...@@ -33,6 +33,7 @@ import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.observe
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
...@@ -41,23 +42,14 @@ import com.github.shadowsocks.MainActivity ...@@ -41,23 +42,14 @@ import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.readableMessage import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.useCancellable
import com.github.shadowsocks.widget.ListHolderListener import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.MainListListener import com.github.shadowsocks.widget.MainListListener
import com.github.shadowsocks.widget.UndoSnackbarManager import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
import me.zhanghai.android.materialprogressbar.MaterialProgressBar
import java.net.HttpURLConnection
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
...@@ -89,7 +81,7 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener ...@@ -89,7 +81,7 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
inputLayout = view.findViewById(R.id.content_layout) inputLayout = view.findViewById(R.id.content_layout)
editText.setText(arg.item) editText.setText(arg.item)
editText.addTextChangedListener(this@SubDialogFragment) editText.addTextChangedListener(this@SubDialogFragment)
setTitle(R.string.add_subscription) setTitle(R.string.edit_subscription)
setPositiveButton(android.R.string.ok, listener) setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null) setNegativeButton(android.R.string.cancel, null)
if (arg.item.isNotEmpty()) setNeutralButton(R.string.delete, listener) if (arg.item.isNotEmpty()) setNeutralButton(R.string.delete, listener)
...@@ -219,55 +211,8 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener ...@@ -219,55 +211,8 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
private val adapter by lazy { SubscriptionAdapter() } private val adapter by lazy { SubscriptionAdapter() }
private lateinit var list: RecyclerView private lateinit var list: RecyclerView
private lateinit var progress: MaterialProgressBar
private var mode: ActionMode? = null private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<Any> private lateinit var undoManager: UndoSnackbarManager<URL>
private var fetchJob: Job? = null
private fun fetchServerFromSubscriptions() {
if (fetchJob?.isActive != true) {
val activity = activity as MainActivity
progress.visibility = View.VISIBLE
fetchJob = GlobalScope.launch {
val subscription = Subscription.instance
val oldProfiles = ProfileManager.getAllProfiles()
var replace = true
for (url in subscription.urls.asIterable()) {
try {
val connection = url.openConnection() as HttpURLConnection
connection.useCancellable {
ProfileManager.createProfilesFromSubscription(sequenceOf(connection.inputStream),
replace, oldProfiles)
}
} catch (e: Exception) {
e.printStackTrace()
activity.snackbar(e.readableMessage).show()
} finally {
replace = false
}
}
val userProfiles = oldProfiles?.filter { it.subscription == Profile.SubscriptionStatus.UserConfigured }
if (userProfiles != null) {
for (profile in userProfiles.asIterable()) {
ProfileManager.createProfile(profile)
}
}
progress.post {
progress.visibility = View.INVISIBLE
}
}
}
}
override fun onPause() {
fetchJob?.cancel()
super.onPause()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_subscriptions, container, false) inflater.inflate(R.layout.layout_subscriptions, container, false)
...@@ -278,13 +223,15 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener ...@@ -278,13 +223,15 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
toolbar.setTitle(R.string.subscriptions) toolbar.setTitle(R.string.subscriptions)
toolbar.inflateMenu(R.menu.subscription_menu) toolbar.inflateMenu(R.menu.subscription_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
SubscriptionService.idle.observe(this) {
toolbar.menu.findItem(R.id.action_update_subscription).isEnabled = it
}
val activity = activity as MainActivity val activity = activity as MainActivity
list = view.findViewById(R.id.list) list = view.findViewById(R.id.list)
list.setOnApplyWindowInsetsListener(MainListListener) list.setOnApplyWindowInsetsListener(MainListListener)
list.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) list.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator() list.itemAnimator = DefaultItemAnimator()
list.adapter = adapter list.adapter = adapter
progress = view.findViewById(R.id.indeterminate_horizontal_progress)
FastScrollerBuilder(list).useMd2Style().build() FastScrollerBuilder(list).useMd2Style().build()
undoManager = UndoSnackbarManager(activity, adapter::undo) undoManager = UndoSnackbarManager(activity, adapter::undo)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START or ItemTouchHelper.END) { ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START or ItemTouchHelper.END) {
...@@ -313,7 +260,8 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener ...@@ -313,7 +260,8 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
true true
} }
R.id.action_update_subscription -> { R.id.action_update_subscription -> {
fetchServerFromSubscriptions() val context = requireContext()
context.startService(Intent(context, SubscriptionService::class.java))
true true
} }
else -> false else -> false
......
...@@ -71,7 +71,7 @@ class ConfigActivity : AppCompatActivity() { ...@@ -71,7 +71,7 @@ class ConfigActivity : AppCompatActivity() {
} }
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() { inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf() internal val profiles = ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf()
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1]) if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1])
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
android:focusable="true" android:focusable="true"
android:padding="12dp" android:padding="12dp"
android:visibility="gone" android:visibility="gone"
app:srcCompat="@drawable/ic_cloud_queue"/> app:srcCompat="@drawable/ic_file_cloud_queue"/>
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/edit" android:id="@+id/edit"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
...@@ -9,18 +8,6 @@ ...@@ -9,18 +8,6 @@
<include layout="@layout/toolbar_light_dark" /> <include layout="@layout/toolbar_light_dark" />
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/indeterminate_horizontal_progress"
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:indeterminate="true"
android:visibility="invisible"
app:mpb_progressStyle="horizontal"
app:mpb_showProgressBackground="false"
app:mpb_useIntrinsicPadding="false" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
android:icon="@drawable/ic_action_description"/> android:icon="@drawable/ic_action_description"/>
<item android:id="@+id/subscriptions" <item android:id="@+id/subscriptions"
android:title="@string/subscriptions" android:title="@string/subscriptions"
android:icon="@drawable/ic_action_cloud_download"/> android:icon="@drawable/ic_file_cloud_download"/>
<item android:id="@+id/customRules" <item android:id="@+id/customRules"
android:title="@string/custom_rules" android:title="@string/custom_rules"
android:icon="@drawable/ic_action_assignment"/> android:icon="@drawable/ic_action_assignment"/>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
app:showAsAction="always"/> app:showAsAction="always"/>
<item android:title="@string/update_subscription" <item android:title="@string/update_subscription"
android:id="@+id/action_update_subscription" android:id="@+id/action_update_subscription"
android:icon="@drawable/ic_action_cloud_download" android:icon="@drawable/ic_file_cloud_download"
android:alphabeticShortcut="r" android:alphabeticShortcut="r"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
</menu> </menu>
...@@ -209,7 +209,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -209,7 +209,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private fun populateProfiles() { private fun populateProfiles() {
ProfileManager.ensureNotEmpty() ProfileManager.ensureNotEmpty()
val profiles = ProfileManager.getAllProfiles()!! val profiles = ProfileManager.getActiveProfiles()!!
fab.value = null fab.value = null
fab.entries = profiles.map { it.formattedName }.toTypedArray() fab.entries = profiles.map { it.formattedName }.toTypedArray()
fab.entryValues = profiles.map { it.id.toString() }.toTypedArray() fab.entryValues = profiles.map { it.id.toString() }.toTypedArray()
......
...@@ -52,7 +52,7 @@ class ProfilesDialogFragment : LeanbackListPreferenceDialogFragmentCompat() { ...@@ -52,7 +52,7 @@ class ProfilesDialogFragment : LeanbackListPreferenceDialogFragmentCompat() {
} }
} }
private inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() { private inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
val profiles = ProfileManager.getAllProfiles()!! val profiles = ProfileManager.getActiveProfiles()!!
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ProfileViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.leanback_list_preference_item_single_2, LayoutInflater.from(parent.context).inflate(R.layout.leanback_list_preference_item_single_2,
......
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