Browse Source

implement a basic eUICC list

Peter Cai 3 years ago
parent
commit
96c5cd131c

+ 10 - 0
.idea/misc.xml

@@ -1,5 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="DesignSurface">
+    <option name="filePathToZoomLevelMap">
+      <map>
+        <entry key="app/src/main/res/layout/activity_main.xml" value="0.19375" />
+        <entry key="app/src/main/res/layout/euicc_profile.xml" value="0.19375" />
+        <entry key="app/src/main/res/layout/fragment_euicc.xml" value="0.19375" />
+        <entry key="app/src/main/res/menu/activity_main_slot_spinner.xml" value="0.19375" />
+      </map>
+    </option>
+  </component>
   <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>

+ 6 - 0
app/build.gradle

@@ -6,6 +6,10 @@ plugins {
 android {
     compileSdk 31
 
+    buildFeatures {
+        viewBinding true
+    }
+
     defaultConfig {
         applicationId "im.angry.openeuicc"
         minSdk 30
@@ -38,6 +42,8 @@ dependencies {
     implementation 'com.google.android.material:material:1.5.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
+    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
+    implementation "androidx.cardview:cardview:1.0.0"
     testImplementation 'junit:junit:4.13.2'
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

+ 1 - 1
app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepository.kt

@@ -9,5 +9,5 @@ data class EuiccChannel(
 
 interface EuiccChannelRepository {
     suspend fun load()
-    val availableChannels: List<EuiccChannel>?
+    val availableChannels: List<EuiccChannel>
 }

+ 12 - 3
app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepositoryProxy.kt

@@ -7,8 +7,17 @@ class EuiccChannelRepositoryProxy(context: Context) : EuiccChannelRepository {
     // TODO: Make this pluggable
     private val inner: EuiccChannelRepository = OmapiEuiccChannelRepository(context)
 
-    override suspend fun load() = inner.load()
+    private var loaded = false
 
-    override val availableChannels: List<EuiccChannel>?
-        get() = inner.availableChannels
+    override suspend fun load() {
+        inner.load()
+        loaded = true
+    }
+
+    override val availableChannels: List<EuiccChannel>
+        get() = if (loaded) {
+            inner.availableChannels
+        } else {
+            throw IllegalStateException("Not loaded yet")
+        }
 }

+ 93 - 0
app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -0,0 +1,93 @@
+package im.angry.openeuicc.ui
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.text.method.PasswordTransformationMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import im.angry.openeuicc.R
+import im.angry.openeuicc.core.EuiccChannel
+import im.angry.openeuicc.databinding.EuiccProfileBinding
+import im.angry.openeuicc.databinding.FragmentEuiccBinding
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class EuiccManagementFragment(private val channel: EuiccChannel) : Fragment() {
+    private var _binding: FragmentEuiccBinding? = null
+    private val binding get() = _binding!!
+
+    private val adapter = EuiccProfileAdapter(listOf())
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentEuiccBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        binding.swipeRefresh.setOnRefreshListener { refresh() }
+        binding.profileList.adapter = adapter
+        binding.profileList.layoutManager =
+            LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        refresh()
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    private fun refresh() {
+        binding.swipeRefresh.isRefreshing = true
+
+        lifecycleScope.launch {
+            val profiles = withContext(Dispatchers.IO) {
+                channel.lpa.profiles
+            }
+
+            withContext(Dispatchers.Main) {
+                adapter.profiles = profiles
+                adapter.notifyDataSetChanged()
+                binding.swipeRefresh.isRefreshing = false
+            }
+        }
+    }
+}
+
+class EuiccProfileAdapter(var profiles: List<Map<String, String>>) :
+        RecyclerView.Adapter<EuiccProfileAdapter.ViewHolder>() {
+    data class ViewHolder(val binding: EuiccProfileBinding) : RecyclerView.ViewHolder(binding.root)
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val binding =
+            EuiccProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        return ViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        // TODO: The library is not exposing the nicknames. Expose them so that we can do something here.
+        holder.binding.name.text = profiles[position]["NAME"]
+        holder.binding.state.setText(
+            if (profiles[position]["STATE"]?.lowercase() == "enabled") {
+                R.string.enabled
+            } else {
+                R.string.disabled
+            }
+        )
+        holder.binding.provider.text = profiles[position]["PROVIDER_NAME"]
+        holder.binding.iccid.text = profiles[position]["ICCID"]
+        holder.binding.iccid.transformationMethod = PasswordTransformationMethod()
+    }
+
+    override fun getItemCount(): Int = profiles.size
+}

+ 54 - 3
app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt

@@ -1,11 +1,17 @@
 package im.angry.openeuicc.ui
 
-import androidx.appcompat.app.AppCompatActivity
 import android.os.Bundle
 import android.util.Log
+import android.view.Menu
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.R
 import im.angry.openeuicc.core.EuiccChannelRepositoryProxy
+import im.angry.openeuicc.databinding.ActivityMainBinding
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -17,22 +23,67 @@ class MainActivity : AppCompatActivity() {
 
     private val repo = EuiccChannelRepositoryProxy(this)
 
+    private lateinit var spinnerAdapter: ArrayAdapter<String>
+    private lateinit var spinner: Spinner
+
+    private val fragments = arrayListOf<EuiccManagementFragment>()
+
+    private lateinit var binding: ActivityMainBinding
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
+        binding = ActivityMainBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        spinnerAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item)
 
         lifecycleScope.launch {
             init()
         }
     }
 
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        menuInflater.inflate(R.menu.activity_main_slot_spinner, menu)
+
+        spinner = menu.findItem(R.id.spinner).actionView as Spinner
+        spinner.adapter = spinnerAdapter
+        spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+            override fun onItemSelected(
+                parent: AdapterView<*>?,
+                view: View?,
+                position: Int,
+                id: Long
+            ) {
+                supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments[position]).commit()
+            }
+
+            override fun onNothingSelected(parent: AdapterView<*>?) {
+            }
+
+        }
+
+        return true
+    }
+
     private suspend fun init() {
         withContext(Dispatchers.IO) {
             repo.load()
-            repo.availableChannels?.forEach {
+            repo.availableChannels.forEach {
                 Log.d(TAG, it.name)
                 Log.d(TAG, it.lpa.eid)
             }
         }
+
+        withContext(Dispatchers.Main) {
+            repo.availableChannels.forEach {
+                spinnerAdapter.add(it.name)
+                fragments.add(EuiccManagementFragment(it))
+            }
+
+            if (fragments.isNotEmpty()) {
+                binding.noEuiccPlaceholder.visibility = View.GONE
+                supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments.first()).commit()
+            }
+        }
     }
 }

+ 9 - 3
app/src/main/res/layout/activity_main.xml

@@ -6,13 +6,19 @@
     android:layout_height="match_parent"
     tools:context=".ui.MainActivity">
 
-    <TextView
+    <FrameLayout
+        android:id="@+id/fragment_root"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="Hello World!"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toTopOf="parent">
+        <TextView
+            android:id="@+id/no_euicc_placeholder"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:text="@string/no_euicc" />
+    </FrameLayout>
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 96 - 0
app/src/main/res/layout/euicc_profile.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <androidx.cardview.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="12dp"
+        android:layout_marginVertical="6dp"
+        app:cardCornerRadius="6dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding="10dp">
+
+            <TextView
+                android:id="@+id/name"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textStyle="bold"
+                android:textSize="16sp"
+                android:singleLine="true"
+                app:layout_constraintLeft_toLeftOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toTopOf="@+id/state"/>
+
+            <TextView
+                android:id="@+id/state"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:textSize="14sp"
+                android:textStyle="italic"
+                android:singleLine="true"
+                app:layout_constraintLeft_toLeftOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/name"
+                app:layout_constraintBottom_toTopOf="@+id/provider_label"/>
+
+            <TextView
+                android:id="@+id/provider_label"
+                android:text="@string/provider"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:textSize="14sp"
+                android:textStyle="bold"
+                android:singleLine="true"
+                app:layout_constraintLeft_toLeftOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/state"
+                app:layout_constraintBottom_toTopOf="@+id/iccid_label"/>
+
+            <TextView
+                android:id="@+id/provider"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:layout_marginLeft="7dp"
+                android:textSize="14sp"
+                android:singleLine="true"
+                app:layout_constraintLeft_toRightOf="@id/provider_label"
+                app:layout_constraintTop_toBottomOf="@id/state"
+                app:layout_constraintBottom_toTopOf="@+id/iccid"/>
+
+            <TextView
+                android:id="@+id/iccid_label"
+                android:text="@string/iccid"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:textSize="14sp"
+                android:textStyle="bold"
+                android:singleLine="true"
+                app:layout_constraintLeft_toLeftOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/provider_label"
+                app:layout_constraintBottom_toBottomOf="parent"/>
+
+            <TextView
+                android:id="@+id/iccid"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:layout_marginLeft="7dp"
+                android:textSize="14sp"
+                android:singleLine="true"
+                app:layout_constraintLeft_toRightOf="@id/iccid_label"
+                app:layout_constraintTop_toBottomOf="@id/provider"
+                app:layout_constraintBottom_toBottomOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </androidx.cardview.widget.CardView>
+
+</FrameLayout>

+ 24 - 0
app/src/main/res/layout/fragment_euicc.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+        android:id="@+id/swipe_refresh"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/profile_list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:paddingTop="6dp"/>
+
+    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 10 - 0
app/src/main/res/menu/activity_main_slot_spinner.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item
+        android:id="@+id/spinner"
+        android:title=""
+        app:actionViewClass="android.widget.Spinner"
+        android:background="?android:attr/colorPrimary"
+        app:showAsAction="always" />
+</menu>

+ 7 - 0
app/src/main/res/values/strings.xml

@@ -1,3 +1,10 @@
 <resources>
     <string name="app_name">OpenEUICC</string>
+
+    <string name="no_euicc">No eUICC found on this device.</string>
+
+    <string name="enabled">Enabled</string>
+    <string name="disabled">Disabled</string>
+    <string name="provider">Provider:</string>
+    <string name="iccid">ICCID:</string>
 </resources>