hxMac 1 ヶ月 前
コミット
c1e3f9271c
52 ファイル変更1241 行追加202 行削除
  1. 66 4
      UtilsApplication/app/app/src/main/AndroidManifest.xml
  2. 2 3
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/MainActivity.kt
  3. 0 2
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/dashboard/DashboardFragment.kt
  4. 1 1
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/dashboard/DashboardViewModel.kt
  5. 18 4
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/home/HomeFragment.kt
  6. 14 1
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/home/HomeViewModel.kt
  7. 0 40
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/notifications/NotificationsFragment.kt
  8. 0 13
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/notifications/NotificationsViewModel.kt
  9. 222 0
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/sms/SmsFragment.kt
  10. 16 0
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/sms/SmsViewModel.kt
  11. 12 0
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/web/UrlFormState.kt
  12. 26 7
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/web/WebFragment.kt
  13. 37 11
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/web/WebViewModel.kt
  14. 16 0
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/HeadlessSmsSendService.java
  15. 15 0
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/MmsReceiver.java
  16. 13 0
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/SmsReceiver.java
  17. 1 48
      UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/TgUtils.java
  18. 7 0
      UtilsApplication/app/app/src/main/res/drawable/twotone_connected_tv_24.xml
  19. 7 0
      UtilsApplication/app/app/src/main/res/drawable/twotone_email_24.xml
  20. 7 0
      UtilsApplication/app/app/src/main/res/drawable/twotone_watch_later_24.xml
  21. 8 2
      UtilsApplication/app/app/src/main/res/layout/activity_main.xml
  22. 1 0
      UtilsApplication/app/app/src/main/res/layout/fragment_home.xml
  23. 143 0
      UtilsApplication/app/app/src/main/res/layout/fragment_sms.xml
  24. 15 3
      UtilsApplication/app/app/src/main/res/layout/fragment_web.xml
  25. 9 6
      UtilsApplication/app/app/src/main/res/layout/item_menu.xml
  26. 0 4
      UtilsApplication/app/app/src/main/res/menu/bottom_nav_menu.xml
  27. 7 7
      UtilsApplication/app/app/src/main/res/navigation/mobile_navigation.xml
  28. 1 1
      UtilsApplication/app/app/src/main/res/values-night/themes.xml
  29. 4 42
      UtilsApplication/app/app/src/main/res/values/strings.xml
  30. 1 1
      UtilsApplication/app/app/src/main/res/values/themes.xml
  31. 3 2
      UtilsApplication/app/gradle/libs.versions.toml
  32. 1 0
      demo/.idea/gradle.xml
  33. 1 0
      demo/app/build.gradle
  34. 4 0
      demo/app/src/main/AndroidManifest.xml
  35. 24 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/LoginDataSource.kt
  36. 46 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/LoginRepository.kt
  37. 18 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/Result.kt
  38. 9 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/model/LoggedInUser.kt
  39. 9 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoggedInUserView.kt
  40. 130 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginActivity.kt
  41. 10 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginFormState.kt
  42. 9 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginResult.kt
  43. 55 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginViewModel.kt
  44. 25 0
      demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginViewModelFactory.kt
  45. 69 0
      demo/app/src/main/res/layout-w1240dp/activity_login.xml
  46. 76 0
      demo/app/src/main/res/layout-w936dp/activity_login.xml
  47. 69 0
      demo/app/src/main/res/layout/activity_login.xml
  48. 1 0
      demo/app/src/main/res/values-land/dimens.xml
  49. 1 0
      demo/app/src/main/res/values-w1240dp/dimens.xml
  50. 1 0
      demo/app/src/main/res/values-w600dp/dimens.xml
  51. 9 0
      demo/app/src/main/res/values/strings.xml
  52. 2 0
      demo/gradle/libs.versions.toml

+ 66 - 4
UtilsApplication/app/app/src/main/AndroidManifest.xml

@@ -1,9 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools" >
+    xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
     <application
         android:allowBackup="true"
@@ -14,16 +24,68 @@
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:theme="@style/Theme.MyDemoApplication"
-        tools:targetApi="31" >
+        tools:targetApi="31">
         <activity
             android:name=".MainActivity"
-            android:exported="true" >
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-
                 <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.APP_MESSAGING" />
+                <category android:name="android.app.role.SMS"/>
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <action android:name="android.intent.action.SENDTO" />
+                <category android:name="android.intent.category.APP_MESSAGING" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.app.role.SMS"/>
+                <data android:scheme="sms" />
+                <data android:scheme="smsto" />
+                <data android:scheme="mms" />
+                <data android:scheme="mmsto" />
             </intent-filter>
         </activity>
+
+        <receiver
+            android:name=".util.SmsReceiver"
+            android:exported="true"
+            android:permission="android.permission.BROADCAST_SMS">
+            <intent-filter>
+                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+            </intent-filter>
+        </receiver>
+
+        <receiver
+            android:name=".util.MmsReceiver"
+            android:exported="true"
+            android:permission="android.permission.BROADCAST_WAP_PUSH">
+            <intent-filter>
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
+
+                <data android:mimeType="application/vnd.wap.mms-message" />
+            </intent-filter>
+        </receiver>
+
+        <service
+            android:name=".util.HeadlessSmsSendService"
+            android:exported="true"
+            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
+            <intent-filter>
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="sms" />
+                <data android:scheme="smsto" />
+                <data android:scheme="mms" />
+                <data android:scheme="mmsto" />
+            </intent-filter>
+        </service>
+
+
+
     </application>
 
 </manifest>

+ 2 - 3
UtilsApplication/app/app/src/main/java/com/hx/utils/application/MainActivity.kt

@@ -22,9 +22,8 @@ class MainActivity : AppCompatActivity() {
         val navController = findNavController(R.id.nav_host_fragment_activity_main)
         val appBarConfiguration = AppBarConfiguration(
             setOf(
-                R.id.navigation_home,        // 显式指定顶级目标 fragment
-                R.id.navigation_dashboard,   // 显式指定顶级目标 fragment
-                R.id.navigation_notifications // 显式指定顶级目标 fragment
+                R.id.navigation_home,
+                R.id.navigation_dashboard
             )
         )
         setupActionBarWithNavController(navController, appBarConfiguration)

+ 0 - 2
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/dashboard/DashboardFragment.kt

@@ -13,8 +13,6 @@ class DashboardFragment : Fragment() {
 
     private var _binding: FragmentDashboardBinding? = null
 
-    // This property is only valid between onCreateView and
-    // onDestroyView.
     private val binding get() = _binding!!
 
     override fun onCreateView(

+ 1 - 1
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/dashboard/DashboardViewModel.kt

@@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel
 class DashboardViewModel : ViewModel() {
 
     private val _text = MutableLiveData<String>().apply {
-        value = "This is dashboard Fragment"
+        value = "Empty"
     }
     val text: LiveData<String> = _text
 }

+ 18 - 4
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/home/HomeFragment.kt

@@ -9,17 +9,17 @@ import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
 import androidx.navigation.Navigation.findNavController
 import androidx.recyclerview.widget.GridLayoutManager
+import com.blankj.utilcode.util.ToastUtils
 import com.hx.utils.application.R
 import com.hx.utils.application.base.BaseAdapter
 import com.hx.utils.application.databinding.FragmentHomeBinding
 import com.hx.utils.application.databinding.ItemMenuBinding
 
 class HomeFragment : Fragment() {
-    private val gridLayoutManager: GridLayoutManager by lazy { GridLayoutManager(activity, 3) }
     private var fyOrderAdapter: BaseAdapter<String, ItemMenuBinding>? = null
     private var _binding: FragmentHomeBinding? = null
     private val binding get() = _binding!!
-
+    private lateinit var homeViewModel: HomeViewModel
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -27,7 +27,7 @@ class HomeFragment : Fragment() {
     ): View {
 
         _binding = FragmentHomeBinding.inflate(inflater, container, false)
-        val homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java]
+        homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java]
         homeViewModel.items.observe(viewLifecycleOwner) {
             initList(it)
         }
@@ -37,6 +37,7 @@ class HomeFragment : Fragment() {
     private fun initList(newList: MutableList<String>) {
         fyOrderAdapter?.let {
             it.setList(newList)
+            binding.menuList.layoutManager = GridLayoutManager(activity, 2)
             binding.menuList.adapter = fyOrderAdapter
         } ?: run {
             fyOrderAdapter = object : BaseAdapter<String, ItemMenuBinding>() {
@@ -46,6 +47,8 @@ class HomeFragment : Fragment() {
 
                 override fun onBindView(bind: ItemMenuBinding, data: String, position: Int) {
                     bind.name.text = data
+                    homeViewModel.itemsImg.value?.get(position)
+                        ?.let { bind.name.setCompoundDrawablesWithIntrinsicBounds(0, it, 0, 0) }
                 }
             }.also {
                 it.setList(newList)
@@ -59,9 +62,20 @@ class HomeFragment : Fragment() {
                                 R.id.nav_host_fragment_activity_main
                             ).navigate(R.id.navigation_web, bundle)
                         }
+
+                        1 -> {
+                            findNavController(
+                                requireActivity(),
+                                R.id.nav_host_fragment_activity_main
+                            ).navigate(R.id.navigation_sms, bundle)
+                        }
+
+                        2 -> {
+                            ToastUtils.showLong(item)
+                        }
                     }
                 }
-                binding.menuList.layoutManager = gridLayoutManager
+                binding.menuList.layoutManager = GridLayoutManager(activity, 2)
                 binding.menuList.adapter = it
             }
         }

+ 14 - 1
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/home/HomeViewModel.kt

@@ -8,10 +8,23 @@ import com.hx.utils.application.R
 
 class HomeViewModel : ViewModel() {
     private val _items = MutableLiveData<MutableList<String>>().apply {
-        value = mutableListOf(StringUtils.getString(R.string.webview_test))
+        value = mutableListOf(
+            StringUtils.getString(R.string.webview_test),
+            StringUtils.getString(R.string.sms_tool),
+            "wait..."
+        )
     }
     val items: LiveData<MutableList<String>> = _items
 
+    private val _itemsImg = MutableLiveData<MutableList<Int>>().apply {
+        value = mutableListOf(
+            R.drawable.twotone_connected_tv_24,
+            R.drawable.twotone_email_24,
+            R.drawable.twotone_watch_later_24
+        )
+    }
+    val itemsImg: LiveData<MutableList<Int>> = _itemsImg
+
     fun updateItems(newItems: List<String>) {
         _items.value?.clear()  // 清空旧数据
         _items.value?.addAll(newItems)  // 添加新数据

+ 0 - 40
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/notifications/NotificationsFragment.kt

@@ -1,40 +0,0 @@
-package com.hx.utils.application.ui.notifications
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
-import com.hx.utils.application.databinding.FragmentNotificationsBinding
-
-class NotificationsFragment : Fragment() {
-
-    private var _binding: FragmentNotificationsBinding? = null
-
-    private val binding get() = _binding!!
-
-    override fun onCreateView(
-        inflater: LayoutInflater,
-        container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View {
-        val notificationsViewModel =
-            ViewModelProvider(this)[NotificationsViewModel::class.java]
-
-        _binding = FragmentNotificationsBinding.inflate(inflater, container, false)
-        val root: View = binding.root
-
-        val textView: TextView = binding.textNotifications
-        notificationsViewModel.text.observe(viewLifecycleOwner) {
-            textView.text = it
-        }
-        return root
-    }
-
-    override fun onDestroyView() {
-        super.onDestroyView()
-        _binding = null
-    }
-}

+ 0 - 13
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/notifications/NotificationsViewModel.kt

@@ -1,13 +0,0 @@
-package com.hx.utils.application.ui.notifications
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-
-class NotificationsViewModel : ViewModel() {
-
-    private val _text = MutableLiveData<String>().apply {
-        value = "This is notifications Fragment"
-    }
-    val text: LiveData<String> = _text
-}

+ 222 - 0
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/sms/SmsFragment.kt

@@ -0,0 +1,222 @@
+package com.hx.utils.application.ui.sms
+
+import android.Manifest.permission.READ_SMS
+import android.Manifest.permission.RECEIVE_SMS
+import android.Manifest.permission.SEND_SMS
+import android.app.DatePickerDialog
+import android.app.TimePickerDialog
+import android.app.role.RoleManager
+import android.content.ContentValues
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.provider.Telephony
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.blankj.utilcode.util.AppUtils
+import com.blankj.utilcode.util.PermissionUtils
+import com.blankj.utilcode.util.TimeUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.hx.utils.application.databinding.FragmentSmsBinding
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.Calendar
+
+class SmsFragment : Fragment() {
+    private var _binding: FragmentSmsBinding? = null
+    private val binding get() = _binding!!
+    private lateinit var smsViewModel: SmsViewModel
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentSmsBinding.inflate(inflater, container, false)
+        val root: View = binding.root
+        smsViewModel = ViewModelProvider(this)[SmsViewModel::class.java]
+        binding.loading.visibility = View.GONE
+
+        smsViewModel.showTime.observe(viewLifecycleOwner) {
+            binding.time.setText(it)
+        }
+
+        binding.insertMms.setOnClickListener {
+            val smsPackageName = Telephony.Sms.getDefaultSmsPackage(context)
+            val currentPackageName = context?.packageName
+            if (smsPackageName != currentPackageName) {
+                val intent: Intent?
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+                    intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
+                    intent.putExtra(
+                        Telephony.Sms.Intents.EXTRA_PACKAGE_NAME,
+                        requireContext().packageName
+                    )
+                    requireContext().startActivity(intent)
+                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                    openSMSappChooser()
+                }
+                return@setOnClickListener
+            }
+
+
+            val name = binding.name.text?.toString().orEmpty()
+            val message = binding.message.text?.toString().orEmpty()
+            if (name.isBlank()) {
+                binding.name.error = "Not empty!"
+                return@setOnClickListener
+            }
+
+            if (message.isBlank()) {
+                binding.message.error = "Not empty!"
+                return@setOnClickListener
+            }
+
+            binding.loading.visibility = View.VISIBLE
+            insertSms()
+
+        }
+
+        binding.selectTime.setOnClickListener {
+            val calendar = Calendar.getInstance()
+            val datePickerDialog = DatePickerDialog(
+                requireActivity(),
+                { _, year, month, dayOfMonth ->
+                    showTimePickerDialog(year, month, dayOfMonth)
+                },
+                calendar.get(Calendar.YEAR),
+                calendar.get(Calendar.MONTH),
+                calendar.get(Calendar.DAY_OF_MONTH)
+            )
+            datePickerDialog.show()
+        }
+
+        binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
+            smsViewModel.read.value = binding.radioButton1.id == checkedId
+        }
+
+        (activity as? AppCompatActivity)?.supportActionBar?.title = arguments?.getString("data")
+        return root
+    }
+
+    override fun onStart() {
+        super.onStart()
+        getPermissions()
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    fun getPermissions() {
+        val permissions = listOf(SEND_SMS, RECEIVE_SMS, READ_SMS).toTypedArray()
+        PermissionUtils.permission(*permissions)
+            .rationale { _, shouldRequest ->
+                run {
+                    shouldRequest.again(true)
+                }
+            }.callback(object : PermissionUtils.FullCallback {
+                override fun onGranted(permissionsGranted: MutableList<String>) {
+
+                }
+
+                override fun onDenied(forever: MutableList<String>, denied: MutableList<String>) {
+                    if (forever.isNotEmpty()) {
+                        AppUtils.launchAppDetailsSettings()
+                    } else {
+                        getPermissions()
+                    }
+                }
+            }).request()
+    }
+
+
+    @OptIn(DelicateCoroutinesApi::class)
+    private fun insertSms() {
+        try {
+            val values = ContentValues().apply {
+                put(Telephony.Sms.ADDRESS, binding.name.text.toString())
+                put(Telephony.Sms.DATE, smsViewModel.time.value)
+                put(Telephony.Sms.DATE_SENT, smsViewModel.time.value?.minus(5000))
+                put(Telephony.Sms.READ, smsViewModel.read.value)
+                put(Telephony.Sms.TYPE, 1)
+                put(Telephony.Sms.BODY, binding.message.text.toString())
+                put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_INBOX)
+            }
+            context?.contentResolver?.insert(Telephony.Sms.CONTENT_URI, values)
+
+            binding.name.text.clear()
+            binding.message.text.clear()
+            smsViewModel.time.postValue(0)
+            smsViewModel.showTime.postValue("")
+
+        } catch (e: Exception) {
+            e.message?.let {
+                GlobalScope.launch(Dispatchers.Main) {
+                    delay(2000)
+                    ToastUtils.showLong(it)
+                }
+            }
+        } finally {
+            GlobalScope.launch(Dispatchers.Main) {
+                delay(2000)
+                binding.loading.visibility = View.GONE
+            }
+        }
+
+    }
+
+    private fun showTimePickerDialog(year: Int, month: Int, dayOfMonth: Int) {
+        val calendar = Calendar.getInstance()
+        val timePickerDialog = TimePickerDialog(
+            requireActivity(),
+            { _, h, m ->
+                val selectedCalendar = Calendar.getInstance()
+                selectedCalendar.set(year, month, dayOfMonth, h, m)
+                val selectedTimestamp = selectedCalendar.timeInMillis
+
+                handleSelectedTimestamp(
+                    selectedTimestamp,
+                    TimeUtils.date2String(TimeUtils.millis2Date(selectedTimestamp))
+                )
+            },
+            calendar.get(Calendar.HOUR_OF_DAY),
+            calendar.get(Calendar.MINUTE),
+            true
+        )
+        timePickerDialog.show()
+    }
+
+    private fun handleSelectedTimestamp(timestamp: Long, showTxt: String) {
+        smsViewModel.time.postValue(timestamp)
+        smsViewModel.showTime.postValue(showTxt)
+
+
+    }
+
+    private fun openSMSappChooser() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            val roleManager = context?.getSystemService(RoleManager::class.java)
+            if (roleManager!!.isRoleAvailable(RoleManager.ROLE_SMS)) {
+                if (!roleManager.isRoleHeld(RoleManager.ROLE_SMS)) {
+                    val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS)
+                    startActivityForResult(intent, 1314)
+                }
+            }
+        } else {
+            val intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
+            intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context?.packageName)
+            startActivity(intent)
+        }
+    }
+
+
+}

+ 16 - 0
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/sms/SmsViewModel.kt

@@ -0,0 +1,16 @@
+package com.hx.utils.application.ui.sms
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class SmsViewModel : ViewModel() {
+    private val _time = MutableLiveData<Long>().apply {
+        value = 0L
+    }
+    val time: MutableLiveData<Long> = _time
+
+    val read: MutableLiveData<Boolean>
+        get() = MutableLiveData(false)
+
+    val showTime: MutableLiveData<String> = MutableLiveData()
+}

+ 12 - 0
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/web/UrlFormState.kt

@@ -0,0 +1,12 @@
+package com.hx.utils.application.ui.web
+
+/**
+ * Data validation state of the login form.
+ */
+data class UrlFormState(
+    val username: String? = null,
+    val firstName: String? = null,
+    val url: String? = null,
+    val success: Boolean? = false,
+    val error: String? = null
+)

+ 26 - 7
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/web/WebFragment.kt

@@ -26,8 +26,14 @@ class WebFragment : Fragment() {
         val root: View = binding.root
         webViewModel = ViewModelProvider(this)[WebViewModel::class.java]
 
-        webViewModel.mUrl.observe(viewLifecycleOwner) { url ->
-            showDialog()
+        webViewModel.urlFormState.observe(viewLifecycleOwner) {
+            binding.fab.show()
+            binding.loading.visibility = View.GONE
+            if (it.success == true)
+                successTip()
+            else
+                errorTip()
+
         }
         webView?.loadUrl("初始默认地址") ?: run {
             webView = BaseWebView(requireActivity()).apply {
@@ -41,6 +47,8 @@ class WebFragment : Fragment() {
             )
         }
         binding.fab.setOnClickListener {
+            binding.fab.hide()
+            binding.loading.visibility = View.VISIBLE
             webViewModel.startListening()
         }
         (activity as? AppCompatActivity)?.supportActionBar?.title = arguments?.getString("data")
@@ -52,23 +60,34 @@ class WebFragment : Fragment() {
         _binding = null
     }
 
-    private fun showDialog() {
+    private fun successTip() {
         val dialog = AlertDialog.Builder(requireActivity())
             .setTitle("WebTestBot Message")
             .setMessage(
-                "URL: ${webViewModel.mUrl.value}\nName: ${webViewModel.mName.value}\nFirst Name: ${webViewModel.mFirstName.value}"
+                "URL: ${webViewModel.urlFormState.value!!.url}\nName: ${webViewModel.urlFormState.value!!.username}\nFirst Name: ${webViewModel.urlFormState.value!!.firstName}"
             )
             .setPositiveButton("Go") { dialogInterface, _ ->
-                webView?.loadUrl(webViewModel.mUrl.value)
+                webView?.loadUrl(webViewModel.urlFormState.value!!.url)
                 dialogInterface.dismiss()
             }
             .setNegativeButton("Cancel") { dialogInterface, _ ->
                 dialogInterface.dismiss()
             }
             .create()
-
-        // 显示对话框
         dialog.show()
     }
 
+    private fun errorTip() {
+        val dialog = AlertDialog.Builder(requireActivity())
+            .setTitle("Error Info")
+            .setMessage(
+                webViewModel.urlFormState.value!!.error
+            )
+            .setPositiveButton("Ok") { dialogInterface, _ ->
+                webView?.loadUrl(webViewModel.urlFormState.value!!.url)
+                dialogInterface.dismiss()
+            }
+            .create()
+        dialog.show()
+    }
 }

+ 37 - 11
UtilsApplication/app/app/src/main/java/com/hx/utils/application/ui/web/WebViewModel.kt

@@ -1,6 +1,6 @@
 package com.hx.utils.application.ui.web
 
-import android.util.Log
+import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import com.hx.utils.application.util.TgUtils
@@ -10,25 +10,51 @@ import kotlinx.coroutines.launch
 import org.json.JSONObject
 
 class WebViewModel : ViewModel() {
-    val mUrl = MutableLiveData<String>()
-    val mName = MutableLiveData<String>()
-    val mFirstName = MutableLiveData<String>()
+    private val _urlForm = MutableLiveData<UrlFormState>()
+    val urlFormState: LiveData<UrlFormState> = _urlForm
+
     fun startListening() {
         GlobalScope.launch(Dispatchers.IO) {
             try {
                 TgUtils().getUpdatesSync()?.let {
                     val data = JSONObject(it)
-                    mUrl.postValue(data.getJSONObject("message").get("text").toString())
-                    mName.postValue(
-                        data.getJSONObject("message").getJSONObject("chat").getString("username")
-                    )
-                    mFirstName.postValue(
-                        data.getJSONObject("message").getJSONObject("chat").getString("first_name")
+                    _urlForm.postValue(
+                        UrlFormState(
+                            username = data.getJSONObject("message").getJSONObject("chat")
+                                .getString("username"),
+                            firstName = data.getJSONObject("message").getJSONObject("chat")
+                                .getString("first_name"),
+                            url = data.getJSONObject("message").get("text").toString(),
+                            success = true,
+                            error = null
+                        )
                     )
                 }
 
             } catch (e: Exception) {
-                Log.e("hzshkj", "startListening: ", e)
+                e.message?.let {
+                    _urlForm.postValue(
+                        UrlFormState(
+                            username = "Error",
+                            firstName = "Error",
+                            url = "Error",
+                            success = false,
+                            error = it
+                        )
+                    )
+                } ?: run {
+                    _urlForm.postValue(
+                        UrlFormState(
+                            username = "Error",
+                            firstName = "Error",
+                            url = "Error",
+                            success = false,
+                            error = "Error"
+                        )
+                    )
+                }
+
+
             }
         }
 

+ 16 - 0
UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/HeadlessSmsSendService.java

@@ -0,0 +1,16 @@
+package com.hx.utils.application.util;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+
+public class HeadlessSmsSendService extends Service {
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}

+ 15 - 0
UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/MmsReceiver.java

@@ -0,0 +1,15 @@
+package com.hx.utils.application.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Handle incoming Mms transaction finished.
+ */
+public class MmsReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+
+    }
+}

+ 13 - 0
UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/SmsReceiver.java

@@ -0,0 +1,13 @@
+
+package com.hx.utils.application.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class SmsReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+
+    }
+}

+ 1 - 48
UtilsApplication/app/app/src/main/java/com/hx/utils/application/util/TgUtils.java

@@ -2,18 +2,9 @@ package com.hx.utils.application.util;
 
 import android.util.Log;
 
-import androidx.annotation.NonNull;
-
-import com.blankj.utilcode.util.SPUtils;
-import com.blankj.utilcode.util.StringUtils;
-
 import org.json.JSONArray;
 import org.json.JSONObject;
 
-import java.io.IOException;
-
-import okhttp3.Call;
-import okhttp3.Callback;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.Response;
@@ -21,44 +12,6 @@ import okhttp3.Response;
 public class TgUtils {
     private static final String BASE_URL = "https://api.telegram.org/bot7790802518:AAGmUCIdh-uogRuG4gElgxTG-yN7f0mGi7I/";
     private final OkHttpClient client = new OkHttpClient();
-
-    public void getUpdates() {
-        String url = BASE_URL + "getUpdates?offset=-1&limit=1";
-
-        Request request = new Request.Builder()
-                .url(url)
-                .build();
-
-        client.newCall(request).enqueue(new Callback() {
-            @Override
-            public void onFailure(@NonNull Call call, @NonNull IOException e) {
-                Log.e("hzshkj", "onFailure: ", e);
-            }
-
-            @Override
-            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
-                if (response.isSuccessful()) {
-                    String jsonResponse = response.body().string();
-                    try {
-                        Log.d("hzshkj", "[TgUtils] onResponse: " + jsonResponse);
-                        JSONObject jsonObject = new JSONObject(jsonResponse);
-                        JSONArray resultArray = jsonObject.getJSONArray("result");
-
-                        for (int i = 0; i < resultArray.length(); i++) {
-                            JSONObject message = resultArray.getJSONObject(i).getJSONObject("message");
-                            String text = message.getString("text");
-                            int msgId = message.getInt("message_id");
-                            SPUtils.getInstance().put("Message", text);
-                            SPUtils.getInstance().put("MessageId", msgId);
-                        }
-                    } catch (Exception e) {
-                        Log.e("hzshkj", "onResponse: ", e);
-                    }
-                }
-            }
-        });
-    }
-
     public String getUpdatesSync() throws Throwable {
         String url = BASE_URL + "getUpdates?offset=-1&limit=10";
         Request request = new Request.Builder().url(url).build();
@@ -68,7 +21,7 @@ public class TgUtils {
             JSONObject jsonObject = new JSONObject(jsonResponse);
             JSONArray resultArray = jsonObject.getJSONArray("result");
             JSONObject data = new JSONObject(resultArray.get(0).toString());
-            Log.d("hzshkj", "[TgUtils] getUpdatesSync: "+data);
+            Log.d("hzshkj", "[TgUtils] getUpdatesSync: " + data);
             return data.toString();
         } else {
             Log.e("hzshkj", "Request Failed: " + (response.body() != null ? response.body().string() : "No response body"));

+ 7 - 0
UtilsApplication/app/app/src/main/res/drawable/twotone_connected_tv_24.xml

@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M20,3H4C2.9,3 2,3.9 2,5v12c0,1.1 0.9,2 2,2h4v2h8v-2h4c1.1,0 1.99,-0.9 1.99,-2L22,5C22,3.9 21.1,3 20,3zM20,17H4V5h16V17zM5,14v2h2C7,14.89 6.11,14 5,14zM5,11v1.43c1.97,0 3.57,1.6 3.57,3.57H10C10,13.24 7.76,11 5,11zM5,8v1.45c3.61,0 6.55,2.93 6.55,6.55H13C13,11.58 9.41,8 5,8z"/>
+      
+    <path android:fillAlpha="0.3" android:fillColor="@android:color/white" android:pathData="M4,5h16v12h-16z" android:strokeAlpha="0.3"/>
+    
+</vector>

+ 7 - 0
UtilsApplication/app/app/src/main/res/drawable/twotone_email_24.xml

@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillAlpha="0.3" android:fillColor="@android:color/white" android:pathData="M20,8l-8,5 -8,-5v10h16zM20,6L4,6l8,4.99z" android:strokeAlpha="0.3"/>
+      
+    <path android:fillColor="@android:color/white" android:pathData="M4,20h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2zM20,6l-8,4.99L4,6h16zM4,8l8,5 8,-5v10H4V8z"/>
+    
+</vector>

+ 7 - 0
UtilsApplication/app/app/src/main/res/drawable/twotone_watch_later_24.xml

@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillAlpha="0.3" android:fillColor="@android:color/white" android:pathData="M12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8s8,-3.59 8,-8S16.41,4 12,4zM16.2,16.2L11,13V7h1.5v5.2l4.5,2.7L16.2,16.2z" android:strokeAlpha="0.3"/>
+      
+    <path android:fillColor="@android:color/white" android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20zM12.5,7H11v6l5.2,3.2l0.8,-1.3l-4.5,-2.7V7z"/>
+    
+</vector>

+ 8 - 2
UtilsApplication/app/app/src/main/res/layout/activity_main.xml

@@ -12,12 +12,17 @@
         android:fitsSystemWindows="true"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent">
+        app:layout_constraintTop_toTopOf="parent"
+        app:background="?android:attr/windowBackground"
+        app:liftOnScroll="true">
+
 
         <com.google.android.material.appbar.MaterialToolbar
             android:id="@+id/toolbar"
             android:layout_width="match_parent"
-            android:layout_height="?attr/actionBarSize" />
+            android:layout_height="wrap_content"
+            app:background="?android:attr/windowBackground"
+            android:minHeight="?attr/actionBarSize" />
 
     </com.google.android.material.appbar.AppBarLayout>
 
@@ -28,6 +33,7 @@
         android:layout_marginStart="0dp"
         android:layout_marginEnd="0dp"
         android:background="?android:attr/windowBackground"
+        android:tint="?attr/colorControlNormal"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"

+ 1 - 0
UtilsApplication/app/app/src/main/res/layout/fragment_home.xml

@@ -4,6 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:background="@color/white"
     tools:context=".ui.home.HomeFragment">
 
 

+ 143 - 0
UtilsApplication/app/app/src/main/res/layout/fragment_sms.xml

@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.dashboard.DashboardFragment">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+
+            <EditText
+                android:id="@+id/name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="15dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginEnd="15dp"
+                android:hint="发送者名字或者号码/通话号码"
+                android:lines="2" />
+
+            <EditText
+                android:id="@+id/message"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="15dp"
+                android:layout_marginEnd="15dp"
+                android:gravity="top|start"
+                android:hint="短信内容"
+                android:inputType="textMultiLine"
+                android:maxLines="20"
+                android:minLines="10"
+                android:scrollbars="vertical" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="15dp"
+                android:layout_marginEnd="15dp"
+                android:text="短信设置是否已读:"
+                android:textColor="#000000"
+                android:textSize="13sp" />
+
+            <RadioGroup
+                android:id="@+id/radioGroup"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="15dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginEnd="15dp"
+                android:orientation="horizontal">
+
+                <RadioButton
+                    android:id="@+id/radioButton1"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:checked="true"
+                    android:tag="1"
+                    android:text="已读"
+                    android:textColor="#000000" />
+
+
+                <RadioButton
+                    android:id="@+id/radioButton2"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="20dp"
+                    android:tag="0"
+                    android:text="未读"
+                    android:textColor="#000000" />
+            </RadioGroup>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:orientation="horizontal">
+
+                <EditText
+                    android:id="@+id/time"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="15dp"
+                    android:layout_marginEnd="10dp"
+                    android:enabled="false"
+                    android:focusable="false"
+                    android:hint="短信发送/通话 时间"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toStartOf="@+id/selectTime"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <Button
+                    android:id="@+id/selectTime"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginEnd="15dp"
+                    android:layout_marginBottom="10dp"
+                    android:text="选择时间"
+                    android:textColor="#ffffff"
+                    android:textSize="13sp"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <Button
+                android:id="@+id/insert_mms"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="15dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginEnd="15dp"
+                android:text="插入短信"
+                android:textColor="#ffffff"
+                android:textSize="13sp" />
+
+
+        </LinearLayout>
+
+        <com.google.android.material.loadingindicator.LoadingIndicator
+            android:id="@+id/loading"
+            style="@style/Widget.Material3.LoadingIndicator.Contained"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.core.widget.NestedScrollView>

+ 15 - 3
UtilsApplication/app/app/src/main/res/layout/fragment_web.xml

@@ -17,14 +17,26 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <com.google.android.material.floatingactionbutton.FloatingActionButton
+    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
         android:id="@+id/fab"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="bottom|end"
         android:layout_margin="20dp"
         android:importantForAccessibility="no"
+        android:text="@string/telegram_message"
+        app:icon="@drawable/twotone_email_24"
+
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:srcCompat="@android:drawable/ic_dialog_info" />
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <com.google.android.material.loadingindicator.LoadingIndicator
+        android:id="@+id/loading"
+        style="@style/Widget.Material3.LoadingIndicator.Contained"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+
+        android:layout_margin="20dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 9 - 6
UtilsApplication/app/app/src/main/res/layout/item_menu.xml

@@ -3,12 +3,13 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/ll_root_view"
-    android:layout_width="wrap_content"
+    android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_margin="5dp"
     android:orientation="vertical"
-    app:cardCornerRadius="12dp"
-    app:cardElevation="4dp">
+    app:cardBackgroundColor="@color/white"
+    app:cardCornerRadius="15dp"
+    app:cardElevation="2dp">
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -18,15 +19,17 @@
 
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/name"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
+            android:drawableTop="@drawable/twotone_connected_tv_24"
+            android:drawablePadding="5dp"
+            android:gravity="center"
             android:textColor="#000000"
-            android:textSize="13sp"
+            android:textSize="15sp"
             android:textStyle="bold"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent"
-
             tools:text="menu1" />
     </LinearLayout>
 

+ 0 - 4
UtilsApplication/app/app/src/main/res/menu/bottom_nav_menu.xml

@@ -11,9 +11,5 @@
         android:icon="@drawable/ic_dashboard_black_24dp"
         android:title="@string/title_dashboard" />
 
-    <item
-        android:id="@+id/navigation_notifications"
-        android:icon="@drawable/ic_notifications_black_24dp"
-        android:title="@string/title_notifications" />
 
 </menu>

+ 7 - 7
UtilsApplication/app/app/src/main/res/navigation/mobile_navigation.xml

@@ -17,16 +17,16 @@
         android:label="@string/title_dashboard"
         tools:layout="@layout/fragment_dashboard" />
 
-    <fragment
-        android:id="@+id/navigation_notifications"
-        android:name="com.hx.utils.application.ui.notifications.NotificationsFragment"
-        android:label="@string/title_notifications"
-        tools:layout="@layout/fragment_notifications" />
 
     <fragment
         android:id="@+id/navigation_web"
         android:name="com.hx.utils.application.ui.web.WebFragment"
         app:destination="@+id/webFragment"
-        tools:layout="@layout/fragment_web">
-    </fragment>
+        tools:layout="@layout/fragment_web"/>
+
+    <fragment
+        android:id="@+id/navigation_sms"
+        android:name="com.hx.utils.application.ui.sms.SmsFragment"
+        app:destination="@+id/SmsFragment"
+        tools:layout="@layout/fragment_sms"/>
 </navigation>

+ 1 - 1
UtilsApplication/app/app/src/main/res/values-night/themes.xml

@@ -1,6 +1,6 @@
 <resources xmlns:tools="http://schemas.android.com/tools">
     <!-- Base application theme. -->
-    <style name="Theme.MyDemoApplication" parent="Theme.MaterialComponents.DayNight.NoActionBar">
+    <style name="Theme.MyDemoApplication" parent="Theme.Material3.DayNight.NoActionBar">
         <!-- Primary brand color. -->
         <item name="colorPrimary">@color/purple_200</item>
         <item name="colorPrimaryVariant">@color/purple_700</item>

+ 4 - 42
UtilsApplication/app/app/src/main/res/values/strings.xml

@@ -3,47 +3,9 @@
     <string name="title_home">Home</string>
     <string name="title_dashboard">Dashboard</string>
     <string name="title_notifications">Notifications</string>
-    <string name="webview_test">webView Test</string>
-    <!-- Strings used for fragments for navigation -->
-    <string name="first_fragment_label">First Fragment</string>
-    <string name="second_fragment_label">Second Fragment</string>
-    <string name="next">Next</string>
-    <string name="previous">Previous</string>
+    <string name="webview_test">WebView Test</string>
+    <string name="telegram_message">Telegram Message</string>
+    <string name="sms_tool">SMS tool</string>
+
 
-    <string name="lorem_ipsum">
-        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
-        volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
-        dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
-        litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
-        diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
-        ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
-        Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
-        egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
-        neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
-        fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
-        molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
-        bibendum, vel congue leo egestas.\n\n
-        Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
-        amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
-        molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
-        interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
-        lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
-        in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
-        est.\n\n
-        Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
-        Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
-        non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
-        eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
-        quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
-        ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
-        placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
-        convallis.\n\n
-        Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
-        malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
-        gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
-        libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
-        sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
-        libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
-        vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
-    </string>
 </resources>

+ 1 - 1
UtilsApplication/app/app/src/main/res/values/themes.xml

@@ -1,5 +1,5 @@
 <resources>
-    <style name="Theme.MyDemoApplication" parent="Theme.MaterialComponents.DayNight.NoActionBar">
+    <style name="Theme.MyDemoApplication" parent="Theme.Material3.Dark.NoActionBar">
         <!-- Primary brand color. -->
         <item name="colorPrimary">@color/purple_500</item>
         <item name="colorPrimaryVariant">@color/purple_700</item>

+ 3 - 2
UtilsApplication/app/gradle/libs.versions.toml

@@ -6,7 +6,7 @@ junit = "4.13.2"
 junitVersion = "1.2.1"
 espressoCore = "3.6.1"
 appcompat = "1.7.0"
-material = "1.12.0"
+material = "1.13.0-alpha08"
 constraintlayout = "2.2.0"
 lifecycleLivedataKtx = "2.8.7"
 lifecycleViewmodelKtx = "2.8.7"
@@ -29,7 +29,8 @@ utilcodex = 'com.blankj:utilcodex:1.31.0'
 navigation = 'androidx.navigation:navigation-runtime-ktx:2.8.4'
 okhttp3 = 'com.squareup.okhttp3:okhttp:4.12.0'
 okhttp3-logging-interceptor = 'com.squareup.okhttp3:logging-interceptor:4.12.0'
-kotlinx-coroutines-android = 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
+kotlinx-coroutines-android = 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
+com-github-getActivity-XXPermissions = 'com.github.getActivity:XXPermissions:16.6'
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }

+ 1 - 0
demo/.idea/gradle.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>

+ 1 - 0
demo/app/build.gradle

@@ -48,6 +48,7 @@ dependencies {
     implementation libs.androidx.legacy.support.v4
     implementation libs.androidx.recyclerview
     implementation libs.androidx.preference
+    implementation libs.androidx.annotation
     testImplementation libs.junit
     androidTestImplementation libs.androidx.junit
     androidTestImplementation libs.androidx.espresso.core

+ 4 - 0
demo/app/src/main/AndroidManifest.xml

@@ -12,6 +12,10 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.MyDemoApplication"
         tools:targetApi="31">
+        <activity
+            android:name=".ui.login.LoginActivity"
+            android:exported="false"
+            android:label="@string/title_activity_login" />
         <activity
             android:name=".SettingsActivity"
             android:exported="false"

+ 24 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/LoginDataSource.kt

@@ -0,0 +1,24 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.data
+
+import com.hx.utils.mydemoapplication.ui.dashboard.data.model.LoggedInUser
+import java.io.IOException
+
+/**
+ * Class that handles authentication w/ login credentials and retrieves user information.
+ */
+class LoginDataSource {
+
+    fun login(username: String, password: String): Result<LoggedInUser> {
+        try {
+            // TODO: handle loggedInUser authentication
+            val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
+            return Result.Success(fakeUser)
+        } catch (e: Throwable) {
+            return Result.Error(IOException("Error logging in", e))
+        }
+    }
+
+    fun logout() {
+        // TODO: revoke authentication
+    }
+}

+ 46 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/LoginRepository.kt

@@ -0,0 +1,46 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.data
+
+import com.hx.utils.mydemoapplication.ui.dashboard.data.model.LoggedInUser
+
+/**
+ * Class that requests authentication and user information from the remote data source and
+ * maintains an in-memory cache of login status and user credentials information.
+ */
+
+class LoginRepository(val dataSource: LoginDataSource) {
+
+    // in-memory cache of the loggedInUser object
+    var user: LoggedInUser? = null
+        private set
+
+    val isLoggedIn: Boolean
+        get() = user != null
+
+    init {
+        // If user credentials will be cached in local storage, it is recommended it be encrypted
+        // @see https://developer.android.com/training/articles/keystore
+        user = null
+    }
+
+    fun logout() {
+        user = null
+        dataSource.logout()
+    }
+
+    fun login(username: String, password: String): Result<LoggedInUser> {
+        // handle login
+        val result = dataSource.login(username, password)
+
+        if (result is Result.Success) {
+            setLoggedInUser(result.data)
+        }
+
+        return result
+    }
+
+    private fun setLoggedInUser(loggedInUser: LoggedInUser) {
+        this.user = loggedInUser
+        // If user credentials will be cached in local storage, it is recommended it be encrypted
+        // @see https://developer.android.com/training/articles/keystore
+    }
+}

+ 18 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/Result.kt

@@ -0,0 +1,18 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.data
+
+/**
+ * A generic class that holds a value with its loading status.
+ * @param <T>
+ */
+sealed class Result<out T : Any> {
+
+    data class Success<out T : Any>(val data: T) : Result<T>()
+    data class Error(val exception: Exception) : Result<Nothing>()
+
+    override fun toString(): String {
+        return when (this) {
+            is Success<*> -> "Success[data=$data]"
+            is Error -> "Error[exception=$exception]"
+        }
+    }
+}

+ 9 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/data/model/LoggedInUser.kt

@@ -0,0 +1,9 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.data.model
+
+/**
+ * Data class that captures user information for logged in users retrieved from LoginRepository
+ */
+data class LoggedInUser(
+    val userId: String,
+    val displayName: String
+)

+ 9 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoggedInUserView.kt

@@ -0,0 +1,9 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.ui.login
+
+/**
+ * User details post authentication that is exposed to the UI
+ */
+data class LoggedInUserView(
+    val displayName: String
+    //... other data fields that may be accessible to the UI
+)

+ 130 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginActivity.kt

@@ -0,0 +1,130 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.ui.login
+
+import android.app.Activity
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import android.os.Bundle
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AppCompatActivity
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
+import android.widget.Toast
+import com.hx.utils.mydemoapplication.databinding.ActivityLoginBinding
+
+import com.hx.utils.mydemoapplication.ui.dashboard.R
+
+class LoginActivity : AppCompatActivity() {
+
+    private lateinit var loginViewModel: LoginViewModel
+    private lateinit var binding: ActivityLoginBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        binding = ActivityLoginBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        val username = binding.username
+        val password = binding.password
+        val login = binding.login
+        val loading = binding.loading
+
+        loginViewModel = ViewModelProvider(this, LoginViewModelFactory())
+            .get(LoginViewModel::class.java)
+
+        loginViewModel.loginFormState.observe(this@LoginActivity, Observer {
+            val loginState = it ?: return@Observer
+
+            // disable login button unless both username / password is valid
+            login.isEnabled = loginState.isDataValid
+
+            if (loginState.usernameError != null) {
+                username.error = getString(loginState.usernameError)
+            }
+            if (loginState.passwordError != null) {
+                password.error = getString(loginState.passwordError)
+            }
+        })
+
+        loginViewModel.loginResult.observe(this@LoginActivity, Observer {
+            val loginResult = it ?: return@Observer
+
+            loading.visibility = View.GONE
+            if (loginResult.error != null) {
+                showLoginFailed(loginResult.error)
+            }
+            if (loginResult.success != null) {
+                updateUiWithUser(loginResult.success)
+            }
+            setResult(Activity.RESULT_OK)
+
+            //Complete and destroy login activity once successful
+            finish()
+        })
+
+        username.afterTextChanged {
+            loginViewModel.loginDataChanged(
+                username.text.toString(),
+                password.text.toString()
+            )
+        }
+
+        password.apply {
+            afterTextChanged {
+                loginViewModel.loginDataChanged(
+                    username.text.toString(),
+                    password.text.toString()
+                )
+            }
+
+            setOnEditorActionListener { _, actionId, _ ->
+                when (actionId) {
+                    EditorInfo.IME_ACTION_DONE ->
+                        loginViewModel.login(
+                            username.text.toString(),
+                            password.text.toString()
+                        )
+                }
+                false
+            }
+
+            login.setOnClickListener {
+                loading.visibility = View.VISIBLE
+                loginViewModel.login(username.text.toString(), password.text.toString())
+            }
+        }
+    }
+
+    private fun updateUiWithUser(model: LoggedInUserView) {
+        val welcome = getString(R.string.welcome)
+        val displayName = model.displayName
+        // TODO : initiate successful logged in experience
+        Toast.makeText(
+            applicationContext,
+            "$welcome $displayName",
+            Toast.LENGTH_LONG
+        ).show()
+    }
+
+    private fun showLoginFailed(@StringRes errorString: Int) {
+        Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
+    }
+}
+
+/**
+ * Extension function to simplify setting an afterTextChanged action to EditText components.
+ */
+fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
+    this.addTextChangedListener(object : TextWatcher {
+        override fun afterTextChanged(editable: Editable?) {
+            afterTextChanged.invoke(editable.toString())
+        }
+
+        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+
+        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
+    })
+}

+ 10 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginFormState.kt

@@ -0,0 +1,10 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.ui.login
+
+/**
+ * Data validation state of the login form.
+ */
+data class LoginFormState(
+    val usernameError: Int? = null,
+    val passwordError: Int? = null,
+    val isDataValid: Boolean = false
+)

+ 9 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginResult.kt

@@ -0,0 +1,9 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.ui.login
+
+/**
+ * Authentication result : success (user details) or error message.
+ */
+data class LoginResult(
+    val success: LoggedInUserView? = null,
+    val error: Int? = null
+)

+ 55 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginViewModel.kt

@@ -0,0 +1,55 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.ui.login
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import android.util.Patterns
+import com.hx.utils.mydemoapplication.ui.dashboard.data.LoginRepository
+import com.hx.utils.mydemoapplication.ui.dashboard.data.Result
+
+import com.hx.utils.mydemoapplication.ui.dashboard.R
+
+class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
+
+    private val _loginForm = MutableLiveData<LoginFormState>()
+    val loginFormState: LiveData<LoginFormState> = _loginForm
+
+    private val _loginResult = MutableLiveData<LoginResult>()
+    val loginResult: LiveData<LoginResult> = _loginResult
+
+    fun login(username: String, password: String) {
+        // can be launched in a separate asynchronous job
+        val result = loginRepository.login(username, password)
+
+        if (result is Result.Success) {
+            _loginResult.value =
+                LoginResult(success = LoggedInUserView(displayName = result.data.displayName))
+        } else {
+            _loginResult.value = LoginResult(error = R.string.login_failed)
+        }
+    }
+
+    fun loginDataChanged(username: String, password: String) {
+        if (!isUserNameValid(username)) {
+            _loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
+        } else if (!isPasswordValid(password)) {
+            _loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
+        } else {
+            _loginForm.value = LoginFormState(isDataValid = true)
+        }
+    }
+
+    // A placeholder username validation check
+    private fun isUserNameValid(username: String): Boolean {
+        return if (username.contains('@')) {
+            Patterns.EMAIL_ADDRESS.matcher(username).matches()
+        } else {
+            username.isNotBlank()
+        }
+    }
+
+    // A placeholder password validation check
+    private fun isPasswordValid(password: String): Boolean {
+        return password.length > 5
+    }
+}

+ 25 - 0
demo/app/src/main/java/com/hx/utils/mydemoapplication/ui/dashboard/ui/login/LoginViewModelFactory.kt

@@ -0,0 +1,25 @@
+package com.hx.utils.mydemoapplication.ui.dashboard.ui.login
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.hx.utils.mydemoapplication.ui.dashboard.data.LoginDataSource
+import com.hx.utils.mydemoapplication.ui.dashboard.data.LoginRepository
+
+/**
+ * ViewModel provider factory to instantiate LoginViewModel.
+ * Required given LoginViewModel has a non-empty constructor
+ */
+class LoginViewModelFactory : ViewModelProvider.Factory {
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
+            return LoginViewModel(
+                loginRepository = LoginRepository(
+                    dataSource = LoginDataSource()
+                )
+            ) as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class")
+    }
+}

+ 69 - 0
demo/app/src/main/res/layout-w1240dp/activity_login.xml

@@ -0,0 +1,69 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    tools:context=".ui.dashboard.ui.login.LoginActivity">
+
+    <EditText
+        android:id="@+id/username"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="96dp"
+        android:hint="@string/prompt_email"
+        android:inputType="textEmailAddress"
+        android:selectAllOnFocus="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <EditText
+        android:id="@+id/password"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:hint="@string/prompt_password"
+        android:imeActionLabel="@string/action_sign_in_short"
+        android:imeOptions="actionDone"
+        android:inputType="textPassword"
+        android:selectAllOnFocus="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/username" />
+
+    <Button
+        android:id="@+id/login"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:layout_marginTop="16dp"
+        android:layout_marginBottom="64dp"
+        android:enabled="false"
+        android:text="@string/action_sign_in"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/password"
+        app:layout_constraintVertical_bias="0.2" />
+
+    <ProgressBar
+        android:id="@+id/loading"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="64dp"
+        android:layout_marginBottom="64dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@+id/password"
+        app:layout_constraintStart_toStartOf="@+id/password"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.3" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 76 - 0
demo/app/src/main/res/layout-w936dp/activity_login.xml

@@ -0,0 +1,76 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    tools:context=".ui.dashboard.ui.login.LoginActivity">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="840dp"
+        android:layout_height="match_parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent">
+
+        <EditText
+            android:id="@+id/username"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="96dp"
+            android:hint="@string/prompt_email"
+            android:inputType="textEmailAddress"
+            android:selectAllOnFocus="true"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <EditText
+            android:id="@+id/password"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:hint="@string/prompt_password"
+            android:imeActionLabel="@string/action_sign_in_short"
+            android:imeOptions="actionDone"
+            android:inputType="textPassword"
+            android:selectAllOnFocus="true"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/username" />
+
+        <Button
+            android:id="@+id/login"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start"
+            android:layout_marginTop="16dp"
+            android:layout_marginBottom="64dp"
+            android:enabled="false"
+            android:text="@string/action_sign_in"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/password"
+            app:layout_constraintVertical_bias="0.2" />
+
+        <ProgressBar
+            android:id="@+id/loading"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginTop="64dp"
+            android:layout_marginBottom="64dp"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="@+id/password"
+            app:layout_constraintStart_toStartOf="@+id/password"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.3" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 69 - 0
demo/app/src/main/res/layout/activity_login.xml

@@ -0,0 +1,69 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    tools:context=".ui.dashboard.ui.login.LoginActivity">
+
+    <EditText
+        android:id="@+id/username"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="96dp"
+        android:hint="@string/prompt_email"
+        android:inputType="textEmailAddress"
+        android:selectAllOnFocus="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <EditText
+        android:id="@+id/password"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:hint="@string/prompt_password"
+        android:imeActionLabel="@string/action_sign_in_short"
+        android:imeOptions="actionDone"
+        android:inputType="textPassword"
+        android:selectAllOnFocus="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/username" />
+
+    <Button
+        android:id="@+id/login"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:layout_marginTop="16dp"
+        android:layout_marginBottom="64dp"
+        android:enabled="false"
+        android:text="@string/action_sign_in"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/password"
+        app:layout_constraintVertical_bias="0.2" />
+
+    <ProgressBar
+        android:id="@+id/loading"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="64dp"
+        android:layout_marginBottom="64dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@+id/password"
+        app:layout_constraintStart_toStartOf="@+id/password"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.3" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 0
demo/app/src/main/res/values-land/dimens.xml

@@ -1,4 +1,5 @@
 <resources>
     <dimen name="fab_margin">48dp</dimen>
     <dimen name="text_margin">48dp</dimen>
+    <dimen name="activity_horizontal_margin">48dp</dimen>
 </resources>

+ 1 - 0
demo/app/src/main/res/values-w1240dp/dimens.xml

@@ -1,3 +1,4 @@
 <resources>
     <dimen name="fab_margin">200dp</dimen>
+    <dimen name="activity_horizontal_margin">200dp</dimen>
 </resources>

+ 1 - 0
demo/app/src/main/res/values-w600dp/dimens.xml

@@ -1,4 +1,5 @@
 <resources>
     <dimen name="fab_margin">48dp</dimen>
     <dimen name="text_margin">48dp</dimen>
+    <dimen name="activity_horizontal_margin">48dp</dimen>
 </resources>

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

@@ -152,4 +152,13 @@
     <string name="attachment_summary_on">Automatically download attachments for incoming emails
     </string>
     <string name="attachment_summary_off">Only download attachments when manually requested</string>
+    <string name="title_activity_login">LoginActivity</string>
+    <string name="prompt_email">Email</string>
+    <string name="prompt_password">Password</string>
+    <string name="action_sign_in">Sign in or register</string>
+    <string name="action_sign_in_short">Sign in</string>
+    <string name="welcome">"Welcome !"</string>
+    <string name="invalid_username">Not a valid username</string>
+    <string name="invalid_password">Password must be >5 characters</string>
+    <string name="login_failed">"Login failed"</string>
 </resources>

+ 2 - 0
demo/gradle/libs.versions.toml

@@ -15,6 +15,7 @@ navigationUiKtx = "2.8.4"
 legacySupportV4 = "1.0.0"
 recyclerview = "1.3.2"
 preference = "1.2.0"
+annotation = "1.9.1"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -31,6 +32,7 @@ androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation
 androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
 androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
 androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
+androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }