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 @@
android:exported="false">
</service>
<service
android:name="com.github.shadowsocks.subscription.SubscriptionService"
android:exported="false">
</service>
<activity
android:name="com.github.shadowsocks.UrlImportActivity"
android:theme="@style/Theme.AppCompat.Translucent"
......
......@@ -42,6 +42,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.subscription.SubscriptionService
import com.github.shadowsocks.utils.*
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
......@@ -61,6 +62,7 @@ object Core {
lateinit var configureIntent: (Context) -> PendingIntent
val activity by lazy { app.getSystemService<ActivityManager>()!! }
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
val notification by lazy { app.getSystemService<NotificationManager>()!! }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) }
......@@ -128,16 +130,16 @@ object Core {
fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = app.getSystemService<NotificationManager>()!!
nm.createNotificationChannels(listOf(
notification.createNotificationChannels(listOf(
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
else NotificationManager.IMPORTANCE_LOW), // #1355
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW)))
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
NotificationManager.IMPORTANCE_LOW),
SubscriptionService.notificationChannel))
notification.deleteNotificationChannel("service-nat") // NAT mode is gone for good
}
}
......
......@@ -53,10 +53,12 @@ data class Profile(
// user configurable fields
var name: String? = "",
var host: String = sponsored,
var remotePort: Int = 8388,
var password: String = "u1rRWTssNv0p",
var method: String = "aes-256-cfb",
var route: String = "all",
var remoteDns: String = "dns.google",
var proxyApps: Boolean = false,
......@@ -252,8 +254,11 @@ data class Profile(
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
operator fun get(id: Long): Profile?
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
fun list(): List<Profile>
@Query("SELECT * FROM `Profile` WHERE `Subscription` != 2 ORDER BY `userOrder`")
fun listActive(): List<Profile>
@Query("SELECT * FROM `Profile`")
fun listAll(): List<Profile>
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
fun nextOrder(): Long?
......
......@@ -54,38 +54,6 @@ object ProfileManager {
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) {
val profiles = if (replace) getAllProfiles()?.associateBy { it.formattedAddress } else null
val feature = if (replace) {
......@@ -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
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
return JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray())
......@@ -159,9 +127,19 @@ object ProfileManager {
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)
fun getAllProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.list()
PrivateDatabase.profileDao.listAll()
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} 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
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)
computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) }
fun <K, V> MutableMap<K, V>.computeCompat(key: K, remappingFunction: (K, V?) -> V?) = if (Build.VERSION.SDK_INT < 24) {
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 {
return suspendCancellableCoroutine { cont ->
......
......@@ -140,6 +140,9 @@
<string name="add_subscription">Add a subscription</string>
<string name="edit_subscription">Edit 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 -->
<string name="custom_rules">Custom rules</string>
......
......@@ -70,7 +70,6 @@ dependencies {
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'me.zhanghai.android.fastscroll:library:1.1.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
......
......@@ -126,9 +126,9 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
findPreference<Preference>(Key.password)!!.isEnabled = false
findPreference<Preference>(Key.method)!!.isEnabled = false
findPreference<Preference>(Key.remotePort)!!.isEnabled = false
findPreference<Preference>(Key.plugin)!!.isEnabled = false
findPreference<Preference>(Key.pluginConfigure)!!.isEnabled = false
plugin.isEnabled = false
pluginConfigure.isEnabled = false
udpFallback.isEnabled = false
}
}
......
......@@ -344,7 +344,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
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>()
init {
......@@ -543,7 +543,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
true
}
R.id.action_export_clipboard -> {
val profiles = ProfileManager.getAllProfiles()
val profiles = ProfileManager.getActiveProfiles()
(activity as MainActivity).snackbar().setText(if (profiles != null) {
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.joinToString("\n")))
R.string.action_export_msg
......
......@@ -64,7 +64,7 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
}
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() }
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
......
......@@ -33,6 +33,7 @@ import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.observe
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
......@@ -41,23 +42,14 @@ import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment
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.utils.asIterable
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.useCancellable
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.MainListListener
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.material.textfield.TextInputLayout
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.materialprogressbar.MaterialProgressBar
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
......@@ -89,7 +81,7 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
inputLayout = view.findViewById(R.id.content_layout)
editText.setText(arg.item)
editText.addTextChangedListener(this@SubDialogFragment)
setTitle(R.string.add_subscription)
setTitle(R.string.edit_subscription)
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
if (arg.item.isNotEmpty()) setNeutralButton(R.string.delete, listener)
......@@ -219,55 +211,8 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
private val adapter by lazy { SubscriptionAdapter() }
private lateinit var list: RecyclerView
private lateinit var progress: MaterialProgressBar
private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<Any>
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()
}
private lateinit var undoManager: UndoSnackbarManager<URL>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_subscriptions, container, false)
......@@ -278,13 +223,15 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
toolbar.setTitle(R.string.subscriptions)
toolbar.inflateMenu(R.menu.subscription_menu)
toolbar.setOnMenuItemClickListener(this)
SubscriptionService.idle.observe(this) {
toolbar.menu.findItem(R.id.action_update_subscription).isEnabled = it
}
val activity = activity as MainActivity
list = view.findViewById(R.id.list)
list.setOnApplyWindowInsetsListener(MainListListener)
list.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = adapter
progress = view.findViewById(R.id.indeterminate_horizontal_progress)
FastScrollerBuilder(list).useMd2Style().build()
undoManager = UndoSnackbarManager(activity, adapter::undo)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START or ItemTouchHelper.END) {
......@@ -313,7 +260,8 @@ class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener
true
}
R.id.action_update_subscription -> {
fetchServerFromSubscriptions()
val context = requireContext()
context.startService(Intent(context, SubscriptionService::class.java))
true
}
else -> false
......
......@@ -71,7 +71,7 @@ class ConfigActivity : AppCompatActivity() {
}
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) =
if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1])
......
......@@ -43,7 +43,7 @@
android:focusable="true"
android:padding="12dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_cloud_queue"/>
app:srcCompat="@drawable/ic_file_cloud_queue"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/edit"
......
<?xml version="1.0" encoding="utf-8"?>
<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"
android:layout_width="match_parent"
android:layout_height="match_parent"
......@@ -9,18 +8,6 @@
<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
android:layout_width="match_parent"
android:layout_height="0dp"
......
......@@ -7,7 +7,7 @@
android:icon="@drawable/ic_action_description"/>
<item android:id="@+id/subscriptions"
android:title="@string/subscriptions"
android:icon="@drawable/ic_action_cloud_download"/>
android:icon="@drawable/ic_file_cloud_download"/>
<item android:id="@+id/customRules"
android:title="@string/custom_rules"
android:icon="@drawable/ic_action_assignment"/>
......
......@@ -8,7 +8,7 @@
app:showAsAction="always"/>
<item android:title="@string/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"
app:showAsAction="ifRoom"/>
</menu>
......@@ -209,7 +209,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private fun populateProfiles() {
ProfileManager.ensureNotEmpty()
val profiles = ProfileManager.getAllProfiles()!!
val profiles = ProfileManager.getActiveProfiles()!!
fab.value = null
fab.entries = profiles.map { it.formattedName }.toTypedArray()
fab.entryValues = profiles.map { it.id.toString() }.toTypedArray()
......
......@@ -52,7 +52,7 @@ class ProfilesDialogFragment : LeanbackListPreferenceDialogFragmentCompat() {
}
}
private inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
val profiles = ProfileManager.getAllProfiles()!!
val profiles = ProfileManager.getActiveProfiles()!!
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ProfileViewHolder(
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