EuiccManagementFragment.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. package im.angry.openeuicc.ui
  2. import android.annotation.SuppressLint
  3. import android.content.Intent
  4. import android.os.Bundle
  5. import android.text.method.PasswordTransformationMethod
  6. import android.util.Log
  7. import android.view.LayoutInflater
  8. import android.view.Menu
  9. import android.view.MenuInflater
  10. import android.view.MenuItem
  11. import android.view.View
  12. import android.view.ViewGroup
  13. import android.widget.FrameLayout
  14. import android.widget.ImageButton
  15. import android.widget.PopupMenu
  16. import android.widget.TextView
  17. import android.widget.Toast
  18. import androidx.appcompat.app.AlertDialog
  19. import androidx.fragment.app.Fragment
  20. import androidx.lifecycle.lifecycleScope
  21. import androidx.recyclerview.widget.LinearLayoutManager
  22. import androidx.recyclerview.widget.RecyclerView
  23. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
  24. import com.google.android.material.floatingactionbutton.FloatingActionButton
  25. import net.typeblog.lpac_jni.LocalProfileInfo
  26. import im.angry.openeuicc.common.R
  27. import im.angry.openeuicc.util.*
  28. import kotlinx.coroutines.Dispatchers
  29. import kotlinx.coroutines.TimeoutCancellationException
  30. import kotlinx.coroutines.flow.first
  31. import kotlinx.coroutines.launch
  32. import kotlinx.coroutines.withContext
  33. open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
  34. EuiccChannelFragmentMarker {
  35. companion object {
  36. const val TAG = "EuiccManagementFragment"
  37. fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
  38. newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId)
  39. }
  40. private lateinit var swipeRefresh: SwipeRefreshLayout
  41. private lateinit var fab: FloatingActionButton
  42. private lateinit var profileList: RecyclerView
  43. private val adapter = EuiccProfileAdapter()
  44. override fun onCreate(savedInstanceState: Bundle?) {
  45. super.onCreate(savedInstanceState)
  46. setHasOptionsMenu(true)
  47. }
  48. override fun onCreateView(
  49. inflater: LayoutInflater,
  50. container: ViewGroup?,
  51. savedInstanceState: Bundle?
  52. ): View {
  53. val view = inflater.inflate(R.layout.fragment_euicc, container, false)
  54. swipeRefresh = view.requireViewById(R.id.swipe_refresh)
  55. fab = view.requireViewById(R.id.fab)
  56. profileList = view.requireViewById(R.id.profile_list)
  57. return view
  58. }
  59. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  60. super.onViewCreated(view, savedInstanceState)
  61. swipeRefresh.setOnRefreshListener { refresh() }
  62. profileList.adapter = adapter
  63. profileList.layoutManager =
  64. LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
  65. fab.setOnClickListener {
  66. ProfileDownloadFragment.newInstance(slotId, portId)
  67. .show(childFragmentManager, ProfileDownloadFragment.TAG)
  68. }
  69. refresh()
  70. }
  71. override fun onEuiccProfilesChanged() {
  72. refresh()
  73. }
  74. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  75. super.onCreateOptionsMenu(menu, inflater)
  76. inflater.inflate(R.menu.fragment_euicc, menu)
  77. }
  78. override fun onOptionsItemSelected(item: MenuItem): Boolean =
  79. when (item.itemId) {
  80. R.id.show_notifications -> {
  81. Intent(requireContext(), NotificationsActivity::class.java).apply {
  82. putExtra("logicalSlotId", channel.logicalSlotId)
  83. startActivity(this)
  84. }
  85. true
  86. }
  87. else -> super.onOptionsItemSelected(item)
  88. }
  89. protected open suspend fun onCreateFooterViews(parent: ViewGroup): List<View> = listOf()
  90. @SuppressLint("NotifyDataSetChanged")
  91. private fun refresh() {
  92. swipeRefresh.isRefreshing = true
  93. lifecycleScope.launch {
  94. val profiles = withContext(Dispatchers.IO) {
  95. euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
  96. channel.lpa.profiles
  97. }
  98. withContext(Dispatchers.Main) {
  99. adapter.profiles = profiles.operational
  100. adapter.footerViews = onCreateFooterViews(profileList)
  101. adapter.notifyDataSetChanged()
  102. swipeRefresh.isRefreshing = false
  103. }
  104. }
  105. }
  106. private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
  107. swipeRefresh.isRefreshing = true
  108. fab.isEnabled = false
  109. lifecycleScope.launch {
  110. beginTrackedOperation {
  111. val res = if (enable) {
  112. channel.lpa.enableProfile(iccid)
  113. } else {
  114. channel.lpa.disableProfile(iccid)
  115. }
  116. if (!res) {
  117. Log.d(TAG, "Failed to enable / disable profile $iccid")
  118. withContext(Dispatchers.Main) {
  119. Toast.makeText(
  120. context,
  121. R.string.toast_profile_enable_failed,
  122. Toast.LENGTH_LONG
  123. ).show()
  124. }
  125. return@beginTrackedOperation false
  126. }
  127. try {
  128. euiccChannelManager.waitForReconnect(slotId, portId, timeoutMillis = 30 * 1000)
  129. } catch (e: TimeoutCancellationException) {
  130. withContext(Dispatchers.Main) {
  131. // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
  132. AlertDialog.Builder(requireContext()).apply {
  133. setMessage(R.string.enable_disable_timeout)
  134. setPositiveButton(android.R.string.ok) { dialog, _ ->
  135. dialog.dismiss()
  136. requireActivity().finish()
  137. }
  138. setOnDismissListener { _ ->
  139. requireActivity().finish()
  140. }
  141. show()
  142. }
  143. }
  144. return@beginTrackedOperation false
  145. }
  146. if (enable) {
  147. preferenceRepository.notificationEnableFlow.first()
  148. } else {
  149. preferenceRepository.notificationDisableFlow.first()
  150. }
  151. }
  152. refresh()
  153. fab.isEnabled = true
  154. }
  155. }
  156. protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
  157. popup.inflate(R.menu.profile_options)
  158. if (profile.isEnabled) {
  159. popup.menu.findItem(R.id.enable).isVisible = false
  160. popup.menu.findItem(R.id.delete).isVisible = false
  161. }
  162. }
  163. sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
  164. enum class Type(val value: Int) {
  165. PROFILE(0),
  166. FOOTER(1);
  167. companion object {
  168. fun fromInt(value: Int) =
  169. Type.values().first { it.value == value }
  170. }
  171. }
  172. }
  173. inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) {
  174. fun attach(view: View) {
  175. view.parent?.let { (it as ViewGroup).removeView(view) }
  176. (itemView as FrameLayout).addView(view)
  177. }
  178. fun detach() {
  179. (itemView as FrameLayout).removeAllViews()
  180. }
  181. }
  182. inner class ProfileViewHolder(private val root: View) : ViewHolder(root) {
  183. private val iccid: TextView = root.requireViewById(R.id.iccid)
  184. private val name: TextView = root.requireViewById(R.id.name)
  185. private val state: TextView = root.requireViewById(R.id.state)
  186. private val provider: TextView = root.requireViewById(R.id.provider)
  187. private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
  188. init {
  189. iccid.setOnClickListener {
  190. if (iccid.transformationMethod == null) {
  191. iccid.transformationMethod = PasswordTransformationMethod.getInstance()
  192. } else {
  193. iccid.transformationMethod = null
  194. }
  195. }
  196. profileMenu.setOnClickListener { showOptionsMenu() }
  197. }
  198. private lateinit var profile: LocalProfileInfo
  199. fun setProfile(profile: LocalProfileInfo) {
  200. this.profile = profile
  201. name.text = profile.displayName
  202. state.setText(
  203. if (profile.isEnabled) {
  204. R.string.enabled
  205. } else {
  206. R.string.disabled
  207. }
  208. )
  209. provider.text = profile.providerName
  210. iccid.text = profile.iccid
  211. iccid.transformationMethod = PasswordTransformationMethod.getInstance()
  212. }
  213. private fun showOptionsMenu() {
  214. PopupMenu(root.context, profileMenu).apply {
  215. setOnMenuItemClickListener(::onMenuItemClicked)
  216. populatePopupWithProfileActions(this, profile)
  217. show()
  218. }
  219. }
  220. private fun onMenuItemClicked(item: MenuItem): Boolean =
  221. when (item.itemId) {
  222. R.id.enable -> {
  223. enableOrDisableProfile(profile.iccid, true)
  224. true
  225. }
  226. R.id.disable -> {
  227. enableOrDisableProfile(profile.iccid, false)
  228. true
  229. }
  230. R.id.rename -> {
  231. ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
  232. .show(childFragmentManager, ProfileRenameFragment.TAG)
  233. true
  234. }
  235. R.id.delete -> {
  236. ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
  237. .show(childFragmentManager, ProfileDeleteFragment.TAG)
  238. true
  239. }
  240. else -> false
  241. }
  242. }
  243. inner class EuiccProfileAdapter : RecyclerView.Adapter<ViewHolder>() {
  244. var profiles: List<LocalProfileInfo> = listOf()
  245. var footerViews: List<View> = listOf()
  246. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
  247. when (ViewHolder.Type.fromInt(viewType)) {
  248. ViewHolder.Type.PROFILE -> {
  249. val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
  250. ProfileViewHolder(view)
  251. }
  252. ViewHolder.Type.FOOTER -> {
  253. FooterViewHolder()
  254. }
  255. }
  256. override fun getItemViewType(position: Int): Int =
  257. when {
  258. position < profiles.size -> {
  259. ViewHolder.Type.PROFILE.value
  260. }
  261. position >= profiles.size && position < profiles.size + footerViews.size -> {
  262. ViewHolder.Type.FOOTER.value
  263. }
  264. else -> -1
  265. }
  266. override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  267. when (holder) {
  268. is ProfileViewHolder -> {
  269. holder.setProfile(profiles[position])
  270. }
  271. is FooterViewHolder -> {
  272. holder.attach(footerViews[position - profiles.size])
  273. }
  274. }
  275. }
  276. override fun onViewRecycled(holder: ViewHolder) {
  277. if (holder is FooterViewHolder) {
  278. holder.detach()
  279. }
  280. }
  281. override fun getItemCount(): Int = profiles.size + footerViews.size
  282. }
  283. }