EuiccManagementFragment.kt 7.9 KB

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