EuiccManagementFragment.kt 8.0 KB

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