Skip to content

允许一个分组绑定多条订阅链接,点击一次更新全部订阅并合并节点、自动去重 #1218

Open

Description

@fuoins

描述建议

建议的必要性

允许一个分组绑定多条订阅链接,点击一次更新全部订阅并合并节点、自动去重

Activity

  1. fuoins commented on Sep 11, 2026

    @fuoins
    Author

    diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt
    index a000672..ce56d7e 100644
    --- a/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt
    +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt
    @@ -23,17 +23,26 @@ object SubscriptionUpdater {
    suspend fun reconfigureUpdater() {
    RemoteWorkManager.getInstance(app).cancelUniqueWork(WORK_NAME)

    •    val subscriptions = SagerDatabase.groupDao.subscriptions()
      
    •    val legacySubscriptions = SagerDatabase.groupDao.subscriptions()
             .filter { it.subscription!!.autoUpdate }
      
    •    if (subscriptions.isEmpty()) return
      
    •    val entitySubscriptions = SagerDatabase.subscriptionDao.all()
      
    •        .filter { it.bean?.autoUpdate == true }
      
    •    if (legacySubscriptions.isEmpty() && entitySubscriptions.isEmpty()) return
      
         // PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS
      
    •    var minDelay =
      
    •        subscriptions.minByOrNull { it.subscription!!.autoUpdateDelay }!!.subscription!!.autoUpdateDelay.toLong()
      
    •    val now = System.currentTimeMillis() / 1000L
      
    •    var minInitDelay =
      
    •        subscriptions.minOf { now - it.subscription!!.lastUpdated - (minDelay * 60) }
      
    •    var minDelay = legacySubscriptions.minOfOrNull { it.subscription!!.autoUpdateDelay.toLong() }
      
    •        ?: entitySubscriptions.minOfOrNull { it.bean!!.autoUpdateDelay.toLong() }
      
    •        ?: 1440L
         if (minDelay < 15) minDelay = 15
      
    •    val now = System.currentTimeMillis() / 1000L
      
    •    var minInitDelay = legacySubscriptions.minOfOrNull {
      
    •        now - it.subscription!!.lastUpdated - (minDelay * 60)
      
    •    } ?: Long.MAX_VALUE
      
    •    entitySubscriptions.minOfOrNull {
      
    •        now - it.bean!!.lastUpdated - (minDelay * 60)
      
    •    }?.let { minInitDelay = minOf(minInitDelay, it) }
      
    •    if (minInitDelay == Long.MAX_VALUE) minInitDelay = 0
      
    •    if (minInitDelay < 0) minInitDelay = 0
         if (minInitDelay > 60) minInitDelay = 60
      
         // main process
      

    @@ -62,14 +71,17 @@ object SubscriptionUpdater {
    .setCategory(NotificationCompat.CATEGORY_SERVICE)

         override suspend fun doWork(): Result {
    
    •        var subscriptions =
      
    •        var legacySubscriptions =
                 SagerDatabase.groupDao.subscriptions().filter { it.subscription!!.autoUpdate }
      
    •        var entitySubscriptions =
      
    •            SagerDatabase.subscriptionDao.all().filter { it.bean?.autoUpdate == true }
             if (!DataStore.serviceState.connected) {
                 Logs.d("work: not connected")
      
    •            subscriptions = subscriptions.filter { !it.subscription!!.updateWhenConnectedOnly }
      
    •            legacySubscriptions = legacySubscriptions.filter { !it.subscription!!.updateWhenConnectedOnly }
      
    •            entitySubscriptions = entitySubscriptions.filter { !it.bean!!.updateWhenConnectedOnly }
             }
      
    •        if (subscriptions.isNotEmpty()) for (profile in subscriptions) {
      
    •        if (legacySubscriptions.isNotEmpty()) for (profile in legacySubscriptions) {
                 val subscription = profile.subscription!!
      
                 if (((System.currentTimeMillis() / 1000).toInt() - subscription.lastUpdated) < subscription.autoUpdateDelay * 60) {
      

    @@ -88,6 +100,25 @@ object SubscriptionUpdater {
    GroupUpdater.executeUpdate(profile, false)
    }

    •        if (entitySubscriptions.isNotEmpty()) for (entity in entitySubscriptions) {
      
    •            val bean = entity.bean!!
      
    •            if (((System.currentTimeMillis() / 1000).toInt() - bean.lastUpdated) < bean.autoUpdateDelay * 60) {
      
    •                Logs.d("work: not updating " + entity.displayName())
      
    •                continue
      
    •            }
      
    •            Logs.d("work: updating " + entity.displayName())
      
    •            notification.setContentText(
      
    •                applicationContext.getString(
      
    •                    R.string.subscription_update_message, entity.displayName()
      
    •                )
      
    •            )
      
    •            nm.notify(2, notification.build())
      
    •            GroupUpdater.executeUpdate(entity, false)
      
    •        }
      
    •        nm.cancel(2)
      
             return Result.success()
      

    diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt b/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt
    index b2688f4..e73f1e7 100644
    --- a/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt
    +++ b/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt
    @@ -12,6 +12,10 @@ object GroupManager {

         suspend fun groupRemoved(groupId: Long)
         suspend fun groupUpdated(groupId: Long)
    
    •    suspend fun subscriptionAdd(entity: SubscriptionEntity) {}
      
    •    suspend fun subscriptionUpdated(entity: SubscriptionEntity) {}
      
    •    suspend fun subscriptionRemoved(entity: SubscriptionEntity) {}
      

      }

      interface Interface {
      @@ -98,6 +102,7 @@ object GroupManager {
      }

      suspend fun deleteGroup(groupId: Long) {

    •    SagerDatabase.subscriptionDao.deleteByGroup(groupId)
         SagerDatabase.groupDao.deleteById(groupId)
         SagerDatabase.proxyDao.deleteByGroup(groupId)
         iterator { groupRemoved(groupId) }
      

    @@ -105,10 +110,77 @@ object GroupManager {
    }

     suspend fun deleteGroup(group: List<ProxyGroup>) {
    
    •    SagerDatabase.subscriptionDao.deleteByGroup(group.map { it.id }.toLongArray())
         SagerDatabase.groupDao.deleteGroup(group)
         SagerDatabase.proxyDao.deleteByGroup(group.map { it.id }.toLongArray())
         for (proxyGroup in group) iterator { groupRemoved(proxyGroup.id) }
         SubscriptionUpdater.reconfigureUpdater()
      

      }

    • suspend fun createSubscription(entity: SubscriptionEntity): SubscriptionEntity {

    •    entity.userOrder = SagerDatabase.subscriptionDao.nextOrder() ?: 1
      
    •    entity.bean?.applyDefaultValues()
      
    •    entity.id = SagerDatabase.subscriptionDao.create(entity)
      
    •    iterator { subscriptionAdd(entity) }
      
    •    SubscriptionUpdater.reconfigureUpdater()
      
    •    return entity
      
    • }

    • suspend fun updateSubscription(entity: SubscriptionEntity) {

    •    entity.bean?.applyDefaultValues()
      
    •    SagerDatabase.subscriptionDao.update(entity)
      
    •    iterator { subscriptionUpdated(entity) }
      
    •    SubscriptionUpdater.reconfigureUpdater()
      
    • }

    • suspend fun deleteSubscription(entity: SubscriptionEntity) {

    •    SagerDatabase.subscriptionDao.delete(entity)
      
    •    SagerDatabase.proxyDao.deleteBySubscription(entity.id)
      
    •    iterator { subscriptionRemoved(entity) }
      
    •    SubscriptionUpdater.reconfigureUpdater()
      
    • }

    • /**

    • * 把 source 分组合并进 target 分组:
      
    • * 1. source 上挂的订阅实体全部改指向 target,其节点随之移动;
      
    • * 2. source 自己名下的节点(subscriptionId==0):
      
    • *    - 若 source 是旧式订阅分组,则为其创建订阅实体,节点挂到该实体下(保留后续可更新);
      
    • *    - 否则作为手动节点直接移入 target;
      
    • * 3. 删除 source 分组。
      
    • */
      
    • suspend fun mergeGroup(sourceId: Long, targetId: Long) {

    •    if (sourceId == targetId) return
      
    •    val source = SagerDatabase.groupDao.getById(sourceId) ?: return
      
    •    for (entity in SagerDatabase.subscriptionDao.getByGroup(sourceId)) {
      
    •        val nodes = SagerDatabase.proxyDao.getByGroupAndSubscription(sourceId, entity.id)
      
    •        entity.groupId = targetId
      
    •        SagerDatabase.subscriptionDao.update(entity)
      
    •        for (node in nodes) {
      
    •            node.groupId = targetId
      
    •            node.userOrder = SagerDatabase.proxyDao.nextOrder(targetId) ?: 1
      
    •            SagerDatabase.proxyDao.updateProxy(node)
      
    •        }
      
    •    }
      
    •    val owned = SagerDatabase.proxyDao.getByGroup(sourceId).filter { it.subscriptionId == 0L }
      
    •    if (owned.isNotEmpty()) {
      
    •        var ownerSubscriptionId = 0L
      
    •        if (source.type == GroupType.SUBSCRIPTION && source.subscription != null) {
      
    •            ownerSubscriptionId = createSubscription(
      
    •                SubscriptionEntity(name = source.name, groupId = targetId, bean = source.subscription)
      
    •            ).id
      
    •        }
      
    •        for (node in owned) {
      
    •            node.groupId = targetId
      
    •            node.subscriptionId = ownerSubscriptionId
      
    •            node.userOrder = SagerDatabase.proxyDao.nextOrder(targetId) ?: 1
      
    •            SagerDatabase.proxyDao.updateProxy(node)
      
    •        }
      
    •    }
      
    •    deleteGroup(sourceId)
      
    •    postUpdate(targetId)
      
    • }

    }
    \ No newline at end of file
    diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
    index b975695..966131f 100644
    --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
    +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
    @@ -46,6 +46,7 @@ import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity
    data class ProxyEntity(
    @PrimaryKey(autoGenerate = true) var id: Long = 0L,
    var groupId: Long = 0L,

    • @ColumnInfo(defaultValue = "0") var subscriptionId: Long = 0L,
      var type: Int = 0,
      var userOrder: Long = 0L,
      var tx: Long = 0L,
      @@ -119,10 +120,11 @@ data class ProxyEntity(
      }

      override fun serializeToBuffer(output: ByteBufferOutput) {

    •    output.writeInt(0)
      
    •    output.writeInt(1)
      
         output.writeLong(id)
         output.writeLong(groupId)
      
    •    output.writeLong(subscriptionId)
         output.writeInt(type)
         output.writeLong(userOrder)
         output.writeLong(tx)
      

    @@ -144,6 +146,7 @@ data class ProxyEntity(

         id = input.readLong()
         groupId = input.readLong()
    
    •    if (version >= 1) subscriptionId = input.readLong()
         type = input.readInt()
         userOrder = input.readLong()
         tx = input.readLong()
      

    @@ -491,6 +494,15 @@ data class ProxyEntity(
    @query("SELECT * FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder")
    fun getByGroup(groupId: Long): List

    •    @Query("SELECT * FROM proxy_entities WHERE groupId = :groupId AND subscriptionId = :subscriptionId ORDER BY userOrder")
      
    •    fun getByGroupAndSubscription(groupId: Long, subscriptionId: Long): List<ProxyEntity>
      
    •    @Query("DELETE FROM proxy_entities WHERE subscriptionId = :subscriptionId")
      
    •    fun deleteBySubscription(subscriptionId: Long)
      
    •    @Query("DELETE FROM proxy_entities WHERE groupId = :groupId AND subscriptionId = :subscriptionId")
      
    •    fun deleteByGroupAndSubscription(groupId: Long, subscriptionId: Long)
      
    •    @Query("SELECT * FROM proxy_entities WHERE id in (:proxyIds)")
         fun getEntities(proxyIds: List<Long>): List<ProxyEntity>
      

    diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
    index ececc10..42c3412 100644
    --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
    +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
    @@ -15,12 +15,13 @@ import kotlinx.coroutines.GlobalScope
    import kotlinx.coroutines.launch

    @database(

    • entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class],
    • version = 6,
    • entities = [ProxyGroup::class, ProxyEntity::class, SubscriptionEntity::class, RuleEntity::class],
    • version = 7,
      autoMigrations = [
      AutoMigration(from = 3, to = 4),
      AutoMigration(from = 4, to = 5),
    •    AutoMigration(from = 5, to = 6)
      
    •    AutoMigration(from = 5, to = 6),
      
    •    AutoMigration(from = 6, to = 7)
      

      ]
      )
      @TypeConverters(value = [KryoConverters::class, GsonConverters::class])
      @@ -44,12 +45,14 @@ abstract class SagerDatabase : RoomDatabase() {

         val groupDao get() = instance.groupDao()
         val proxyDao get() = instance.proxyDao()
      
    •    val subscriptionDao get() = instance.subscriptionDao()
         val rulesDao get() = instance.rulesDao()
      

      }

      abstract fun groupDao(): ProxyGroup.Dao
      abstract fun proxyDao(): ProxyEntity.Dao

    • abstract fun subscriptionDao(): SubscriptionEntity.Dao
      abstract fun rulesDao(): RuleEntity.Dao

    }
    diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SubscriptionEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SubscriptionEntity.kt
    new file mode 100644
    index 0000000..8001d6b
    --- /dev/null
    +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SubscriptionEntity.kt
    @@ -0,0 +1,58 @@
    +package io.nekohasekai.sagernet.database
    +
    +import androidx.room.*
    +
    +/**

      • 额外订阅(合并到某个分组里的订阅)。
      • 每个订阅拥有独立的 id,节点通过 ProxyEntity.subscriptionId 关联到该订阅,
      • 这样更新单个订阅时只替换它自己的节点,不影响同组其他订阅/手动节点。
    • */
      +@entity(tableName = "subscriptions")
      +data class SubscriptionEntity(

    • @PrimaryKey(autoGenerate = true) var id: Long = 0L,

    • var userOrder: Long = 0L,

    • var name: String? = null,

    • @ColumnInfo(defaultValue = "0") var groupId: Long = 0L,

    • var bean: SubscriptionBean? = null
      +) {

    • fun displayName(): String {

    •    return name?.takeIf { it.isNotBlank() }
      
    •        ?: bean?.link?.takeIf { it.isNotBlank() }
      
    •        ?: "Subscription #$id"
      
    • }

    • @androidx.room.Dao

    • interface Dao {

    •    @Query("SELECT * FROM subscriptions ORDER BY userOrder")
      
    •    fun all(): List<SubscriptionEntity>
      
    •    @Query("SELECT * FROM subscriptions WHERE groupId = :groupId ORDER BY userOrder")
      
    •    fun getByGroup(groupId: Long): List<SubscriptionEntity>
      
    •    @Query("SELECT * FROM subscriptions WHERE id = :id")
      
    •    fun getById(id: Long): SubscriptionEntity?
      
    •    @Query("SELECT COUNT(*) FROM subscriptions WHERE groupId = :groupId")
      
    •    fun countByGroup(groupId: Long): Long
      
    •    @Query("SELECT MAX(userOrder) + 1 FROM subscriptions")
      
    •    fun nextOrder(): Long?
      
    •    @Insert
      
    •    fun create(entity: SubscriptionEntity): Long
      
    •    @Update
      
    •    fun update(entity: SubscriptionEntity)
      
    •    @Delete
      
    •    fun delete(entity: SubscriptionEntity)
      
    •    @Query("DELETE FROM subscriptions WHERE groupId = :groupId")
      
    •    fun deleteByGroup(groupId: Long)
      
    •    @Query("DELETE FROM subscriptions WHERE groupId IN (:groupIds)")
      
    •    fun deleteByGroup(groupIds: LongArray)
      
    • }
      +}
      diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt
      index 426e3d7..41a3db5 100644
      --- a/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt
      +++ b/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt
      @@ -4,7 +4,9 @@ import io.nekohasekai.sagernet.*
      import io.nekohasekai.sagernet.database.DataStore
      import io.nekohasekai.sagernet.database.GroupManager
      import io.nekohasekai.sagernet.database.ProxyGroup
      +import io.nekohasekai.sagernet.database.SagerDatabase
      import io.nekohasekai.sagernet.database.SubscriptionBean
      +import io.nekohasekai.sagernet.database.SubscriptionEntity
      import io.nekohasekai.sagernet.fmt.AbstractBean
      import io.nekohasekai.sagernet.fmt.http.HttpBean
      import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
      @@ -130,6 +132,45 @@ abstract class GroupUpdater {
      }
      }

    •    fun startUpdate(entity: SubscriptionEntity, byUser: Boolean) {
      
    •        runOnDefaultDispatcher {
      
    •            executeUpdate(entity, byUser)
      
    •        }
      
    •    }
      
    •    suspend fun executeUpdate(entity: SubscriptionEntity, byUser: Boolean): Boolean {
      
    •        return coroutineScope {
      
    •            val bean = entity.bean ?: return@coroutineScope false
      
    •            val key = -entity.id
      
    •            if (!updating.add(key)) cancel()
      
    •            val targetGroup = SagerDatabase.groupDao.getById(entity.groupId)
      
    •            val connected = DataStore.serviceState.connected
      
    •            val userInterface = GroupManager.userInterface
      
    •            GroupManager.postReload(entity.groupId)
      
    •            if (byUser && (bean.link?.startsWith("http://") == true || bean.updateWhenConnectedOnly) && !connected) {
      
    •                if (userInterface == null || !userInterface.confirm(app.getString(R.string.update_subscription_warning))) {
      
    •                    finishUpdate(key, entity.groupId)
      
    •                    cancel()
      
    •                    return@coroutineScope true
      
    •                }
      
    •            }
      
    •            try {
      
    •                RawUpdater.doUpdate(entity, userInterface, byUser)
      
    •                true
      
    •            } catch (e: Throwable) {
      
    •                Logs.w(e)
      
    •                if (targetGroup != null) {
      
    •                    userInterface?.onUpdateFailure(targetGroup, e.readableMessage)
      
    •                }
      
    •                finishUpdate(key, entity.groupId)
      
    •                false
      
    •            }
      
    •        }
      
    •    }
      
    •    suspend fun executeUpdate(proxyGroup: ProxyGroup, byUser: Boolean): Boolean {
             return coroutineScope {
                 if (!updating.add(proxyGroup.id)) cancel()
      

    @@ -161,9 +202,13 @@ abstract class GroupUpdater {

         suspend fun finishUpdate(proxyGroup: ProxyGroup) {
    
    •        updating.remove(proxyGroup.id)
      
    •        progress.remove(proxyGroup.id)
      
    •        GroupManager.postUpdate(proxyGroup)
      
    •        finishUpdate(proxyGroup.id, proxyGroup.id)
      
    •    }
      
    •    suspend fun finishUpdate(key: Long, groupId: Long) {
      
    •        updating.remove(key)
      
    •        progress.remove(groupId)
      
    •        GroupManager.postUpdate(groupId)
         }
      

      }
      diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
      index a759799..d2cd221 100644
      --- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
      +++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
      @@ -1,6 +1,7 @@
      package io.nekohasekai.sagernet.group

    import android.annotation.SuppressLint
    +import io.nekohasekai.sagernet.GroupType
    import io.nekohasekai.sagernet.R
    import io.nekohasekai.sagernet.database.*
    import io.nekohasekai.sagernet.fmt.AbstractBean
    @@ -44,6 +45,43 @@ object RawUpdater : GroupUpdater() {
    userInterface: GroupManager.Interface?,
    byUser: Boolean
    ) {

    •    doUpdateCore(
      
    •        targetGroupId = proxyGroup.id,
      
    •        subscriptionId = 0L,
      
    •        subscription = subscription,
      
    •        groupRef = proxyGroup,
      
    •        entityRef = null,
      
    •        userInterface = userInterface,
      
    •        byUser = byUser
      
    •    )
      
    • }

    • suspend fun doUpdate(

    •    entity: SubscriptionEntity,
      
    •    userInterface: GroupManager.Interface?,
      
    •    byUser: Boolean
      
    • ) {

    •    val subscription = entity.bean ?: error("Null subscription")
      
    •    doUpdateCore(
      
    •        targetGroupId = entity.groupId,
      
    •        subscriptionId = entity.id,
      
    •        subscription = subscription,
      
    •        groupRef = null,
      
    •        entityRef = entity,
      
    •        userInterface = userInterface,
      
    •        byUser = byUser
      
    •    )
      
    • }

    • private suspend fun doUpdateCore(

    •    targetGroupId: Long,
      
    •    subscriptionId: Long,
      
    •    subscription: SubscriptionBean,
      
    •    groupRef: ProxyGroup?,
      
    •    entityRef: SubscriptionEntity?,
      
    •    userInterface: GroupManager.Interface?,
      
    •    byUser: Boolean
      
    • ) {

      val link = subscription.link
      var proxies: List<AbstractBean>
      

    @@ -76,12 +114,13 @@ object RawUpdater : GroupUpdater() {
    Util.getStringBox(response.getHeader("Subscription-Userinfo"))

             // 修改默认名字
    
    •        if (proxyGroup.name?.startsWith("Subscription #") == true) {
      
    •        val currentName = groupRef?.name ?: entityRef?.name
      
    •        if (currentName?.startsWith("Subscription #") == true) {
                 var remoteName = Util.getStringBox(response.getHeader("content-disposition"))
                 if (remoteName.isNotBlank()) {
                     remoteName = Util.decodeFilename(remoteName)
                     if (remoteName.isNotBlank()) {
      
    •                    proxyGroup.name = remoteName
      
    •                    if (groupRef != null) groupRef.name = remoteName else entityRef?.name = remoteName
                     }
                 }
             }
      

    @@ -102,9 +141,13 @@ object RawUpdater : GroupUpdater() {
    }
    proxies = proxiesMap.values.toList()

    •    if (subscription.forceResolve) forceResolve(proxies, proxyGroup.id)
      
    •    if (subscription.forceResolve) forceResolve(proxies, targetGroupId)
      
    •    val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
      
    •    val exists = if (subscriptionId == 0L) {
      
    •        SagerDatabase.proxyDao.getByGroup(targetGroupId).filter { it.subscriptionId == 0L }
      
    •    } else {
      
    •        SagerDatabase.proxyDao.getByGroupAndSubscription(targetGroupId, subscriptionId)
      
    •    }
         val duplicate = ArrayList<String>()
         if (subscription.deduplication) {
             Logs.d("Before deduplication: ${proxies.size}")
      

    @@ -190,7 +233,7 @@ object RawUpdater : GroupUpdater() {
    changed++
    SagerDatabase.proxyDao.addProxy(
    ProxyEntity(

    •                    groupId = proxyGroup.id, userOrder = userOrder
      
    •                    groupId = targetGroupId, subscriptionId = subscriptionId, userOrder = userOrder
                     ).apply {
                         putBean(bean)
                     })
      

    @@ -208,18 +251,30 @@ object RawUpdater : GroupUpdater() {
    Logs.d("Deleted profiles: $it")
    }

    •    val existCount = SagerDatabase.proxyDao.countByGroup(proxyGroup.id).toInt()
      
    •    val existCount = if (subscriptionId == 0L) {
      
    •        SagerDatabase.proxyDao.getByGroup(targetGroupId).count { it.subscriptionId == 0L }
      
    •    } else {
      
    •        SagerDatabase.proxyDao.getByGroupAndSubscription(targetGroupId, subscriptionId).size
      
    •    }
      
         if (existCount != proxies.size) {
             Logs.e("Exist profiles: $existCount, new profiles: ${proxies.size}")
         }
      
         subscription.lastUpdated = (System.currentTimeMillis() / 1000).toInt()
      
    •    SagerDatabase.groupDao.updateGroup(proxyGroup)
      
    •    finishUpdate(proxyGroup)
      
    •    if (entityRef != null) {
      
    •        SagerDatabase.subscriptionDao.update(entityRef)
      
    •    } else {
      
    •        SagerDatabase.groupDao.updateGroup(groupRef!!)
      
    •    }
      
    •    finishUpdate(if (entityRef != null) -entityRef.id else targetGroupId, targetGroupId)
      
    •    val resultGroup = groupRef ?: SagerDatabase.groupDao.getById(targetGroupId) ?: ProxyGroup().apply {
      
    •        name = entityRef?.name
      
    •        type = GroupType.SUBSCRIPTION
      
    •    }
         userInterface?.onUpdateSuccess(
      
    •        proxyGroup, changed, added, updated, deleted, duplicate, byUser
      
    •        resultGroup, changed, added, updated, deleted, duplicate, byUser
         )
      
      }

    diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
    index 0540910..5f063d8 100644
    --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
    +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
    @@ -447,12 +447,20 @@ class ConfigurationFragment @jvmoverloads constructor(

             R.id.action_update_subscription -> {
                 val group = DataStore.currentGroup()
    
    •            if (group.type != GroupType.SUBSCRIPTION) {
      
    •                snackbar(R.string.group_not_subscription).show()
      
    •                Logs.e("onMenuItemClick: Group(${group.displayName()}) is not subscription")
      
    •            } else {
      
    •                runOnLifecycleDispatcher {
      
    •                    GroupUpdater.startUpdate(group, true)
      
    •            runOnLifecycleDispatcher {
      
    •                val hasSubs = SagerDatabase.subscriptionDao.countByGroup(group.id) > 0
      
    •                if (group.type != GroupType.SUBSCRIPTION && !hasSubs) {
      
    •                    onMainDispatcher {
      
    •                        snackbar(R.string.group_not_subscription).show()
      
    •                    }
      
    •                    Logs.e("onMenuItemClick: Group(${group.displayName()}) is not subscription")
      
    •                } else {
      
    •                    if (group.type == GroupType.SUBSCRIPTION && group.subscription != null) {
      
    •                        GroupUpdater.startUpdate(group, true)
      
    •                    }
      
    •                    for (entity in SagerDatabase.subscriptionDao.getByGroup(group.id)) {
      
    •                        GroupUpdater.startUpdate(entity, true)
      
    •                    }
                     }
                 }
             }
      

    diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
    index 840234b..46c39df 100644
    --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
    +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
    @@ -2,11 +2,17 @@ package io.nekohasekai.sagernet.ui

    import android.content.Intent
    import android.os.Bundle
    +import android.text.TextUtils
    import android.text.format.Formatter
    import android.view.MenuItem
    import android.view.View
    import android.view.ViewGroup
    +import android.widget.Button
    +import android.widget.EditText
    import android.widget.LinearLayout
    +import android.widget.ScrollView
    +import android.widget.TextView
    +import com.google.android.material.switchmaterial.SwitchMaterial
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.appcompat.widget.PopupMenu
    import androidx.appcompat.widget.Toolbar
    @@ -122,6 +128,10 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
    .forEach {
    GroupUpdater.startUpdate(it, true)
    }

    •                    SagerDatabase.subscriptionDao.all()
      
    •                        .forEach {
      
    •                            GroupUpdater.startUpdate(it, true)
      
    •                        }
                     }
                     .setNegativeButton(R.string.no, null)
                     .show()
      

    @@ -300,6 +310,18 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
    }
    }

    •    override suspend fun subscriptionAdd(entity: SubscriptionEntity) {
      
    •        reload()
      
    •    }
      
    •    override suspend fun subscriptionUpdated(entity: SubscriptionEntity) {
      
    •        reload()
      
    •    }
      
    •    override suspend fun subscriptionRemoved(entity: SubscriptionEntity) {
      
    •        reload()
      
    •    }
      
    • }

      override fun onDestroy() {
      @@ -372,6 +394,18 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
      .setNegativeButton(android.R.string.cancel, null)
      .show()
      }

    •            R.id.action_add_subscription -> {
      
    •                showAddSubscriptionDialog(proxyGroup)
      
    •            }
      
    •            R.id.action_manage_subscriptions -> {
      
    •                showManageSubscriptionsDialog(proxyGroup)
      
    •            }
      
    •            R.id.action_merge_to_group -> {
      
    •                showMergeGroupDialog(proxyGroup)
      
    •            }
             }
      
             return true
      

    @@ -394,7 +428,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
    }

             updateButton.setOnClickListener {
    
    •            GroupUpdater.startUpdate(proxyGroup, true)
      
    •            updateGroupSubscriptions(proxyGroup)
             }
      
             optionsButton.setOnClickListener {
      

    @@ -406,8 +440,16 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
    if (proxyGroup.type != GroupType.SUBSCRIPTION) {
    popup.menu.removeItem(R.id.action_share_subscription)
    }

    •            popup.setOnMenuItemClickListener(this)
      
    •            popup.show()
      
    •            runOnDefaultDispatcher {
      
    •                val hasSubs = SagerDatabase.subscriptionDao.countByGroup(proxyGroup.id) > 0
      
    •                onMainDispatcher {
      
    •                    if (!hasSubs) {
      
    •                        popup.menu.removeItem(R.id.action_manage_subscriptions)
      
    •                    }
      
    •                    popup.setOnMenuItemClickListener(this@GroupHolder)
      
    •                    popup.show()
      
    •                }
      
    •            }
             }
      
             if (proxyGroup.id in GroupUpdater.updating) {
      

    @@ -513,9 +555,20 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),

             runOnDefaultDispatcher {
                 val size = SagerDatabase.proxyDao.countByGroup(group.id)
    
    •            val subCount = SagerDatabase.subscriptionDao.countByGroup(group.id)
                 onMainDispatcher {
      
    •                @Suppress("DEPRECATION") when (group.type) {
      
    •                    GroupType.BASIC -> {
      
    •                if (proxyGroup.id !in GroupUpdater.updating) {
      
    •                    updateButton.isInvisible =
      
    •                        group.type != GroupType.SUBSCRIPTION && subCount == 0L
      
    •                }
      
    •                @Suppress("DEPRECATION") when {
      
    •                    subCount > 0L -> {
      
    •                        groupStatus.text = getString(
      
    •                            R.string.group_status_proxies_subscriptions, size, subCount
      
    •                        )
      
    •                    }
      
    •                    group.type == GroupType.BASIC -> {
                             if (size == 0L) {
                                 groupStatus.setText(R.string.group_status_empty)
                             } else {
      

    @@ -523,7 +576,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
    }
    }

    •                    GroupType.SUBSCRIPTION -> {
      
    •                    group.type == GroupType.SUBSCRIPTION -> {
                             groupStatus.text = if (size == 0L) {
                                 getString(R.string.group_status_empty_subscription)
                             } else {
      

    @@ -542,6 +595,221 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
    }

         }
    
    •    // ===== 多订阅支持:一个分组绑定多个订阅 =====
      
    •    private fun updateGroupSubscriptions(group: ProxyGroup) {
      
    •        runOnDefaultDispatcher {
      
    •            if (group.type == GroupType.SUBSCRIPTION && group.subscription != null) {
      
    •                GroupUpdater.startUpdate(group, true)
      
    •            }
      
    •            for (entity in SagerDatabase.subscriptionDao.getByGroup(group.id)) {
      
    •                GroupUpdater.startUpdate(entity, true)
      
    •            }
      
    •        }
      
    •    }
      
    •    private fun showAddSubscriptionDialog(group: ProxyGroup) {
      
    •        val view = layoutInflater.inflate(R.layout.dialog_add_subscription, null)
      
    •        val nameInput = view.findViewById<EditText>(R.id.subscription_name)
      
    •        val linkInput = view.findViewById<EditText>(R.id.subscription_link)
      
    •        val autoUpdateSwitch = view.findViewById<SwitchMaterial>(R.id.subscription_auto_update)
      
    •        MaterialAlertDialogBuilder(requireContext())
      
    •            .setTitle(R.string.add_subscription_to_group)
      
    •            .setView(view)
      
    •            .setPositiveButton(R.string.yes) { _, _ ->
      
    •                val link = linkInput.text?.toString()?.trim().orEmpty()
      
    •                if (link.isBlank()) {
      
    •                    snackbar(R.string.subscription_link_empty).show()
      
    •                    return@setPositiveButton
      
    •                }
      
    •                val name = nameInput.text?.toString()?.trim().orEmpty()
      
    •                val bean = SubscriptionBean().applyDefaultValues().apply {
      
    •                    this.link = link
      
    •                    autoUpdate = autoUpdateSwitch.isChecked
      
    •                }
      
    •                val entity = SubscriptionEntity(
      
    •                    name = name.ifBlank { null },
      
    •                    groupId = group.id,
      
    •                    bean = bean
      
    •                )
      
    •                runOnDefaultDispatcher {
      
    •                    val created = GroupManager.createSubscription(entity)
      
    •                    GroupUpdater.startUpdate(created, true)
      
    •                }
      
    •            }
      
    •            .setNegativeButton(android.R.string.cancel, null)
      
    •            .show()
      
    •    }
      
    •    private fun showManageSubscriptionsDialog(group: ProxyGroup) {
      
    •        runOnDefaultDispatcher {
      
    •            val entities = SagerDatabase.subscriptionDao.getByGroup(group.id)
      
    •            onMainDispatcher {
      
    •                if (entities.isEmpty()) {
      
    •                    snackbar(R.string.no_subscriptions).show()
      
    •                    return@onMainDispatcher
      
    •                }
      
    •                val scroll = ScrollView(requireContext())
      
    •                val container = LinearLayout(requireContext()).apply {
      
    •                    orientation = LinearLayout.VERTICAL
      
    •                    setPadding(dp2px(20), dp2px(12), dp2px(20), 0)
      
    •                }
      
    •                scroll.addView(container)
      
    •                val dialog = MaterialAlertDialogBuilder(requireContext())
      
    •                    .setTitle(R.string.manage_subscriptions)
      
    •                    .setView(scroll)
      
    •                    .setPositiveButton(android.R.string.ok, null)
      
    •                    .create()
      
    •                for (entity in entities) {
      
    •                    container.addView(buildSubscriptionRow(entity) { dialog.dismiss() })
      
    •                }
      
    •                dialog.show()
      
    •            }
      
    •        }
      
    •    }
      
    •    private fun buildSubscriptionRow(entity: SubscriptionEntity, dismiss: () -> Unit): View {
      
    •        val row = LinearLayout(requireContext()).apply {
      
    •            orientation = LinearLayout.VERTICAL
      
    •            setPadding(0, dp2px(8), 0, dp2px(8))
      
    •        }
      
    •        row.addView(TextView(requireContext()).apply {
      
    •            text = entity.displayName()
      
    •            textSize = 15f
      
    •            setTextColor(0xFF222222.toInt())
      
    •        })
      
    •        row.addView(TextView(requireContext()).apply {
      
    •            text = entity.bean?.link.orEmpty()
      
    •            maxLines = 1
      
    •            ellipsize = TextUtils.TruncateAt.MIDDLE
      
    •            textSize = 12f
      
    •            setTextColor(0xFF888888.toInt())
      
    •        })
      
    •        val lastUpdated = entity.bean?.lastUpdated ?: 0
      
    •        row.addView(TextView(requireContext()).apply {
      
    •            text = if (lastUpdated > 0) {
      
    •                getString(R.string.subscription_updated_at, subscriptionTimeText(lastUpdated))
      
    •            } else {
      
    •                getString(R.string.subscription_never_updated)
      
    •            }
      
    •            textSize = 12f
      
    •            setTextColor(0xFF666666.toInt())
      
    •        })
      
    •        val buttons = LinearLayout(requireContext()).apply {
      
    •            orientation = LinearLayout.HORIZONTAL
      
    •            addView(Button(requireContext()).apply {
      
    •                text = getString(R.string.update_subscription)
      
    •                isAllCaps = false
      
    •                setOnClickListener {
      
    •                    dismiss()
      
    •                    runOnDefaultDispatcher {
      
    •                        GroupUpdater.startUpdate(entity, true)
      
    •                    }
      
    •                }
      
    •            })
      
    •            addView(Button(requireContext()).apply {
      
    •                text = getString(R.string.edit_subscription)
      
    •                isAllCaps = false
      
    •                setOnClickListener {
      
    •                    dismiss()
      
    •                    showEditSubscriptionDialog(entity)
      
    •                }
      
    •            })
      
    •            addView(Button(requireContext()).apply {
      
    •                text = getString(R.string.delete_subscription)
      
    •                isAllCaps = false
      
    •                setOnClickListener {
      
    •                    dismiss()
      
    •                    MaterialAlertDialogBuilder(requireContext())
      
    •                        .setTitle(R.string.delete_subscription)
      
    •                        .setMessage(R.string.delete_subscription_confirm)
      
    •                        .setPositiveButton(R.string.yes) { _, _ ->
      
    •                            runOnDefaultDispatcher {
      
    •                                GroupManager.deleteSubscription(entity)
      
    •                            }
      
    •                        }
      
    •                        .setNegativeButton(android.R.string.cancel, null)
      
    •                        .show()
      
    •                }
      
    •            })
      
    •        }
      
    •        row.addView(buttons)
      
    •        return row
      
    •    }
      
    •    private fun showEditSubscriptionDialog(entity: SubscriptionEntity) {
      
    •        val view = layoutInflater.inflate(R.layout.dialog_add_subscription, null)
      
    •        val nameInput = view.findViewById<EditText>(R.id.subscription_name)
      
    •        val linkInput = view.findViewById<EditText>(R.id.subscription_link)
      
    •        val autoUpdateSwitch = view.findViewById<SwitchMaterial>(R.id.subscription_auto_update)
      
    •        nameInput.setText(entity.name ?: "")
      
    •        linkInput.setText(entity.bean?.link ?: "")
      
    •        autoUpdateSwitch.isChecked = entity.bean?.autoUpdate == true
      
    •        MaterialAlertDialogBuilder(requireContext())
      
    •            .setTitle(R.string.edit_subscription)
      
    •            .setView(view)
      
    •            .setPositiveButton(R.string.save) { _, _ ->
      
    •                val link = linkInput.text?.toString()?.trim().orEmpty()
      
    •                if (link.isBlank()) {
      
    •                    snackbar(R.string.subscription_link_empty).show()
      
    •                    return@setPositiveButton
      
    •                }
      
    •                val bean = entity.bean ?: SubscriptionBean().applyDefaultValues()
      
    •                bean.link = link
      
    •                bean.autoUpdate = autoUpdateSwitch.isChecked
      
    •                entity.name = nameInput.text?.toString()?.trim()?.takeIf { it.isNotBlank() }
      
    •                entity.bean = bean
      
    •                runOnDefaultDispatcher {
      
    •                    GroupManager.updateSubscription(entity)
      
    •                }
      
    •            }
      
    •            .setNegativeButton(android.R.string.cancel, null)
      
    •            .show()
      
    •    }
      
    •    private fun showMergeGroupDialog(source: ProxyGroup) {
      
    •        runOnDefaultDispatcher {
      
    •            val groups = SagerDatabase.groupDao.allGroups()
      
    •                .filter { it.id != source.id && !it.ungrouped }
      
    •            onMainDispatcher {
      
    •                if (groups.isEmpty()) {
      
    •                    snackbar(R.string.no_groups_to_merge).show()
      
    •                    return@onMainDispatcher
      
    •                }
      
    •                val names = groups.map { it.displayName() }.toTypedArray()
      
    •                MaterialAlertDialogBuilder(requireContext())
      
    •                    .setTitle(R.string.merge_to_other_group)
      
    •                    .setItems(names) { _, which ->
      
    •                        val target = groups[which]
      
    •                        MaterialAlertDialogBuilder(requireContext())
      
    •                            .setTitle(R.string.confirm)
      
    •                            .setMessage(
      
    •                                getString(
      
    •                                    R.string.merge_confirm_message,
      
    •                                    source.displayName(),
      
    •                                    target.displayName()
      
    •                                )
      
    •                            )
      
    •                            .setPositiveButton(R.string.yes) { _, _ ->
      
    •                                runOnDefaultDispatcher {
      
    •                                    GroupManager.mergeGroup(source.id, target.id)
      
    •                                }
      
    •                            }
      
    •                            .setNegativeButton(R.string.no, null)
      
    •                            .show()
      
    •                    }
      
    •                    .setNegativeButton(android.R.string.cancel, null)
      
    •                    .show()
      
    •            }
      
    •        }
      
    •    }
      
    •    private fun subscriptionTimeText(lastUpdated: Int): String {
      
    •        val date = Date(lastUpdated * 1000L)
      
    •        val minute = if (date.minutes < 10) "0${date.minutes}" else "${date.minutes}"
      
    •        return "${date.year + 1900}-${date.month + 1}-${date.date} ${date.hours}:$minute"
      
    •    }
      
      }

    }
    \ No newline at end of file
    diff --git a/app/src/main/res/layout/dialog_add_subscription.xml b/app/src/main/res/layout/dialog_add_subscription.xml
    new file mode 100644
    index 0000000..b35f56b
    --- /dev/null
    +++ b/app/src/main/res/layout/dialog_add_subscription.xml
    @@ -0,0 +1,34 @@
    +
    +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    • android:layout_width="match_parent"
    • android:layout_height="wrap_content"
    • android:orientation="vertical"
    • android:paddingStart="12dp"
    • android:paddingEnd="12dp"
    • android:paddingTop="4dp">
    • <EditText
    •    android:id="@+id/subscription_name"
      
    •    android:layout_width="match_parent"
      
    •    android:layout_height="wrap_content"
      
    •    android:hint="@string/subscription_name"
      
    •    android:inputType="text"
      
    •    android:importantForAutofill="no" />
      
    • <EditText
    •    android:id="@+id/subscription_link"
      
    •    android:layout_width="match_parent"
      
    •    android:layout_height="wrap_content"
      
    •    android:layout_marginTop="8dp"
      
    •    android:hint="@string/subscription_link"
      
    •    android:inputType="textUri"
      
    •    android:importantForAutofill="no" />
      
    • <com.google.android.material.switchmaterial.SwitchMaterial
    •    android:id="@+id/subscription_auto_update"
      
    •    android:layout_width="match_parent"
      
    •    android:layout_height="wrap_content"
      
    •    android:layout_marginTop="8dp"
      
    •    android:text="@string/subscription_auto_update" />
      

    +
    diff --git a/app/src/main/res/menu/group_action_menu.xml b/app/src/main/res/menu/group_action_menu.xml
    index f749f3d..a18e253 100644
    --- a/app/src/main/res/menu/group_action_menu.xml
    +++ b/app/src/main/res/menu/group_action_menu.xml
    @@ -24,6 +24,15 @@
    android:title="@string/action_export_file" />

    • <item
    •    android:id="@+id/action_add_subscription"
      
    •    android:title="@string/add_subscription_to_group" />
      
    • <item
    •    android:id="@+id/action_manage_subscriptions"
      
    •    android:title="@string/manage_subscriptions" />
      
    • <item
    •    android:id="@+id/action_merge_to_group"
      
    •    android:title="@string/merge_to_other_group" />
      

      diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
      index a9f0c1c..c9f7c96 100644
      --- a/app/src/main/res/values-zh-rCN/strings.xml
      +++ b/app/src/main/res/values-zh-rCN/strings.xml
      @@ -499,4 +499,21 @@
      最小化
      无法读取已安装的应用。\n这通常是由于系统限制了应用的读取权限(例如某些中国厂商系统)。\n请在系统设置中授予权限。
      打开系统设置
    • 添加订阅到本组
    • 管理订阅
    • 并入其他分组
    • 订阅名称(可选)
    • 订阅链接
    • 自动更新
    • 请输入订阅链接
    • 本组暂无订阅
    • 更新
    • 编辑订阅
    • 删除订阅
    • 确定删除该订阅及其全部节点吗?
    • 没有其他分组可合并
    • 将“%1$s”并入“%2$s”?\n其订阅与节点会移入目标分组,原分组将被删除。
    • %1$d 个节点 · %2$d 个订阅
    • 更新于 %1$s
    • 尚未更新
    \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1995ff0..6664c72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -579,4 +579,21 @@ Minimize Unable to read installed apps.\nThis is usually because the system has restricted app read permissions.\nPlease grant permissions in the system settings. Open System Settings + Add subscription to this group + Manage subscriptions + Merge into another group + Subscription name (optional) + Subscription link + Auto update + Please enter a subscription link + No subscriptions in this group + Update + Edit subscription + Delete subscription + Delete this subscription and all its proxies? + No other groups to merge into + Merge \"%1$s\" into \"%2$s\"?\nIts subscriptions and proxies will be moved into the target group, and this group will be deleted. + %1$d nodes · %2$d subscriptions + Updated %1$s + Never updated \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7d843ab..e76e3a6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,9 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC -Dfile.encoding=UTF-8 +org.gradle.workers.max=2 +kotlin.daemon.jvmargs=-Xmx1024m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/release.keystore b/release.keystore index 91dcf36..bdfc3a7 100644 Binary files a/release.keystore and b/release.keystore differ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions