Implement app shortcuts

Re: #127
This commit is contained in:
Gaurav Ujjwal 2023-02-21 15:26:16 +05:30
parent 68af560709
commit 7e2f91e3c4
6 changed files with 142 additions and 3 deletions

View file

@ -58,6 +58,8 @@
android:parentActivityName=".ui.home.HomeActivity"
android:windowSoftInputMode="stateVisible" />
<activity android:name=".ui.home.ShortcutActivity" />
<activity
android:name=".ui.vnc.VncActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"

View file

@ -26,6 +26,9 @@ interface ServerProfileDao {
@Query("SELECT * FROM profiles")
suspend fun getList(): List<ServerProfile>
@Query("SELECT * FROM profiles WHERE ID = :id")
suspend fun getByID(id: Long): ServerProfile?
@Query("SELECT * FROM profiles WHERE name = :name")
suspend fun getByName(name: String): List<ServerProfile>

View file

@ -12,10 +12,16 @@ import android.app.ActivityOptions
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Window
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.gaurav.avnc.R
import com.gaurav.avnc.databinding.ActivityHomeBinding
import com.gaurav.avnc.model.ServerProfile
@ -27,6 +33,8 @@ import com.gaurav.avnc.util.MsgDialog
import com.gaurav.avnc.viewmodel.HomeViewModel
import com.gaurav.avnc.vnc.VncClient
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Primary activity of the app.
@ -56,9 +64,10 @@ class HomeActivity : AppCompatActivity() {
//Observers
viewModel.editProfileEvent.observe(this) { showProfileEditor() }
viewModel.profileInsertedEvent.observe(this) { onProfileInserted(it) }
viewModel.profileDeletedEvent.observe(this) { showProfileDeletedMsg(it) }
viewModel.profileDeletedEvent.observe(this) { onProfileDeleted(it) }
viewModel.newConnectionEvent.observe(this) { startNewConnection(it) }
viewModel.discovery.servers.observe(this) { updateDiscoveryBadge(it) }
viewModel.serverProfiles.observe(this) { updateShortcuts(it) }
showWelcomeMsg()
}
@ -136,14 +145,26 @@ class HomeActivity : AppCompatActivity() {
}
/**
* Shows delete confirmation snackbar.
* Shows delete confirmation snackbar, allowing the user to Undo deletion.
*/
private fun showProfileDeletedMsg(profile: ServerProfile) {
private fun onProfileDeleted(profile: ServerProfile) {
val callback = object : Snackbar.Callback() {
override fun onDismissed(snackbar: Snackbar?, event: Int) {
if (event != DISMISS_EVENT_ACTION)
onProfileDeleteConfirmed(profile)
}
}
Snackbar.make(binding.root, R.string.msg_server_profile_deleted, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.title_undo)) { viewModel.insertProfile(profile) }
.addCallback(callback)
.show()
}
private fun onProfileDeleteConfirmed(profile: ServerProfile) {
disableShortcut(profile)
}
private fun updateDiscoveryBadge(list: List<ServerProfile>) {
tabs.updateDiscoveryBadge(list.size)
}
@ -172,4 +193,36 @@ class HomeActivity : AppCompatActivity() {
MsgDialog.show(supportFragmentManager, "Native library is missing!", msg)
}.isSuccess
}
private fun createShortcutId(profile: ServerProfile) = "shortcut:pid:${profile.ID}"
private fun updateShortcuts(profiles: List<ServerProfile>) {
val context = this
lifecycleScope.launch(Dispatchers.IO) {
runCatching {
val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context)
val shortcutProfiles = profiles.filter { it.name.isNotBlank() }.take(maxShortcuts)
val shortcuts = shortcutProfiles.map { p ->
val intent = ShortcutActivity.createIntent(context, p.ID)
ShortcutInfoCompat.Builder(context, createShortcutId(p))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_computer_shortcut))
.setShortLabel(p.name)
.setLongLabel("${p.name} (${p.host})")
.setIntent(intent)
.build()
}
ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts)
}.onFailure {
Log.e("Shortcuts", "Unable to update shortcuts", it)
Toast.makeText(context, "Unable to update shortcuts", Toast.LENGTH_SHORT).show()
}
}
}
private fun disableShortcut(profile: ServerProfile) {
val shortcutId = createShortcutId(profile)
val msg = getString(R.string.msg_shortcut_server_deleted)
ShortcutManagerCompat.disableShortcuts(this, listOf(shortcutId), msg)
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2023 Gaurav Ujjwal.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* See COPYING.txt for more details.
*/
package com.gaurav.avnc.ui.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.gaurav.avnc.model.db.MainDb
import com.gaurav.avnc.ui.vnc.startVncActivity
import kotlinx.coroutines.runBlocking
/**
* Used for launching app shortcuts.
*/
class ShortcutActivity : AppCompatActivity() {
companion object {
private const val PROFILE_ID_KEY = "com.gaurav.avnc.SHORTCUT_PROFILE_ID"
fun createIntent(context: Context, profileId: Long) = Intent(context, ShortcutActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra(PROFILE_ID_KEY, profileId)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val id = intent?.getLongExtra(PROFILE_ID_KEY, 0) ?: 0
if (id == 0L)
toast("Invalid shortcut ID")
else
runCatching {
val dao = MainDb.getInstance(this).serverProfileDao
val profile = runBlocking { dao.getByID(id) } // Blocking IO on Main thread
if (profile == null)
toast("This server has been deleted")
else
startVncActivity(this, profile)
}.onFailure {
Log.e(javaClass.simpleName, "Unable to launch shortcut [ID: $id]", it)
toast("Unable to launch shortcut")
}
finish()
}
private fun toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

View file

@ -0,0 +1,22 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#f5f5f5" />
</shape>
</item>
<item
android:bottom="16dp"
android:left="16dp"
android:right="16dp"
android:top="16dp">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#2128ab"
android:pathData="M20,18c1.1,0 1.99,-0.9 1.99,-2L22,6c0,-1.1 -0.9,-2 -2,-2H4c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2H0v2h24v-2h-4zM4,6h16v10H4V6z" />
</vector>
</item>
</layer-list>

View file

@ -80,6 +80,7 @@
<string name="msg_key_compat_mode_help">Some servers have limited Unicode support. Legacy key events can help input non-English text.</string>
<string name="msg_button_up_delay_help">Delaying click events can help in some rare cases if an app is not responding to clicks.</string>
<string name="msg_drag_gesture_help">Assigning an action to this gesture will change Long press detection:\n\n<b>Press-hold-release</b> → Long press\n<b>Press-hold-swipe</b> → Long press and swipe</string>
<string name="msg_shortcut_server_deleted">This server has been deleted</string>
<string name="tip_empty_server_list">Server list is empty.\nClick \'<b>+</b>\' to add a server, or\nuse the top address bar to connect directly.</string>
<string name="tip_urlbar">You can enter IP address, hostname, or \'vnc://\' URI.</string>
<string name="tip_discovery">Discovery can automatically find supported servers\nrunning on your network.</string>