Peter Cai
632b6b4931
yes yes yes we should be using dependency injection but let's keep it simple with AOSP building...
306 lines
11 KiB
Kotlin
306 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, EuiccFragmentMarker {
|
|
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.findViewById(R.id.swipe_refresh)
|
|
fab = view.findViewById(R.id.fab)
|
|
profileList = view.findViewById(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.findViewById(R.id.iccid)
|
|
private val name: TextView = root.findViewById(R.id.name)
|
|
private val state: TextView = root.findViewById(R.id.state)
|
|
private val provider: TextView = root.findViewById(R.id.provider)
|
|
private val profileMenu: ImageButton = root.findViewById(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
|
|
}
|
|
} |