Skip to content

Commit

Permalink
Enable app shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
gujjwal00 committed Apr 22, 2023
1 parent 7ec479d commit 8d1eeba
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.content.Intent
import android.net.Uri
import android.view.View
import android.view.WindowManager.LayoutParams.TYPE_TOAST
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.*
import androidx.test.espresso.Root
Expand All @@ -25,6 +26,7 @@ import kotlinx.coroutines.runBlocking
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test

Expand Down Expand Up @@ -66,8 +68,10 @@ class IntentReceiverTest {
@Test
fun simpleVncUri() {
val server = TestServer().apply { start() }
ActivityScenario.launch<Activity>(newUriIntent("vnc://localhost:${server.port}"))
onView(withId(R.id.frame_view)).checkWillBeDisplayed()
ActivityScenario.launch<Activity>(newUriIntent("vnc://localhost:${server.port}")).use {
onView(withId(R.id.frame_view)).checkWillBeDisplayed()
assertEquals(Lifecycle.State.DESTROYED, it.state)
}
}

@Test
Expand All @@ -83,8 +87,10 @@ class IntentReceiverTest {
runBlocking {
dbRule.db.serverProfileDao.insert(ServerProfile(name = "Example", host = "localhost", port = server.port))
}
ActivityScenario.launch<Activity>(newUriIntent("vnc://?ConnectionName=Example"))
onView(withId(R.id.frame_view)).checkWillBeDisplayed()
ActivityScenario.launch<Activity>(newUriIntent("vnc://?ConnectionName=Example")).use {
onView(withId(R.id.frame_view)).checkWillBeDisplayed()
assertEquals(Lifecycle.State.DESTROYED, it.state)
}
}

@Test
Expand All @@ -93,4 +99,28 @@ class IntentReceiverTest {
ActivityScenario.launch<Activity>(newUriIntent("vnc://?ConnectionName=NoSuchServer"))
onToast(withSubstring("No server found with name")).checkWillBeDisplayed()
}


/******************************** Shortcuts **************************************/

private fun newShortcutIntent(profile: ServerProfile) = IntentReceiverActivity.createShortcutIntent(targetContext, profile.ID)

@Test
fun simpleShortcut() {
val server = TestServer().apply { start() }
val profile = ServerProfile(host = "localhost", port = server.port)
runBlocking { profile.ID = dbRule.db.serverProfileDao.insert(profile) }

ActivityScenario.launch<Activity>(newShortcutIntent(profile)).use {
onView(withId(R.id.frame_view)).checkWillBeDisplayed()
assertEquals(Lifecycle.State.DESTROYED, it.state)
}
}

@Test
@SdkSuppress(maxSdkVersion = 29)
fun invalidShortcut() {
ActivityScenario.launch<Activity>(newShortcutIntent(ServerProfile(ID = 123456)))
onToast(withText(R.string.msg_shortcut_server_deleted)).checkWillBeDisplayed()
}
}
2 changes: 0 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
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"
Expand Down
72 changes: 39 additions & 33 deletions app/src/main/java/com/gaurav/avnc/ui/home/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ 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
Expand All @@ -27,6 +26,7 @@ import com.gaurav.avnc.databinding.ActivityHomeBinding
import com.gaurav.avnc.model.ServerProfile
import com.gaurav.avnc.ui.about.AboutActivity
import com.gaurav.avnc.ui.prefs.PrefsActivity
import com.gaurav.avnc.ui.vnc.IntentReceiverActivity
import com.gaurav.avnc.ui.vnc.startVncActivity
import com.gaurav.avnc.util.Debugging
import com.gaurav.avnc.util.MsgDialog
Expand Down Expand Up @@ -67,7 +67,7 @@ class HomeActivity : AppCompatActivity() {
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) }
viewModel.serverProfiles.observe(this) { updateShortcuts(it) }

showWelcomeMsg()
}
Expand Down Expand Up @@ -148,23 +148,11 @@ class HomeActivity : AppCompatActivity() {
* Shows delete confirmation snackbar, allowing the user to Undo deletion.
*/
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)
}
Expand Down Expand Up @@ -194,35 +182,53 @@ class HomeActivity : AppCompatActivity() {
}.isSuccess
}

/************************************************************************************
* Shortcuts
************************************************************************************/

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)

updateShortcutState(profiles)
updateDynamicShortcuts(profiles)
}.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)
/**
* Enable/Disable shortcuts based on availability in [profiles]
*/
private fun updateShortcutState(profiles: List<ServerProfile>) {
val pinnedShortcuts = ShortcutManagerCompat.getShortcuts(this, ShortcutManagerCompat.FLAG_MATCH_PINNED)
val disabledMessage = getString(R.string.msg_shortcut_server_deleted)

val possibleIds = profiles.map { createShortcutId(it) }
val pinnedIds = pinnedShortcuts.map { it.id }
val enabledIds = pinnedIds.intersect(possibleIds).toList()
val enabledShortcuts = pinnedShortcuts.filter { it.id in enabledIds }
val disabledIds = pinnedIds.subtract(enabledIds).toList()

ShortcutManagerCompat.enableShortcuts(this, enabledShortcuts)
ShortcutManagerCompat.disableShortcuts(this, disabledIds, disabledMessage)
}

/**
* Updates dynamic shortcut list
*/
private fun updateDynamicShortcuts(profiles: List<ServerProfile>) {
val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(this)
val shortcuts = profiles.take(maxShortcuts).map { p ->
ShortcutInfoCompat.Builder(this, createShortcutId(p))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_computer_shortcut))
.setShortLabel(p.name.ifBlank { p.host })
.setLongLabel(p.name.ifBlank { p.host })
.setIntent(IntentReceiverActivity.createShortcutIntent(this, p.ID))
.build()
}
ShortcutManagerCompat.setDynamicShortcuts(this, shortcuts)
}
}
58 changes: 0 additions & 58 deletions app/src/main/java/com/gaurav/avnc/ui/home/ShortcutActivity.kt

This file was deleted.

73 changes: 55 additions & 18 deletions app/src/main/java/com/gaurav/avnc/ui/vnc/IntentReceiverActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
*/
package com.gaurav.avnc.ui.vnc

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 androidx.lifecycle.lifecycleScope
Expand All @@ -20,52 +21,88 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* Handles intents with 'vnc' URI.
* Handles "external" intents and launches [VncActivity] with appropriate profiles.
*
* Current intent types:
* - vnc:// URIs
* - App shortcuts
*/
class IntentReceiverActivity : AppCompatActivity() {

companion object {
private const val SHORTCUT_PROFILE_ID_KEY = "com.gaurav.avnc.shortcut_profile_id"

fun createShortcutIntent(context: Context, profileId: Long): Intent {
check(profileId != 0L) { "Cannot create shortcut with profileId = 0." }
return Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra(SHORTCUT_PROFILE_ID_KEY, profileId)
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val uri = getUri()

if (uri.connectionName.isNotBlank())
launchSavedProfile(uri.connectionName)
if (intent.data?.scheme == "vnc")
launchFromVncUri(VncUri(intent.data!!))
else if (intent.hasExtra(SHORTCUT_PROFILE_ID_KEY))
launchFromProfileId(intent.getLongExtra(SHORTCUT_PROFILE_ID_KEY, 0))
else
launchUri(uri)
handleUnknownIntent()
}

private fun getUri(): VncUri {
if (intent.data?.scheme != "vnc") {
Log.e(javaClass.simpleName, "Invalid intent!")
return VncUri("")
}

return VncUri(intent.data!!)
private fun launchFromVncUri(uri: VncUri) {
if (uri.connectionName.isNotBlank())
launchFromProfileName(uri.connectionName)
else
launchVncUri(uri)
}

private fun launchUri(uri: VncUri) {
private fun launchVncUri(uri: VncUri) {
if (uri.host.isEmpty())
Toast.makeText(this, R.string.msg_invalid_vnc_uri, Toast.LENGTH_LONG).show()
toast(getString(R.string.msg_invalid_vnc_uri))
else
startVncActivity(this, uri)

finish()
}

private fun launchSavedProfile(name: String) {
private fun launchFromProfileName(name: String) {
val context = this
lifecycleScope.launch(Dispatchers.IO) {
val dao = MainDb.getInstance(context).serverProfileDao
val profile = dao.getByName(name).firstOrNull()
withContext(Dispatchers.Main) {
if (profile == null)
Toast.makeText(context, "No server found with name '$name'", Toast.LENGTH_LONG).show()
toast("No server found with name '$name'")
else
startVncActivity(context, profile)

finish()
}
}
}

private fun launchFromProfileId(profileId: Long) {
val context = this
lifecycleScope.launch(Dispatchers.IO) {
val dao = MainDb.getInstance(context).serverProfileDao
val profile = dao.getByID(profileId)
withContext(Dispatchers.Main) {
if (profile == null)
toast(getString(R.string.msg_shortcut_server_deleted))
else
startVncActivity(context, profile)
finish()
}
}
}

private fun handleUnknownIntent() {
toast("Invalid intent: Server info is missing!")
finish()
}

private fun toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

0 comments on commit 8d1eeba

Please sign in to comment.