Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- Permission required to read phone state and detect SIM cards -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

<application
android:name=".NetworkSwitchApplication"
android:allowBackup="true"
Expand Down Expand Up @@ -32,6 +35,13 @@
android:parentActivityName=".presentation.ui.activity.MainActivity"
android:theme="@style/Theme.NetworkSwitch" />

<activity
android:name=".presentation.ui.activity.AboutActivity"
android:exported="false"
android:label="About"
android:parentActivityName=".presentation.ui.activity.SettingsActivity"
android:theme="@style/Theme.NetworkSwitch" />

<activity
android:name=".presentation.ui.activity.NetworkModeConfigActivity"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.supernova.networkswitch.data.repository

import android.telephony.SubscriptionManager
import com.supernova.networkswitch.data.source.NetworkControlDataSource
import com.supernova.networkswitch.data.source.RootNetworkControlDataSource
import com.supernova.networkswitch.data.source.ShizukuNetworkControlDataSource
Expand All @@ -23,14 +24,16 @@ class NetworkControlRepositoryImpl @Inject constructor(

override suspend fun checkCompatibility(method: ControlMethod): CompatibilityState {
val dataSource = getDataSource(method)
val subId = android.telephony.SubscriptionManager.getDefaultDataSubscriptionId()
// Use selected subscription ID for compatibility check
val subId = getEffectiveSubscriptionId()
return dataSource.checkCompatibility(subId)
}

override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? {
return try {
val method = preferencesRepository.getControlMethod()
val dataSource = getDataSource(method)
// Use the provided subId (which should come from getEffectiveSubscriptionId in callers)
dataSource.getCurrentNetworkMode(subId)
} catch (e: Exception) {
null
Expand All @@ -41,6 +44,7 @@ class NetworkControlRepositoryImpl @Inject constructor(
return try {
val method = preferencesRepository.getControlMethod()
val dataSource = getDataSource(method)
// Use the provided subId (which should come from getEffectiveSubscriptionId in callers)
dataSource.setNetworkMode(subId, mode)
Result.success(Unit)
} catch (e: Exception) {
Expand All @@ -55,6 +59,21 @@ class NetworkControlRepositoryImpl @Inject constructor(
rootDataSource.resetConnection()
shizukuDataSource.resetConnection()
}

/**
* Get the effective subscription ID to use for network operations
* Returns the user's selected subscription ID from preferences, or the default if -1
*/
private suspend fun getEffectiveSubscriptionId(): Int {
val selectedSubId = preferencesRepository.getSelectedSubscriptionId()
return if (selectedSubId == -1) {
// User selected "Auto" - use system default
SubscriptionManager.getDefaultDataSubscriptionId()
} else {
// User selected a specific SIM
selectedSubId
}
}

private fun getDataSource(method: ControlMethod): NetworkControlDataSource {
return when (method) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ class PreferencesRepositoryImpl @Inject constructor(
override fun observeToggleModeConfig(): Flow<ToggleModeConfig> {
return preferencesDataSource.observeToggleModeConfig()
}

override suspend fun getSelectedSubscriptionId(): Int {
return preferencesDataSource.getSelectedSubscriptionId()
}

override suspend fun setSelectedSubscriptionId(subscriptionId: Int) {
preferencesDataSource.setSelectedSubscriptionId(subscriptionId)
}

override fun observeSelectedSubscriptionId(): Flow<Int> {
return preferencesDataSource.observeSelectedSubscriptionId()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.supernova.networkswitch.data.repository

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import androidx.core.content.ContextCompat
import com.supernova.networkswitch.domain.model.SimInfo
import com.supernova.networkswitch.domain.repository.SimRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

/**
* Implementation of SimRepository that uses Android's SubscriptionManager
* to detect and retrieve information about available SIM cards
*/
@Singleton
class SimRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context
) : SimRepository {

private val subscriptionManager: SubscriptionManager? by lazy {
context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as? SubscriptionManager
}

override suspend fun getAvailableSimCards(): List<SimInfo> {
// Check if we have the required permission
if (!hasReadPhoneStatePermission()) {
return emptyList()
}

val manager = subscriptionManager ?: return emptyList()

return try {
// Get active subscriptions
val subscriptions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
manager.activeSubscriptionInfoList ?: emptyList()
} else {
emptyList()
}

// Map to SimInfo objects
subscriptions.mapNotNull { subscriptionInfo ->
mapToSimInfo(subscriptionInfo)
}
} catch (e: SecurityException) {
// Permission was revoked or not granted
emptyList()
} catch (e: Exception) {
// Handle other potential errors
emptyList()
}
}

/**
* Check if READ_PHONE_STATE permission is granted
*/
private fun hasReadPhoneStatePermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_PHONE_STATE
) == PackageManager.PERMISSION_GRANTED
}

/**
* Map SubscriptionInfo to SimInfo domain model
*/
private fun mapToSimInfo(subscriptionInfo: SubscriptionInfo): SimInfo? {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
val displayName = buildDisplayName(subscriptionInfo)

SimInfo(
subscriptionId = subscriptionInfo.subscriptionId,
simSlotIndex = subscriptionInfo.simSlotIndex,
displayName = displayName
)
} else {
null
}
} catch (e: Exception) {
null
}
}

/**
* Build a user-friendly display name for the SIM card
*/
private fun buildDisplayName(subscriptionInfo: SubscriptionInfo): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
// Try to get the display name from the subscription
val carrierName = subscriptionInfo.displayName?.toString()
val slotIndex = subscriptionInfo.simSlotIndex

return when {
// If carrier name exists and slot index is valid
!carrierName.isNullOrBlank() && slotIndex >= 0 -> {
"$carrierName (Slot ${slotIndex + 1})"
}
// If only carrier name exists
!carrierName.isNullOrBlank() -> carrierName
// If only slot index is valid
slotIndex >= 0 -> "SIM ${slotIndex + 1}"
// Fallback
else -> "SIM ${subscriptionInfo.subscriptionId}"
}
}
return "Unknown SIM"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ class PreferencesDataSource @Inject constructor(
private val TOGGLE_MODE_A_KEY = intPreferencesKey("toggle_mode_a")
private val TOGGLE_MODE_B_KEY = intPreferencesKey("toggle_mode_b")
private val TOGGLE_NEXT_IS_B_KEY = booleanPreferencesKey("toggle_next_is_b")
private val SELECTED_SUBSCRIPTION_ID_KEY = intPreferencesKey("selected_subscription_id")

private const val DEFAULT_CONTROL_METHOD = "SHIZUKU"

private val DEFAULT_MODE_A = NetworkMode.LTE_ONLY
private val DEFAULT_MODE_B = NetworkMode.NR_ONLY
private const val DEFAULT_NEXT_IS_B = true

// -1 indicates no specific SIM selected (use default)
private const val DEFAULT_SUBSCRIPTION_ID = -1
}

private fun parseControlMethod(methodString: String?): ControlMethod {
Expand Down Expand Up @@ -92,4 +96,33 @@ class PreferencesDataSource @Inject constructor(
ToggleModeConfig(modeA, modeB, nextIsB)
}
}

/**
* Get the selected subscription ID for the SIM card
* Returns -1 if no specific SIM is selected (use default)
*/
suspend fun getSelectedSubscriptionId(): Int {
return dataStore.data.map { preferences ->
preferences[SELECTED_SUBSCRIPTION_ID_KEY] ?: DEFAULT_SUBSCRIPTION_ID
}.first()
}

/**
* Set the selected subscription ID for the SIM card
* Pass -1 to use the default subscription
*/
suspend fun setSelectedSubscriptionId(subscriptionId: Int) {
dataStore.edit { preferences ->
preferences[SELECTED_SUBSCRIPTION_ID_KEY] = subscriptionId
}
}

/**
* Observe changes to the selected subscription ID
*/
fun observeSelectedSubscriptionId(): Flow<Int> {
return dataStore.data.map { preferences ->
preferences[SELECTED_SUBSCRIPTION_ID_KEY] ?: DEFAULT_SUBSCRIPTION_ID
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.supernova.networkswitch.data.repository.NetworkControlRepositoryImpl
import com.supernova.networkswitch.data.repository.PreferencesRepositoryImpl
import com.supernova.networkswitch.data.repository.SimRepositoryImpl
import com.supernova.networkswitch.domain.repository.NetworkControlRepository
import com.supernova.networkswitch.domain.repository.PreferencesRepository
import com.supernova.networkswitch.domain.repository.SimRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -37,6 +39,12 @@ abstract class DataModule {
preferencesRepositoryImpl: PreferencesRepositoryImpl
): PreferencesRepository

@Binds
@Singleton
abstract fun bindSimRepository(
simRepositoryImpl: SimRepositoryImpl
): SimRepository

companion object {
@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,12 @@ sealed class CompatibilityState {
data class Incompatible(val reason: String) : CompatibilityState()
data class PermissionDenied(val method: ControlMethod) : CompatibilityState()
}

/**
* Represents information about a SIM card in the device
*/
data class SimInfo(
val subscriptionId: Int,
val simSlotIndex: Int,
val displayName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.supernova.networkswitch.domain.repository
import com.supernova.networkswitch.domain.model.CompatibilityState
import com.supernova.networkswitch.domain.model.ControlMethod
import com.supernova.networkswitch.domain.model.NetworkMode
import com.supernova.networkswitch.domain.model.SimInfo
import com.supernova.networkswitch.domain.model.ToggleModeConfig
import kotlinx.coroutines.flow.Flow

Expand Down Expand Up @@ -64,4 +65,32 @@ interface PreferencesRepository {
* Observe toggle mode configuration changes
*/
fun observeToggleModeConfig(): Flow<ToggleModeConfig>

/**
* Get the selected subscription ID for the SIM card
* Returns -1 if no specific SIM is selected (use default)
*/
suspend fun getSelectedSubscriptionId(): Int

/**
* Set the selected subscription ID for the SIM card
* Pass -1 to use the default subscription
*/
suspend fun setSelectedSubscriptionId(subscriptionId: Int)

/**
* Observe changes to the selected subscription ID
*/
fun observeSelectedSubscriptionId(): Flow<Int>
}

/**
* Repository interface for SIM card operations
*/
interface SimRepository {
/**
* Get list of all available SIM cards in the device
* Returns empty list if permission is not granted
*/
suspend fun getAvailableSimCards(): List<SimInfo>
}
Loading