EuiccManagementFragment.kt 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package im.angry.openeuicc.ui
  2. import android.annotation.SuppressLint
  3. import android.os.Bundle
  4. import android.text.method.PasswordTransformationMethod
  5. import android.util.Log
  6. import android.view.LayoutInflater
  7. import android.view.MenuItem
  8. import android.view.View
  9. import android.view.ViewGroup
  10. import android.widget.PopupMenu
  11. import android.widget.Toast
  12. import androidx.fragment.app.Fragment
  13. import androidx.lifecycle.lifecycleScope
  14. import androidx.recyclerview.widget.LinearLayoutManager
  15. import androidx.recyclerview.widget.RecyclerView
  16. import com.truphone.lpa.LocalProfileInfo
  17. import com.truphone.lpad.progress.Progress
  18. import im.angry.openeuicc.R
  19. import im.angry.openeuicc.databinding.EuiccProfileBinding
  20. import im.angry.openeuicc.databinding.FragmentEuiccBinding
  21. import im.angry.openeuicc.util.openEuiccApplication
  22. import im.angry.openeuicc.util.tryRefreshCachedEuiccInfo
  23. import kotlinx.coroutines.Dispatchers
  24. import kotlinx.coroutines.launch
  25. import kotlinx.coroutines.withContext
  26. import java.lang.Exception
  27. class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
  28. companion object {
  29. const val TAG = "EuiccManagementFragment"
  30. fun newInstance(slotId: Int): EuiccManagementFragment =
  31. newInstanceEuicc(EuiccManagementFragment::class.java, slotId)
  32. }
  33. private var _binding: FragmentEuiccBinding? = null
  34. private val binding get() = _binding!!
  35. private val adapter = EuiccProfileAdapter(listOf())
  36. override fun onCreateView(
  37. inflater: LayoutInflater,
  38. container: ViewGroup?,
  39. savedInstanceState: Bundle?
  40. ): View {
  41. _binding = FragmentEuiccBinding.inflate(inflater, container, false)
  42. return binding.root
  43. }
  44. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  45. super.onViewCreated(view, savedInstanceState)
  46. binding.swipeRefresh.setOnRefreshListener { refresh() }
  47. binding.profileList.adapter = adapter
  48. binding.profileList.layoutManager =
  49. LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
  50. binding.fab.setOnClickListener {
  51. ProfileDownloadFragment.newInstance(slotId)
  52. .show(childFragmentManager, ProfileDownloadFragment.TAG)
  53. }
  54. }
  55. override fun onStart() {
  56. super.onStart()
  57. refresh()
  58. }
  59. override fun onEuiccProfilesChanged() {
  60. refresh()
  61. }
  62. @SuppressLint("NotifyDataSetChanged")
  63. private fun refresh() {
  64. binding.swipeRefresh.isRefreshing = true
  65. lifecycleScope.launch {
  66. val profiles = withContext(Dispatchers.IO) {
  67. openEuiccApplication.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
  68. channel.lpa.profiles
  69. }
  70. withContext(Dispatchers.Main) {
  71. adapter.profiles = profiles.filter { it.profileClass != LocalProfileInfo.Clazz.Testing }
  72. adapter.notifyDataSetChanged()
  73. binding.swipeRefresh.isRefreshing = false
  74. }
  75. }
  76. }
  77. private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
  78. binding.swipeRefresh.isRefreshing = true
  79. binding.swipeRefresh.isEnabled = false
  80. binding.fab.isEnabled = false
  81. lifecycleScope.launch {
  82. try {
  83. if (enable) {
  84. doEnableProfile(iccid)
  85. } else {
  86. doDisableProfile(iccid)
  87. }
  88. Toast.makeText(context, R.string.toast_profile_enabled, Toast.LENGTH_LONG).show()
  89. // The APDU channel will be invalid when the SIM reboots. For now, just exit the app
  90. euiccChannelManager.invalidate()
  91. requireActivity().finish()
  92. } catch (e: Exception) {
  93. Log.d(TAG, "Failed to enable / disable profile $iccid")
  94. Log.d(TAG, Log.getStackTraceString(e))
  95. binding.fab.isEnabled = true
  96. binding.swipeRefresh.isEnabled = true
  97. Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show()
  98. }
  99. }
  100. }
  101. private suspend fun doEnableProfile(iccid: String) =
  102. withContext(Dispatchers.IO) {
  103. channel.lpa.enableProfile(iccid, Progress())
  104. }
  105. private suspend fun doDisableProfile(iccid: String) =
  106. withContext(Dispatchers.IO) {
  107. channel.lpa.disableProfile(iccid, Progress())
  108. }
  109. inner class ViewHolder(private val binding: EuiccProfileBinding) : RecyclerView.ViewHolder(binding.root) {
  110. init {
  111. binding.iccid.setOnClickListener {
  112. if (binding.iccid.transformationMethod == null) {
  113. binding.iccid.transformationMethod = PasswordTransformationMethod.getInstance()
  114. } else {
  115. binding.iccid.transformationMethod = null
  116. }
  117. }
  118. binding.profileMenu.setOnClickListener { showOptionsMenu() }
  119. }
  120. private lateinit var profile: LocalProfileInfo
  121. fun setProfile(profile: LocalProfileInfo) {
  122. this.profile = profile
  123. binding.name.text = getName()
  124. binding.state.setText(
  125. if (isEnabled()) {
  126. R.string.enabled
  127. } else {
  128. R.string.disabled
  129. }
  130. )
  131. binding.provider.text = profile.providerName
  132. binding.iccid.text = profile.iccidLittleEndian
  133. binding.iccid.transformationMethod = PasswordTransformationMethod.getInstance()
  134. }
  135. private fun isEnabled(): Boolean =
  136. profile.state == LocalProfileInfo.State.Enabled
  137. private fun getName(): String =
  138. profile.nickName.ifEmpty {
  139. profile.name
  140. }
  141. private fun showOptionsMenu() {
  142. PopupMenu(binding.root.context, binding.profileMenu).apply {
  143. setOnMenuItemClickListener(::onMenuItemClicked)
  144. inflate(R.menu.profile_options)
  145. if (isEnabled()) {
  146. menu.findItem(R.id.enable).isVisible = false
  147. menu.findItem(R.id.delete).isVisible = false
  148. } else {
  149. menu.findItem(R.id.disable).isVisible = false
  150. }
  151. show()
  152. }
  153. }
  154. private fun onMenuItemClicked(item: MenuItem): Boolean =
  155. when (item.itemId) {
  156. R.id.enable -> {
  157. enableOrDisableProfile(profile.iccid, true)
  158. true
  159. }
  160. R.id.disable -> {
  161. enableOrDisableProfile(profile.iccid, false)
  162. true
  163. }
  164. R.id.rename -> {
  165. ProfileRenameFragment.newInstance(slotId, profile.iccid, getName())
  166. .show(childFragmentManager, ProfileRenameFragment.TAG)
  167. true
  168. }
  169. R.id.delete -> {
  170. ProfileDeleteFragment.newInstance(slotId, profile.iccid, getName())
  171. .show(childFragmentManager, ProfileDeleteFragment.TAG)
  172. true
  173. }
  174. else -> false
  175. }
  176. }
  177. inner class EuiccProfileAdapter(var profiles: List<LocalProfileInfo>) : RecyclerView.Adapter<ViewHolder>() {
  178. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
  179. val binding =
  180. EuiccProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
  181. return ViewHolder(binding)
  182. }
  183. override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  184. holder.setProfile(profiles[position])
  185. }
  186. override fun getItemCount(): Int = profiles.size
  187. }
  188. }