forked from PeterCxy/OpenEUICC
Compare commits
11 commits
aed2479044
...
9d18253e44
Author | SHA1 | Date | |
---|---|---|---|
9d18253e44 | |||
6039679693 | |||
5a8d92c3df | |||
55c99831f3 | |||
343dfb43f8 | |||
815d4d4324 | |||
ec334d104a | |||
70f1e00eb4 | |||
bc238c45cd | |||
14ea84c36e | |||
aefa79b18b |
32 changed files with 264 additions and 184 deletions
|
@ -37,6 +37,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
return EuiccChannelImpl(
|
return EuiccChannelImpl(
|
||||||
context.getString(R.string.omapi),
|
context.getString(R.string.omapi),
|
||||||
port,
|
port,
|
||||||
|
intrinsicChannelName = null,
|
||||||
OmapiApduInterface(
|
OmapiApduInterface(
|
||||||
seService!!,
|
seService!!,
|
||||||
port,
|
port,
|
||||||
|
@ -67,6 +68,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
return EuiccChannelImpl(
|
return EuiccChannelImpl(
|
||||||
context.getString(R.string.usb),
|
context.getString(R.string.usb),
|
||||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||||
|
intrinsicChannelName = usbDevice.productName,
|
||||||
UsbApduInterface(
|
UsbApduInterface(
|
||||||
conn,
|
conn,
|
||||||
bulkIn,
|
bulkIn,
|
||||||
|
|
|
@ -16,5 +16,12 @@ interface EuiccChannel {
|
||||||
|
|
||||||
val valid: Boolean
|
val valid: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intrinsic name of this channel. For device-internal SIM slots,
|
||||||
|
* this should be null; for USB readers, this should be the name of
|
||||||
|
* the reader device.
|
||||||
|
*/
|
||||||
|
val intrinsicChannelName: String?
|
||||||
|
|
||||||
fun close()
|
fun close()
|
||||||
}
|
}
|
|
@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
|
||||||
class EuiccChannelImpl(
|
class EuiccChannelImpl(
|
||||||
override val type: String,
|
override val type: String,
|
||||||
override val port: UiccPortInfoCompat,
|
override val port: UiccPortInfoCompat,
|
||||||
|
override val intrinsicChannelName: String?,
|
||||||
apduInterface: ApduInterface,
|
apduInterface: ApduInterface,
|
||||||
verboseLoggingFlow: Flow<Boolean>,
|
verboseLoggingFlow: Flow<Boolean>,
|
||||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||||
|
|
|
@ -31,6 +31,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
||||||
override val lpa: LocalProfileAssistant by lpaDelegate
|
override val lpa: LocalProfileAssistant by lpaDelegate
|
||||||
override val valid: Boolean
|
override val valid: Boolean
|
||||||
get() = channel.valid
|
get() = channel.valid
|
||||||
|
override val intrinsicChannelName: String?
|
||||||
|
get() = channel.intrinsicChannelName
|
||||||
|
|
||||||
override fun close() = channel.close()
|
override fun close() = channel.close()
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,5 @@ interface AppContainer {
|
||||||
val preferenceRepository: PreferenceRepository
|
val preferenceRepository: PreferenceRepository
|
||||||
val uiComponentFactory: UiComponentFactory
|
val uiComponentFactory: UiComponentFactory
|
||||||
val euiccChannelFactory: EuiccChannelFactory
|
val euiccChannelFactory: EuiccChannelFactory
|
||||||
|
val customizableTextProvider: CustomizableTextProvider
|
||||||
}
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
interface CustomizableTextProvider {
|
||||||
|
/**
|
||||||
|
* Explanation string for when no eUICC is found on the device.
|
||||||
|
* This could be different depending on whether the app is privileged or not.
|
||||||
|
*/
|
||||||
|
val noEuiccExplanation: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown when we timed out switching between profiles.
|
||||||
|
*/
|
||||||
|
val profileSwitchingTimeoutMessage: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the name of a logical slot; internal only -- not intended for
|
||||||
|
* other channels such as USB.
|
||||||
|
*/
|
||||||
|
fun formatInternalChannelName(logicalSlotId: Int): String
|
||||||
|
}
|
|
@ -38,4 +38,8 @@ open class DefaultAppContainer(context: Context) : AppContainer {
|
||||||
override val euiccChannelFactory by lazy {
|
override val euiccChannelFactory by lazy {
|
||||||
DefaultEuiccChannelFactory(context)
|
DefaultEuiccChannelFactory(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val customizableTextProvider by lazy {
|
||||||
|
DefaultCustomizableTextProvider(context)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import im.angry.openeuicc.common.R
|
||||||
|
|
||||||
|
open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider {
|
||||||
|
override val noEuiccExplanation: String
|
||||||
|
get() = context.getString(R.string.no_euicc)
|
||||||
|
|
||||||
|
override val profileSwitchingTimeoutMessage: String
|
||||||
|
get() = context.getString(R.string.enable_disable_timeout)
|
||||||
|
|
||||||
|
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||||
|
context.getString(R.string.channel_name_format, logicalSlotId)
|
||||||
|
}
|
|
@ -1,13 +1,18 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -32,6 +37,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
||||||
|
|
||||||
private var logicalSlotId: Int = -1
|
private var logicalSlotId: Int = -1
|
||||||
|
|
||||||
|
data class Item(
|
||||||
|
@StringRes
|
||||||
|
val titleResId: Int,
|
||||||
|
val content: String?,
|
||||||
|
val copiedToastResId: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -41,12 +53,11 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||||
infoList = requireViewById(R.id.recycler_view)
|
infoList = requireViewById<RecyclerView>(R.id.recycler_view).also {
|
||||||
|
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||||
infoList.layoutManager =
|
it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
it.adapter = EuiccInfoAdapter()
|
||||||
infoList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
}
|
||||||
infoList.adapter = EuiccInfoAdapter()
|
|
||||||
|
|
||||||
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||||
|
|
||||||
|
@ -81,29 +92,33 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
|
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
|
||||||
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildPairs).map {
|
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
|
||||||
Pair(getString(it.first), it.second ?: getString(R.string.unknown))
|
|
||||||
}
|
|
||||||
|
|
||||||
swipeRefresh.isRefreshing = false
|
swipeRefresh.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPairs(channel: EuiccChannel) = buildList {
|
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
|
||||||
add(Pair(R.string.euicc_info_access_mode, channel.type))
|
add(Item(R.string.euicc_info_access_mode, channel.type))
|
||||||
add(
|
add(
|
||||||
Pair(
|
Item(
|
||||||
R.string.euicc_info_removable,
|
R.string.euicc_info_removable,
|
||||||
formatByBoolean(channel.port.card.isRemovable, YES_NO)
|
formatByBoolean(channel.port.card.isRemovable, YES_NO)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(Pair(R.string.euicc_info_eid, channel.lpa.eID))
|
add(
|
||||||
|
Item(
|
||||||
|
R.string.euicc_info_eid,
|
||||||
|
channel.lpa.eID,
|
||||||
|
copiedToastResId = R.string.toast_eid_copied
|
||||||
|
)
|
||||||
|
)
|
||||||
channel.lpa.euiccInfo2.let { info ->
|
channel.lpa.euiccInfo2.let { info ->
|
||||||
add(Pair(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
||||||
add(Pair(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
||||||
add(Pair(R.string.euicc_info_pp_version, info?.ppVersion))
|
add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
|
||||||
add(Pair(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
|
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
|
||||||
add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
|
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
|
||||||
}
|
}
|
||||||
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
|
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
|
||||||
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
|
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
|
||||||
|
@ -116,7 +131,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
||||||
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
|
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
|
||||||
else -> R.string.euicc_info_ci_unknown
|
else -> R.string.euicc_info_ci_unknown
|
||||||
}
|
}
|
||||||
add(Pair(R.string.euicc_info_ci_type, getString(resId)))
|
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,15 +147,34 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
||||||
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
|
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
|
||||||
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
|
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
|
||||||
private val content: TextView = root.requireViewById(R.id.euicc_info_content)
|
private val content: TextView = root.requireViewById(R.id.euicc_info_content)
|
||||||
|
private var copiedToastResId: Int? = null
|
||||||
|
|
||||||
fun bind(item: Pair<String, String>) {
|
init {
|
||||||
title.text = item.first
|
root.setOnClickListener {
|
||||||
content.text = item.second
|
if (copiedToastResId != null) {
|
||||||
|
val label = title.text.toString()
|
||||||
|
getSystemService(ClipboardManager::class.java)!!
|
||||||
|
.setPrimaryClip(ClipData.newPlainText(label, content.text))
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@EuiccInfoActivity,
|
||||||
|
copiedToastResId!!,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(item: Item) {
|
||||||
|
copiedToastResId = item.copiedToastResId
|
||||||
|
title.setText(item.titleResId)
|
||||||
|
content.text = item.content ?: getString(R.string.unknown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
|
inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
|
||||||
var euiccInfoItems: List<Pair<String, String>> = listOf()
|
var euiccInfoItems: List<Item> = listOf()
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
set(newVal) {
|
set(newVal) {
|
||||||
field = newVal
|
field = newVal
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.PasswordTransformationMethod
|
import android.text.method.PasswordTransformationMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -261,7 +262,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
invalid = true
|
invalid = true
|
||||||
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
setMessage(R.string.enable_disable_timeout)
|
setMessage(appContainer.customizableTextProvider.profileSwitchingTimeoutMessage)
|
||||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
|
@ -348,7 +349,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
iccid.setOnLongClickListener {
|
iccid.setOnLongClickListener {
|
||||||
requireContext().getSystemService(ClipboardManager::class.java)!!
|
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||||
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text))
|
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text))
|
||||||
Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast
|
||||||
|
.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.view.View
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
@ -17,7 +16,6 @@ import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LogsActivity : AppCompatActivity() {
|
class LogsActivity : AppCompatActivity() {
|
||||||
|
@ -27,15 +25,15 @@ class LogsActivity : AppCompatActivity() {
|
||||||
private lateinit var logStr: String
|
private lateinit var logStr: String
|
||||||
|
|
||||||
private val saveLogs =
|
private val saveLogs =
|
||||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
setupLogSaving(
|
||||||
if (uri == null) return@registerForActivityResult
|
getLogFileName = {
|
||||||
if (!this::logStr.isInitialized) return@registerForActivityResult
|
getString(
|
||||||
contentResolver.openFileDescriptor(uri, "w")?.use {
|
R.string.logs_filename_template,
|
||||||
FileOutputStream(it.fileDescriptor).use { os ->
|
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||||
os.write(logStr.encodeToByteArray())
|
)
|
||||||
}
|
},
|
||||||
}
|
getLogText = { logStr }
|
||||||
}
|
)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
@ -76,9 +74,7 @@ class LogsActivity : AppCompatActivity() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.save -> {
|
R.id.save -> {
|
||||||
saveLogs.launch(getString(R.string.logs_filename_template,
|
saveLogs()
|
||||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
|
||||||
))
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
|
|
@ -163,7 +163,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
// but it could change in the future
|
// but it could change in the future
|
||||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||||
|
|
||||||
val channelName = getString(R.string.channel_name_format, channel.logicalSlotId)
|
val channelName =
|
||||||
|
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
|
||||||
newPages.add(Page(channel.logicalSlotId, channelName) {
|
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||||
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
|
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,15 +4,20 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
|
||||||
class NoEuiccPlaceholderFragment : Fragment() {
|
class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
return inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false)
|
val view = inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false)
|
||||||
|
val textView = view.requireViewById<TextView>(R.id.no_euicc_placeholder)
|
||||||
|
textView.text = appContainer.customizableTextProvider.noEuiccExplanation
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,6 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
@ -14,7 +13,6 @@ import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -60,25 +58,25 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_notifications_download")
|
findPreference<CheckBoxPreference>("pref_notifications_download")
|
||||||
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
|
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_notifications_delete")
|
findPreference<CheckBoxPreference>("pref_notifications_delete")
|
||||||
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
|
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_notifications_switch")
|
findPreference<CheckBoxPreference>("pref_notifications_switch")
|
||||||
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH)
|
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
||||||
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM)
|
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
||||||
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
|
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
findPreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
||||||
?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow, PreferenceKeys.UNFILTERED_PROFILE_LIST)
|
?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_ignore_tls_certificate")
|
findPreference<CheckBoxPreference>("pref_ignore_tls_certificate")
|
||||||
?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow, PreferenceKeys.IGNORE_TLS_CERTIFICATE)
|
?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -99,10 +97,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
|
|
||||||
if (numClicks == 7) {
|
if (numClicks == 7) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
preferenceRepository.updatePreference(
|
preferenceRepository.developerOptionsEnabledFlow.updatePreference(true)
|
||||||
PreferenceKeys.DEVELOPER_OPTIONS_ENABLED,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
lastToast?.cancel()
|
lastToast?.cancel()
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
|
@ -124,14 +119,14 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {
|
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
flow.collect { isChecked = it }
|
flow.collect { isChecked = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
runBlocking {
|
runBlocking {
|
||||||
preferenceRepository.updatePreference(key, newValue as Boolean)
|
flow.updatePreference(newValue as Boolean)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
package im.angry.openeuicc.ui
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import im.angry.openeuicc.common.R
|
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
|
||||||
import im.angry.openeuicc.util.*
|
|
||||||
|
|
||||||
class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "SlotSelectFragment"
|
|
||||||
|
|
||||||
fun newInstance(slotIds: List<Int>, logicalSlotIds: List<Int>, portIds: List<Int>): SlotSelectFragment {
|
|
||||||
return SlotSelectFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putIntArray("slotIds", slotIds.toIntArray())
|
|
||||||
putIntArray("logicalSlotIds", logicalSlotIds.toIntArray())
|
|
||||||
putIntArray("portIds", portIds.toIntArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SlotSelectedListener {
|
|
||||||
fun onSlotSelected(slotId: Int, portId: Int)
|
|
||||||
fun onSlotSelectCancelled()
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var toolbar: Toolbar
|
|
||||||
private lateinit var spinner: Spinner
|
|
||||||
private lateinit var adapter: ArrayAdapter<String>
|
|
||||||
private lateinit var slotIds: IntArray
|
|
||||||
private lateinit var logicalSlotIds: IntArray
|
|
||||||
private lateinit var portIds: IntArray
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val view = inflater.inflate(R.layout.fragment_slot_select, container, false)
|
|
||||||
|
|
||||||
toolbar = view.requireViewById(R.id.toolbar)
|
|
||||||
toolbar.setTitle(R.string.slot_select)
|
|
||||||
toolbar.inflateMenu(R.menu.fragment_slot_select)
|
|
||||||
|
|
||||||
adapter = ArrayAdapter<String>(inflater.context, R.layout.spinner_item)
|
|
||||||
|
|
||||||
spinner = view.requireViewById(R.id.spinner)
|
|
||||||
spinner.adapter = adapter
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
slotIds = requireArguments().getIntArray("slotIds")!!
|
|
||||||
logicalSlotIds = requireArguments().getIntArray("logicalSlotIds")!!
|
|
||||||
portIds = requireArguments().getIntArray("portIds")!!
|
|
||||||
|
|
||||||
logicalSlotIds.forEach { id ->
|
|
||||||
adapter.add(getString(R.string.channel_name_format, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbar.setNavigationOnClickListener {
|
|
||||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
|
||||||
}
|
|
||||||
toolbar.setOnMenuItemClickListener {
|
|
||||||
val slotId = slotIds[spinner.selectedItemPosition]
|
|
||||||
val portId = portIds[spinner.selectedItemPosition]
|
|
||||||
(requireActivity() as SlotSelectedListener).onSlotSelected(slotId, portId)
|
|
||||||
dismiss()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
setWidthPercent(75)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(dialog: DialogInterface) {
|
|
||||||
super.onCancel(dialog)
|
|
||||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,10 +6,8 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||||
|
@ -21,14 +19,15 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
|
||||||
private lateinit var diagnosticTextView: TextView
|
private lateinit var diagnosticTextView: TextView
|
||||||
|
|
||||||
private val saveDiagnostics =
|
private val saveDiagnostics =
|
||||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
setupLogSaving(
|
||||||
if (uri == null) return@registerForActivityResult
|
getLogFileName = {
|
||||||
requireActivity().contentResolver.openFileDescriptor(uri, "w")?.use {
|
getString(
|
||||||
FileOutputStream(it.fileDescriptor).use { os ->
|
R.string.download_wizard_diagnostics_file_template,
|
||||||
os.write(diagnosticTextView.text.toString().encodeToByteArray())
|
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||||
}
|
)
|
||||||
}
|
},
|
||||||
}
|
getLogText = { diagnosticTextView.text.toString() }
|
||||||
|
)
|
||||||
|
|
||||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||||
|
|
||||||
|
@ -41,12 +40,7 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
|
||||||
): View? {
|
): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false)
|
val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false)
|
||||||
view.requireViewById<View>(R.id.download_wizard_diagnostics_save).setOnClickListener {
|
view.requireViewById<View>(R.id.download_wizard_diagnostics_save).setOnClickListener {
|
||||||
saveDiagnostics.launch(
|
saveDiagnostics()
|
||||||
getString(
|
|
||||||
R.string.download_wizard_diagnostics_file_template,
|
|
||||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text)
|
diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text)
|
||||||
return view
|
return view
|
||||||
|
|
|
@ -35,7 +35,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
val eID: String,
|
val eID: String,
|
||||||
val freeSpace: Int,
|
val freeSpace: Int,
|
||||||
val imei: String,
|
val imei: String,
|
||||||
val enabledProfileName: String?
|
val enabledProfileName: String?,
|
||||||
|
val intrinsicChannelName: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
private var loaded = false
|
private var loaded = false
|
||||||
|
@ -61,7 +62,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
setMessage(R.string.profile_download_low_nvram_message)
|
setMessage(R.string.profile_download_low_nvram_message)
|
||||||
setCancelable(true)
|
setCancelable(true)
|
||||||
setPositiveButton(android.R.string.ok, null)
|
setPositiveButton(android.R.string.ok, null)
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +109,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName
|
channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName,
|
||||||
|
channel.intrinsicChannelName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.toList().sortedBy { it.logicalSlotId }
|
}.toList().sortedBy { it.logicalSlotId }
|
||||||
|
@ -177,9 +181,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
}
|
}
|
||||||
|
|
||||||
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
root.context.getString(R.string.usb)
|
item.intrinsicChannelName ?: root.context.getString(R.string.usb)
|
||||||
} else {
|
} else {
|
||||||
root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId)
|
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
|
||||||
}
|
}
|
||||||
eID.text = item.eID
|
eID.text = item.eID
|
||||||
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
|
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
|
||||||
|
|
|
@ -19,7 +19,7 @@ val Context.preferenceRepository: PreferenceRepository
|
||||||
val Fragment.preferenceRepository: PreferenceRepository
|
val Fragment.preferenceRepository: PreferenceRepository
|
||||||
get() = requireContext().preferenceRepository
|
get() = requireContext().preferenceRepository
|
||||||
|
|
||||||
object PreferenceKeys {
|
internal object PreferenceKeys {
|
||||||
// ---- Profile Notifications ----
|
// ---- Profile Notifications ----
|
||||||
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
||||||
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
||||||
|
@ -51,9 +51,22 @@ class PreferenceRepository(private val context: Context) {
|
||||||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||||
|
|
||||||
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): Flow<T> =
|
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
|
||||||
context.dataStore.data.map { it[key] ?: defaultValue }
|
PreferenceFlowWrapper(context, key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) =
|
class PreferenceFlowWrapper<T> private constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val key: Preferences.Key<T>,
|
||||||
|
inner: Flow<T>
|
||||||
|
) : Flow<T> by inner {
|
||||||
|
internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) : this(
|
||||||
|
context,
|
||||||
|
key,
|
||||||
|
context.dataStore.data.map { it[key] ?: defaultValue }
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun updatePreference(value: T) {
|
||||||
context.dataStore.edit { it[key] = value }
|
context.dataStore.edit { it[key] = value }
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,17 +1,23 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultCaller
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
||||||
/**
|
/**
|
||||||
|
@ -69,4 +75,44 @@ fun setupRootViewInsets(view: ViewGroup) {
|
||||||
|
|
||||||
WindowInsetsCompat.CONSUMED
|
WindowInsetsCompat.CONSUMED
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : ActivityResultCaller> T.setupLogSaving(
|
||||||
|
getLogFileName: () -> String,
|
||||||
|
getLogText: () -> String
|
||||||
|
): () -> Unit {
|
||||||
|
val launchSaveIntent =
|
||||||
|
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||||
|
if (uri == null) return@registerForActivityResult
|
||||||
|
|
||||||
|
val context = when (this@setupLogSaving) {
|
||||||
|
is Context -> this@setupLogSaving
|
||||||
|
is Fragment -> requireContext()
|
||||||
|
else -> throw IllegalArgumentException("Must be either Context or Fragment!")
|
||||||
|
}
|
||||||
|
|
||||||
|
context.contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||||
|
FileOutputStream(it.fileDescriptor).use { os ->
|
||||||
|
os.write(getLogText().encodeToByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(context).apply {
|
||||||
|
setMessage(R.string.logs_saved_message)
|
||||||
|
setNegativeButton(R.string.no) { _, _ -> }
|
||||||
|
setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(Intent.createChooser(intent, null))
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
launchSaveIntent.launch(getLogFileName())
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -43,7 +43,6 @@
|
||||||
<string name="download_wizard_back">戻る</string>
|
<string name="download_wizard_back">戻る</string>
|
||||||
<string name="download_wizard_next">次へ</string>
|
<string name="download_wizard_next">次へ</string>
|
||||||
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</string>
|
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</string>
|
||||||
<string name="download_wizard_slot_title">論理スロット %d</string>
|
|
||||||
<string name="download_wizard_slot_type">タイプ:</string>
|
<string name="download_wizard_slot_type">タイプ:</string>
|
||||||
<string name="download_wizard_slot_type_removable">リムーバブル</string>
|
<string name="download_wizard_slot_type_removable">リムーバブル</string>
|
||||||
<string name="download_wizard_slot_type_internal">内部</string>
|
<string name="download_wizard_slot_type_internal">内部</string>
|
||||||
|
|
|
@ -81,7 +81,6 @@
|
||||||
<string name="download_wizard_back">返回</string>
|
<string name="download_wizard_back">返回</string>
|
||||||
<string name="download_wizard_next">下一步</string>
|
<string name="download_wizard_next">下一步</string>
|
||||||
<string name="download_wizard_slot_select">请选择或确认下载目标 eSIM 卡槽:</string>
|
<string name="download_wizard_slot_select">请选择或确认下载目标 eSIM 卡槽:</string>
|
||||||
<string name="download_wizard_slot_title">逻辑卡槽 %d</string>
|
|
||||||
<string name="download_wizard_slot_type">类型:</string>
|
<string name="download_wizard_slot_type">类型:</string>
|
||||||
<string name="download_wizard_slot_type_removable">可插拔</string>
|
<string name="download_wizard_slot_type_removable">可插拔</string>
|
||||||
<string name="download_wizard_slot_type_internal">内置</string>
|
<string name="download_wizard_slot_type_internal">内置</string>
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
|
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
|
||||||
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||||
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
||||||
|
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||||
|
|
||||||
<string name="slot_select">Select Slot</string>
|
<string name="slot_select">Select Slot</string>
|
||||||
<string name="slot_select_select">Select</string>
|
<string name="slot_select_select">Select</string>
|
||||||
|
@ -64,7 +65,6 @@
|
||||||
<string name="download_wizard_back">Back</string>
|
<string name="download_wizard_back">Back</string>
|
||||||
<string name="download_wizard_next">Next</string>
|
<string name="download_wizard_next">Next</string>
|
||||||
<string name="download_wizard_slot_select">Select or confirm the eSIM you would like to download to:</string>
|
<string name="download_wizard_slot_select">Select or confirm the eSIM you would like to download to:</string>
|
||||||
<string name="download_wizard_slot_title">Logical slot %d</string>
|
|
||||||
<string name="download_wizard_slot_type">Type:</string>
|
<string name="download_wizard_slot_type">Type:</string>
|
||||||
<string name="download_wizard_slot_type_removable">Removable</string>
|
<string name="download_wizard_slot_type_removable">Removable</string>
|
||||||
<string name="download_wizard_slot_type_internal">Internal</string>
|
<string name="download_wizard_slot_type_internal">Internal</string>
|
||||||
|
@ -95,6 +95,8 @@
|
||||||
<string name="download_wizard_diagnostics_save">Save</string>
|
<string name="download_wizard_diagnostics_save">Save</string>
|
||||||
<string name="download_wizard_diagnostics_file_template">Diagnostics at %s</string>
|
<string name="download_wizard_diagnostics_file_template">Diagnostics at %s</string>
|
||||||
|
|
||||||
|
<string name="logs_saved_message">Logs have been saved to the selected path. Would you like to share the log through another app?</string>
|
||||||
|
|
||||||
<string name="profile_rename_new_name">New nickname</string>
|
<string name="profile_rename_new_name">New nickname</string>
|
||||||
|
|
||||||
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
|
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
|
||||||
|
|
|
@ -6,4 +6,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context)
|
||||||
override val uiComponentFactory by lazy {
|
override val uiComponentFactory by lazy {
|
||||||
UnprivilegedUiComponentFactory()
|
UnprivilegedUiComponentFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val customizableTextProvider by lazy {
|
||||||
|
UnprivilegedCustomizableTextProvider(context)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
|
||||||
|
class UnprivilegedCustomizableTextProvider(private val context: Context) :
|
||||||
|
DefaultCustomizableTextProvider(context) {
|
||||||
|
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||||
|
context.getString(R.string.channel_name_format_unpriv, logicalSlotId)
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package im.angry.openeuicc.ui
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
@ -35,7 +36,8 @@ class UnprivilegedSettingsFragment : SettingsFragment() {
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
requireContext().getSystemService(ClipboardManager::class.java)!!
|
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||||
.setPrimaryClip(ClipData.newPlainText("ara-m", summary))
|
.setPrimaryClip(ClipData.newPlainText("ara-m", summary))
|
||||||
Toast.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast
|
||||||
|
.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">EasyEUICC</string>
|
<string name="app_name" translatable="false">EasyEUICC</string>
|
||||||
<string name="channel_name_format" translatable="false">SIM %d</string>
|
<string name="channel_name_format_unpriv" translatable="false">SIM %d</string>
|
||||||
<string name="compatibility_check">Compatibility Check</string>
|
<string name="compatibility_check">Compatibility Check</string>
|
||||||
<string name="open_sim_toolkit">Open SIM Toolkit</string>
|
<string name="open_sim_toolkit">Open SIM Toolkit</string>
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
||||||
return EuiccChannelImpl(
|
return EuiccChannelImpl(
|
||||||
context.getString(R.string.telephony_manager),
|
context.getString(R.string.telephony_manager),
|
||||||
port,
|
port,
|
||||||
|
intrinsicChannelName = null,
|
||||||
TelephonyManagerApduInterface(
|
TelephonyManagerApduInterface(
|
||||||
port,
|
port,
|
||||||
tm,
|
tm,
|
||||||
|
|
|
@ -23,4 +23,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
||||||
override val euiccChannelFactory by lazy {
|
override val euiccChannelFactory by lazy {
|
||||||
PrivilegedEuiccChannelFactory(context)
|
PrivilegedEuiccChannelFactory(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val customizableTextProvider by lazy {
|
||||||
|
PrivilegedCustomizableTextProvider(context)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import im.angry.openeuicc.R
|
||||||
|
|
||||||
|
class PrivilegedCustomizableTextProvider(private val context: Context) :
|
||||||
|
DefaultCustomizableTextProvider(context) {
|
||||||
|
override val noEuiccExplanation: String
|
||||||
|
get() = context.getString(R.string.no_euicc_priv)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="no_euicc">このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。</string>
|
<string name="no_euicc_priv">このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。</string>
|
||||||
<string name="telephony_manager">TelephonyManager (特権)</string>
|
<string name="telephony_manager">TelephonyManager (特権)</string>
|
||||||
<string name="dsds">デュアル SIM</string>
|
<string name="dsds">デュアル SIM</string>
|
||||||
<string name="toast_dsds_switched">DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。</string>
|
<string name="toast_dsds_switched">DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。</string>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="no_euicc">在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。</string>
|
<string name="no_euicc_priv">在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。</string>
|
||||||
<string name="dsds">双卡</string>
|
<string name="dsds">双卡</string>
|
||||||
<string name="toast_dsds_switched">双卡支持状态已切换。请等待基带重新启动。</string>
|
<string name="toast_dsds_switched">双卡支持状态已切换。请等待基带重新启动。</string>
|
||||||
<string name="footer_mep">此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。</string>
|
<string name="footer_mep">此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。</string>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">OpenEUICC</string>
|
<string name="app_name" translatable="false">OpenEUICC</string>
|
||||||
<string name="no_euicc">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string>
|
<string name="no_euicc_priv">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string>
|
||||||
<string name="telephony_manager">TelephonyManager (Privileged)</string>
|
<string name="telephony_manager">TelephonyManager (Privileged)</string>
|
||||||
|
|
||||||
<string name="dsds">Dual SIM</string>
|
<string name="dsds">Dual SIM</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue