Peter Cai
3a0d805eb2
All checks were successful
/ build-debug (push) Successful in 4m48s
Mainly a treewide `findViewById` -> `requireViewById`, with miscellaneous fixes.
307 lines
11 KiB
Kotlin
307 lines
11 KiB
Kotlin
package im.angry.openeuicc.ui
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import android.text.method.PasswordTransformationMethod
|
|
import android.util.Log
|
|
import android.view.LayoutInflater
|
|
import android.view.Menu
|
|
import android.view.MenuInflater
|
|
import android.view.MenuItem
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.widget.FrameLayout
|
|
import android.widget.ImageButton
|
|
import android.widget.PopupMenu
|
|
import android.widget.TextView
|
|
import android.widget.Toast
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
import net.typeblog.lpac_jni.LocalProfileInfo
|
|
import im.angry.openeuicc.common.R
|
|
import im.angry.openeuicc.util.*
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import java.lang.Exception
|
|
|
|
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|
EuiccChannelFragmentMarker {
|
|
companion object {
|
|
const val TAG = "EuiccManagementFragment"
|
|
|
|
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
|
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId)
|
|
}
|
|
|
|
private lateinit var swipeRefresh: SwipeRefreshLayout
|
|
private lateinit var fab: FloatingActionButton
|
|
private lateinit var profileList: RecyclerView
|
|
|
|
private val adapter = EuiccProfileAdapter()
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setHasOptionsMenu(true)
|
|
}
|
|
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View {
|
|
val view = inflater.inflate(R.layout.fragment_euicc, container, false)
|
|
|
|
swipeRefresh = view.requireViewById(R.id.swipe_refresh)
|
|
fab = view.requireViewById(R.id.fab)
|
|
profileList = view.requireViewById(R.id.profile_list)
|
|
|
|
return view
|
|
}
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
swipeRefresh.setOnRefreshListener { refresh() }
|
|
profileList.adapter = adapter
|
|
profileList.layoutManager =
|
|
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
|
|
|
fab.setOnClickListener {
|
|
ProfileDownloadFragment.newInstance(slotId, portId)
|
|
.show(childFragmentManager, ProfileDownloadFragment.TAG)
|
|
}
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
refresh()
|
|
}
|
|
|
|
override fun onEuiccProfilesChanged() {
|
|
refresh()
|
|
}
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
super.onCreateOptionsMenu(menu, inflater)
|
|
inflater.inflate(R.menu.fragment_euicc, menu)
|
|
}
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
|
when (item.itemId) {
|
|
R.id.show_notifications -> {
|
|
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
|
putExtra("logicalSlotId", channel.logicalSlotId)
|
|
startActivity(this)
|
|
}
|
|
true
|
|
}
|
|
else -> super.onOptionsItemSelected(item)
|
|
}
|
|
|
|
protected open suspend fun onCreateFooterViews(parent: ViewGroup): List<View> = listOf()
|
|
|
|
@SuppressLint("NotifyDataSetChanged")
|
|
private fun refresh() {
|
|
swipeRefresh.isRefreshing = true
|
|
|
|
lifecycleScope.launch {
|
|
val profiles = withContext(Dispatchers.IO) {
|
|
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
|
channel.lpa.profiles
|
|
}
|
|
|
|
withContext(Dispatchers.Main) {
|
|
adapter.profiles = profiles.operational
|
|
adapter.footerViews = onCreateFooterViews(profileList)
|
|
adapter.notifyDataSetChanged()
|
|
swipeRefresh.isRefreshing = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
|
|
swipeRefresh.isRefreshing = true
|
|
fab.isEnabled = false
|
|
|
|
lifecycleScope.launch {
|
|
try {
|
|
if (enable) {
|
|
doEnableProfile(iccid)
|
|
} else {
|
|
doDisableProfile(iccid)
|
|
}
|
|
refresh()
|
|
fab.isEnabled = true
|
|
} catch (e: Exception) {
|
|
Log.d(TAG, "Failed to enable / disable profile $iccid")
|
|
Log.d(TAG, Log.getStackTraceString(e))
|
|
fab.isEnabled = true
|
|
Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show()
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun doEnableProfile(iccid: String) =
|
|
channel.lpa.beginOperation {
|
|
channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) &&
|
|
preferenceRepository.notificationEnableFlow.first()
|
|
}
|
|
|
|
private suspend fun doDisableProfile(iccid: String) =
|
|
channel.lpa.beginOperation {
|
|
channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) &&
|
|
preferenceRepository.notificationDisableFlow.first()
|
|
}
|
|
|
|
protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
|
|
popup.inflate(R.menu.profile_options)
|
|
if (profile.isEnabled) {
|
|
popup.menu.findItem(R.id.enable).isVisible = false
|
|
popup.menu.findItem(R.id.delete).isVisible = false
|
|
}
|
|
}
|
|
|
|
sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
|
enum class Type(val value: Int) {
|
|
PROFILE(0),
|
|
FOOTER(1);
|
|
|
|
companion object {
|
|
fun fromInt(value: Int) =
|
|
Type.values().first { it.value == value }
|
|
}
|
|
}
|
|
}
|
|
|
|
inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) {
|
|
fun attach(view: View) {
|
|
view.parent?.let { (it as ViewGroup).removeView(view) }
|
|
(itemView as FrameLayout).addView(view)
|
|
}
|
|
|
|
fun detach() {
|
|
(itemView as FrameLayout).removeAllViews()
|
|
}
|
|
}
|
|
|
|
inner class ProfileViewHolder(private val root: View) : ViewHolder(root) {
|
|
private val iccid: TextView = root.requireViewById(R.id.iccid)
|
|
private val name: TextView = root.requireViewById(R.id.name)
|
|
private val state: TextView = root.requireViewById(R.id.state)
|
|
private val provider: TextView = root.requireViewById(R.id.provider)
|
|
private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
|
|
|
|
init {
|
|
iccid.setOnClickListener {
|
|
if (iccid.transformationMethod == null) {
|
|
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
|
|
} else {
|
|
iccid.transformationMethod = null
|
|
}
|
|
}
|
|
|
|
profileMenu.setOnClickListener { showOptionsMenu() }
|
|
}
|
|
|
|
private lateinit var profile: LocalProfileInfo
|
|
|
|
fun setProfile(profile: LocalProfileInfo) {
|
|
this.profile = profile
|
|
name.text = profile.displayName
|
|
|
|
state.setText(
|
|
if (profile.isEnabled) {
|
|
R.string.enabled
|
|
} else {
|
|
R.string.disabled
|
|
}
|
|
)
|
|
provider.text = profile.providerName
|
|
iccid.text = profile.iccid
|
|
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
|
|
}
|
|
|
|
private fun showOptionsMenu() {
|
|
PopupMenu(root.context, profileMenu).apply {
|
|
setOnMenuItemClickListener(::onMenuItemClicked)
|
|
populatePopupWithProfileActions(this, profile)
|
|
show()
|
|
}
|
|
}
|
|
|
|
private fun onMenuItemClicked(item: MenuItem): Boolean =
|
|
when (item.itemId) {
|
|
R.id.enable -> {
|
|
enableOrDisableProfile(profile.iccid, true)
|
|
true
|
|
}
|
|
R.id.disable -> {
|
|
enableOrDisableProfile(profile.iccid, false)
|
|
true
|
|
}
|
|
R.id.rename -> {
|
|
ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
|
|
.show(childFragmentManager, ProfileRenameFragment.TAG)
|
|
true
|
|
}
|
|
R.id.delete -> {
|
|
ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
|
|
.show(childFragmentManager, ProfileDeleteFragment.TAG)
|
|
true
|
|
}
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
inner class EuiccProfileAdapter : RecyclerView.Adapter<ViewHolder>() {
|
|
var profiles: List<LocalProfileInfo> = listOf()
|
|
var footerViews: List<View> = listOf()
|
|
|
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
|
when (ViewHolder.Type.fromInt(viewType)) {
|
|
ViewHolder.Type.PROFILE -> {
|
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
|
|
ProfileViewHolder(view)
|
|
}
|
|
ViewHolder.Type.FOOTER -> {
|
|
FooterViewHolder()
|
|
}
|
|
}
|
|
|
|
override fun getItemViewType(position: Int): Int =
|
|
when {
|
|
position < profiles.size -> {
|
|
ViewHolder.Type.PROFILE.value
|
|
}
|
|
position >= profiles.size && position < profiles.size + footerViews.size -> {
|
|
ViewHolder.Type.FOOTER.value
|
|
}
|
|
else -> -1
|
|
}
|
|
|
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
when (holder) {
|
|
is ProfileViewHolder -> {
|
|
holder.setProfile(profiles[position])
|
|
}
|
|
is FooterViewHolder -> {
|
|
holder.attach(footerViews[position - profiles.size])
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onViewRecycled(holder: ViewHolder) {
|
|
if (holder is FooterViewHolder) {
|
|
holder.detach()
|
|
}
|
|
}
|
|
|
|
override fun getItemCount(): Int = profiles.size + footerViews.size
|
|
}
|
|
} |