iT邦幫忙

2021 iThome 鐵人賽

DAY 27
0
Mobile Development

花30天做個Android小專案系列 第 27

Day27 - 登出及連線中斷

今天來做登出的功能以及連線中斷的處理。

Navigation Action

不論是登出還是中斷連線,在App中我最後都會希望回到登入畫面,因此在nav_main.xml中增加了Action:

<action
    android:id="@+id/action_to_loginFragment"
    app:destination="@id/loginFragment"
    app:enterAnim="@anim/nav_default_enter_anim"
    app:exitAnim="@anim/nav_default_exit_anim"
    app:popEnterAnim="@anim/nav_default_pop_enter_anim"
    app:popExitAnim="@anim/nav_default_pop_exit_anim"
    app:popUpTo="@id/searchArticleFragment"
    app:popUpToInclusive="true" />

此Action為Global Action,是放在navigation tag下而非fragment tag。

登出

登出發生的時間點比較單純,預期是在SearchArticleFragment頁面點擊返回按鈕的時候,所以先用onBackPressedDispatcher攔截返回事件並在裡面跳出Dialog詢問是否登出:

// ...
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,
    object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            AlertDialog.Builder(requireActivity())
                .setTitle("登出")
                .setMessage("是否登出Ptt?")
                .setPositiveButton(android.R.string.ok) { _, _ ->
                    //TODO: Logout Process
                }
                .setNegativeButton(android.R.string.cancel, null)
                .show()
        }
    })
// ...

Logout Process

// ...
val activity = (requireActivity() as MainActivity)
activity.showLoading("Logout...")
PttClient.getInstance().end()
activity.startPttClient {
    viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
        activity.dismissLoading()
        if (it == 0) {
            NavHostFragment.findNavController(this@SearchArticleFragment)
                .navigate(R.id.action_to_loginFragment)
        } else {
            activity.showConnectionErrorDialog()
        }
    }
}
// ...

可以看到登出的部分我不打算走Ptt的指令流程。直接將WebSocket連線切斷後重連就好了。MainActivity.startPttClient是稍微改寫WelcomeFragment頁的程式碼,並將PttClient.expect的值做為callback回傳。

MainActivity.startPttClient

public val startPattern = arrayOf(
    "請輸入代號,或以 guest 參觀,或以 new 註冊:"
)

public fun startPttClient(callback: ((Int) -> Unit)) {
    lifecycleScope.launch(Dispatchers.IO) {
        delay(500)
        PttClient.getInstance().start()
        callback(PttClient.getInstance().expect(startPattern))
    }
}

這邊做完後WelcomeFragment使用到的地方也一起做了修改:

private fun startPttClient() {
    if (checkOverlayDisplayPermission()) {
        (requireActivity() as MainActivity).startPttClient {
            viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
                delay(500)
                withContext(Dispatchers.Main) {
                    if (it == 0) {
                        val extras = FragmentNavigator.Extras.Builder()
                            .addSharedElement(binding.logo, "logo")
                            .build()

                        NavHostFragment.findNavController(this@WelcomeFragment)
                            .navigate(
                                R.id.action_welcomeFragment_to_loginFragment,
                                null,
                                null,
                                extras
                            )
                    } else {
                        (requireActivity() as MainActivity).showConnectionErrorDialog()
                    }
                }
            }
        }
    } else {
        requestOverlayDisplayPermission()
    }
}

連線中斷

連線中斷的模擬我是使用另外登入同帳號的方法。

首先在PttClient中加入判斷用的Flag和Callback

companion object {
    // ...
    private var isClosedByUser = false
    public var unexpectedClosedCallback: (() -> Unit)? = null
    // ...
}

isClosedByUser flag是用來標註這次的onClose事件是否是發生在App主動斷線(如:登出)時:

public fun start() {
    // ...
    isClosedByUser = false
    // ...
}
public fun end() {
    isClosedByUser = true
    // ...
}

並且在WebSocketClientonClose中呼叫unexpectedClosedCallback

override fun onClose(code: Int, reason: String?, remote: Boolean) {
    if (!isClosedByUser) {
        unexpectedClosedCallback?.invoke()
    }
}

在MainActivty中賦值unexpectedClosedCallback

PttClient.unexpectedClosedCallback = {
    lifecycleScope.launch(Dispatchers.Main) {
        AlertDialog.Builder(this@MainActivity)
            .setTitle("Disconnected")
            .setMessage("Reconnect to Ptt?")
            .setPositiveButton(android.R.string.ok) { _, _ ->
                showLoading("Reconnecting...")
                PttClient.getInstance().end()
                startPttClient {
                    lifecycleScope.launch(Dispatchers.Main) {
                        dismissLoading()
                        if (it == 0) {
                            findNavController(R.id.nav_host_fragment)
                                .navigate(R.id.action_to_loginFragment)
                        } else {
                            showConnectionErrorDialog()
                        }
                    }
                }
            }
            .setNegativeButton(android.R.string.cancel) { _, _ -> finish() }
            .setCancelable(false)
            .show()
    }
}

基本上就是走關閉→重啟→回到登入頁的流程,若不重連線或是再連線失敗的話就關閉App。

在ObserveService中賦值unexpectedClosedCallback

如果是在懸浮視窗中斷線的話,我選擇的做法是關閉Service並發送Notification告知使用者連線中斷。
以下內容寫在ObserveServiceonCreate方法中:

PttClient.unexpectedClosedCallback = {
    val manager = NotificationManagerCompat.from(this)
    if (manager.getNotificationChannel(notificationChannelIdImportant) == null) {
        val channel = NotificationChannelCompat.Builder(
            notificationChannelIdImportant,
            NotificationManager.IMPORTANCE_HIGH
        ).setName("CA_HIGH").build()
        manager.createNotificationChannel(channel)
    }

    val builder = NotificationCompat.Builder(this, notificationChannelIdImportant)
        .setSmallIcon(R.drawable.ic_outline_track)
        .setContentTitle("Ptt connection has closed")
    manager.notify(notificationIdImportant, builder.build())
    stopSelf()
}

今日功能畫面

今天測試的時間比較長,所以使用了三倍速播放,看不清楚又很有興趣的話,就麻煩多看幾次囉...XD
https://i.imgur.com/UkqU6TB.gif


上一篇
Day26 - 收放工具按鈕
下一篇
Day28 - 儲存帳密及自動登入
系列文
花30天做個Android小專案30

尚未有邦友留言

立即登入留言