EuiccManagementFragment.kt 7.5 KB

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