chore(refactor): use intents in LaunchRequest and ShortcutRequest

This commit is contained in:
Adam M. Szalkowski 2026-07-04 20:49:06 +02:00
parent 2ba06beadc
commit da20db567c
27 changed files with 201 additions and 169 deletions

View file

@ -29,16 +29,20 @@ class ActivityLauncherImplTest {
val extras = Bundle().apply {
putString("test_key", "test_value")
}
val request = LaunchRequest(componentName, extras)
val intent = Intent().apply {
component = componentName
putExtras(extras)
}
val request = LaunchRequest(intent)
activityLauncher.launchActivity(request)
argumentCaptor<Intent>().apply {
verify(context).startActivity(capture())
val intent = firstValue
assertEquals(componentName, intent.component)
assertEquals("test_value", intent.getStringExtra("test_key"))
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK)
val capturedIntent = firstValue
assertEquals(componentName, capturedIntent.component)
assertEquals("test_value", capturedIntent.getStringExtra("test_key"))
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, capturedIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
}

View file

@ -88,14 +88,14 @@ class ProxyImplTest {
fun testCreateLauncherIconDelegation() {
val componentName = ComponentName("com.test", "Activity")
val icon: IconCompat = mock()
whenever(intentSigner.signRequest(any())).thenReturn("signature")
whenever(intentSigner.signRequest(any<ShortcutRequest>())).thenReturn("signature")
// Mock the context and capture it to verify startActivity
val mockContext: Context = mock()
whenever(mockContext.packageManager).thenReturn(packageManager)
val proxyWithMockContext = ShortcutCreatorProxyImpl(mockContext, intentSigner)
val request = ShortcutRequest("Test", componentName, icon)
val request = ShortcutRequest("Test", Intent().setComponent(componentName), icon)
proxyWithMockContext.createLauncherIcon(request, null)
verify(intentSigner).signRequest(eq(request))

View file

@ -89,7 +89,7 @@ class ShortcutCreatorImplTest {
android.graphics.Bitmap.createBitmap(1, 1, android.graphics.Bitmap.Config.ARGB_8888),
)
val signature = "test_signature"
whenever(intentSigner.signRequest(any())).thenReturn(signature)
whenever(intentSigner.signRequest(any<ShortcutRequest>())).thenReturn(signature)
val shortcutManager = mock<ShortcutManager>()
val mockContext = object : android.content.ContextWrapper(context) {
@ -105,7 +105,7 @@ class ShortcutCreatorImplTest {
}
val shortcutCreatorWithMock = ShortcutCreatorImpl(mockContext, intentSigner)
val request = ShortcutRequest("Test App", componentName, icon)
val request = ShortcutRequest("Test App", Intent().setComponent(componentName), icon)
shortcutCreatorWithMock.createLauncherIcon(request)

View file

@ -31,8 +31,8 @@ class ViewIntentParserImplTest {
val request = parser.parseLaunchRequest(intent)
assertEquals("com.example", request?.component?.packageName)
assertEquals("com.example.MainActivity", request?.component?.className)
assertEquals("com.example", request?.intent?.component?.packageName)
assertEquals("com.example.MainActivity", request?.intent?.component?.className)
}
@Test
@ -45,8 +45,8 @@ class ViewIntentParserImplTest {
}
val request = parser.parseLaunchRequest(intent)
assertEquals("com.test", request?.component?.packageName)
assertEquals("com.test.Activity", request?.component?.className)
assertEquals("com.test", request?.intent?.component?.packageName)
assertEquals("com.test.Activity", request?.intent?.component?.className)
}
@Test
@ -72,7 +72,7 @@ class ViewIntentParserImplTest {
val request = parser.parseShortcutRequest(intent)
assertEquals("Test Name", request?.name)
assertEquals("com.test", request?.component?.packageName)
assertEquals("com.test.Activity", request?.component?.className)
assertEquals("com.test", request?.intent?.component?.packageName)
assertEquals("com.test.Activity", request?.intent?.component?.className)
}
}

View file

@ -95,7 +95,6 @@ class MainActivityIntentTest {
whenever(viewIntentParser.parseLaunchRequest(any())).thenAnswer { invocation: org.mockito.invocation.InvocationOnMock -> realParser.parseLaunchRequest(invocation.getArgument(0)) }
whenever(viewIntentParser.parseShortcutRequest(any())).thenAnswer { invocation: org.mockito.invocation.InvocationOnMock -> realParser.parseShortcutRequest(invocation.getArgument(0)) }
whenever(viewIntentParser.componentNameFromIntent(any())).thenAnswer { invocation: org.mockito.invocation.InvocationOnMock -> realParser.componentNameFromIntent(invocation.getArgument(0)) }
whenever(viewIntentParser.packageFromIntent(any())).thenAnswer { invocation: org.mockito.invocation.InvocationOnMock -> realParser.packageFromIntent(invocation.getArgument(0)) }
whenever(viewIntentParser.parseShortcutIntent(any())).thenAnswer { invocation: org.mockito.invocation.InvocationOnMock -> realParser.parseShortcutIntent(invocation.getArgument(0)) }
whenever(settingsRepository.disclaimerAccepted).thenReturn(true)
whenever(supportReminder.shouldDisplayReminder()).thenReturn(false)

View file

@ -119,9 +119,8 @@ class ShortcutFlowTest {
ShortcutRequest(
name = appName,
component = launchIntent.component!!,
intent = launchIntent,
icon = mock<androidx.core.graphics.drawable.IconCompat>(),
extras = launchIntent.extras,
launcherPlugin = launchPlugin,
)
}
@ -135,9 +134,12 @@ class ShortcutFlowTest {
null
} ?: return@thenAnswer null
val launchPluginStr = intent.getStringExtra(ShortcutCreator.INTENT_EXTRA_LAUNCH_PLUGIN)
val launchPlugin = launchPluginStr?.let { ComponentName.unflattenFromString(it) }
LaunchRequest(
component = launchIntent.component!!,
extras = launchIntent.extras,
intent = launchIntent,
launcherPlugin = launchPlugin,
)
}
}
@ -164,9 +166,9 @@ class ShortcutFlowTest {
ActivityScenario.launch<ShortcutActivity>(intent).use {
val captor = argumentCaptor<LaunchRequest>()
verify(activityLauncher).launchActivity(captor.capture())
assertEquals("com.test", captor.firstValue.component.packageName)
assertEquals("com.test.Activity", captor.firstValue.component.className)
assertEquals("value", captor.firstValue.extras?.getString("key"))
assertEquals("com.test", captor.firstValue.intent.component?.packageName)
assertEquals("com.test.Activity", captor.firstValue.intent.component?.className)
assertEquals("value", captor.firstValue.intent.extras?.getString("key"))
}
}
@ -215,7 +217,7 @@ class ShortcutFlowTest {
val captor = argumentCaptor<ShortcutRequest>()
verify(shortcutCreator).createLauncherIcon(captor.capture())
assertEquals("Test App", captor.firstValue.name)
assertEquals(componentName, captor.firstValue.component)
assertEquals(componentName, captor.firstValue.intent.component)
}
}
@ -232,7 +234,7 @@ class ShortcutFlowTest {
ActivityScenario.launch<ShortcutActivity>(intent).use {
val captor = argumentCaptor<LaunchRequest>()
verify(activityLauncher).launchActivity(captor.capture())
assertEquals(componentName, captor.firstValue.component)
assertEquals(componentName, captor.firstValue.intent.component)
}
}
@ -254,7 +256,9 @@ class ShortcutFlowTest {
ActivityScenario.launch<ShortcutActivity>(intent).use {
verify(activityLauncher, never()).launchActivity(any())
verify(activityLauncherProxy).launchActivity(any(), eq(ComponentName.unflattenFromString(launchPlugin)))
val captor = argumentCaptor<LaunchRequest>()
verify(activityLauncherProxy).launchActivity(captor.capture())
assertEquals(ComponentName.unflattenFromString(launchPlugin), captor.firstValue.launcherPlugin)
}
}
}

View file

@ -5,7 +5,6 @@ import android.content.Intent
import android.widget.Toast
import dagger.hilt.android.qualifiers.ApplicationContext
import de.szalkowski.activitylauncher.R
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.launcher.ActivityLauncher
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import javax.inject.Inject
@ -13,7 +12,7 @@ import javax.inject.Inject
class ActivityLauncherImpl @Inject constructor(@ApplicationContext private val context: Context) :
ActivityLauncher {
override fun launchActivity(request: LaunchRequest) {
val intent = getActivityIntent(request.component, request.extras)
val intent = request.intent
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {

View file

@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.core.util.toIconCompat
import de.szalkowski.activitylauncher.domain.launcher.ActivityLauncherProxy
import de.szalkowski.activitylauncher.domain.launcher.ShortcutCreator
@ -18,14 +17,13 @@ class ActivityLauncherProxyImpl @Inject constructor(
) : ActivityLauncherProxy {
private val pm: PackageManager = context.packageManager
override fun launchActivity(request: LaunchRequest, plugin: ComponentName?) {
override fun launchActivity(request: LaunchRequest) {
val intent = Intent(ActivityLauncherProxy.INTENT_LAUNCH_ACTIVITY)
if (plugin != null) {
intent.component = plugin
if (request.launcherPlugin != null) {
intent.component = request.launcherPlugin
}
val launchIntent = getActivityIntent(request.component, request.extras)
intent.putExtra(ShortcutCreator.INTENT_EXTRA_INTENT, launchIntent.toUri(Intent.URI_INTENT_SCHEME))
intent.putExtra(ShortcutCreator.INTENT_EXTRA_INTENT, request.intent.toUri(Intent.URI_INTENT_SCHEME))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)

View file

@ -1,11 +1,13 @@
package de.szalkowski.activitylauncher.data.launcher
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.util.Base64
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.launcher.IntentSigner
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.model.ShortcutRequest
import java.security.SecureRandom
import javax.crypto.Mac
@ -29,10 +31,12 @@ class IntentSignerImpl @Inject constructor(@ApplicationContext context: Context)
}
}
override fun signRequest(request: ShortcutRequest): String {
val launchIntent = getActivityIntent(request.component, request.extras)
val uri = launchIntent.toUri(0)
val launcherPlugin = request.launcherPlugin?.flattenToString()
override fun signRequest(request: ShortcutRequest): String = signIntent(request.intent, request.launcherPlugin)
override fun signRequest(request: LaunchRequest): String = signIntent(request.intent, request.launcherPlugin)
private fun signIntent(intent: Intent, launcherPlugin: ComponentName?): String {
val uri = intent.toUri(0)
val launcherPlugin = launcherPlugin?.flattenToString()
val message = if (launcherPlugin == null) {
uri
} else {
@ -41,7 +45,7 @@ class IntentSignerImpl @Inject constructor(@ApplicationContext context: Context)
return hmac256(key, message)
}
override fun validateRequestSignature(request: ShortcutRequest, signature: String): Boolean {
override fun validateRequestSignature(request: LaunchRequest, signature: String): Boolean {
val compSignature = signRequest(request)
return signature == compSignature
}

View file

@ -6,7 +6,6 @@ import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.launcher.IntentSigner
import de.szalkowski.activitylauncher.domain.launcher.ShortcutCreator
import de.szalkowski.activitylauncher.domain.model.ShortcutRequest
@ -28,10 +27,9 @@ class ShortcutCreatorImpl @Inject constructor(
ShortcutActivity::class.java,
)
val launchIntent = getActivityIntent(request.component, request.extras)
intent.putExtra(
ShortcutCreator.INTENT_EXTRA_INTENT,
launchIntent.toUri(Intent.URI_INTENT_SCHEME),
request.intent.toUri(Intent.URI_INTENT_SCHEME),
)
val signature = intentSigner.signRequest(request)

View file

@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.core.util.toIconCompat
import de.szalkowski.activitylauncher.domain.launcher.IntentSigner
import de.szalkowski.activitylauncher.domain.launcher.ShortcutCreator
@ -28,8 +27,7 @@ class ShortcutCreatorProxyImpl @Inject constructor(
}
intent.putExtra(ShortcutCreator.INTENT_EXTRA_NAME, request.name)
val launchIntent = getActivityIntent(request.component, request.extras)
intent.putExtra(ShortcutCreator.INTENT_EXTRA_INTENT, launchIntent.toUri(Intent.URI_INTENT_SCHEME))
intent.putExtra(ShortcutCreator.INTENT_EXTRA_INTENT, request.intent.toUri(Intent.URI_INTENT_SCHEME))
intent.putExtra(ShortcutCreator.INTENT_EXTRA_ICON, request.icon.toBundle())
val signature = intentSigner.signRequest(request)

View file

@ -13,32 +13,24 @@ import javax.inject.Inject
class ViewIntentParserImpl @Inject constructor(
private val getActivityIconUseCase: GetActivityIconUseCase,
) : ViewIntentParser {
override fun packageFromIntent(intent: Intent): String? {
if (intent.action != Intent.ACTION_VIEW) {
return null
}
return runCatching {
val url = intent.dataString.orEmpty()
if (url.startsWith("https://activitylauncher.net/activity/")) {
val rawComponent = url.removePrefix("https://activitylauncher.net/activity/")
ComponentName.unflattenFromString(rawComponent)?.packageName
} else {
null
}
}.getOrNull()
}
override fun parseLaunchRequest(intent: Intent): LaunchRequest? {
val launchIntentStr = intent.getStringExtra(ShortcutCreator.INTENT_EXTRA_INTENT)
?: intent.getStringExtra(ShortcutCreator.LEGACY_INTENT_EXTRA_INTENT)
val launchIntent = launchIntentStr?.let { parseShortcutIntent(it) }
val component = launchIntent?.component
?: componentNameFromIntent(intent)
?: return null
val launcherPluginStr = intent.getStringExtra(ShortcutCreator.INTENT_EXTRA_LAUNCH_PLUGIN)
val launcherPlugin = launcherPluginStr?.let { ComponentName.unflattenFromString(it) }
return LaunchRequest(component, launchIntent?.extras)
if (launchIntent != null) {
return LaunchRequest(launchIntent, launcherPlugin)
}
val component = componentNameFromIntent(intent) ?: return null
val newLaunchIntent = Intent().apply {
this.component = component
}
return LaunchRequest(newLaunchIntent, launcherPlugin)
}
override fun parseShortcutRequest(intent: Intent): ShortcutRequest? {
@ -55,7 +47,7 @@ class ViewIntentParserImpl @Inject constructor(
val launcherPluginStr = intent.getStringExtra(ShortcutCreator.INTENT_EXTRA_LAUNCH_PLUGIN)
val launcherPlugin = launcherPluginStr?.let { ComponentName.unflattenFromString(it) }
return ShortcutRequest(appName, component, icon, launchIntent.extras, launcherPlugin)
return ShortcutRequest(appName, launchIntent, icon, launcherPlugin)
}
override fun componentNameFromIntent(intent: Intent): ComponentName? {

View file

@ -1,11 +1,10 @@
package de.szalkowski.activitylauncher.domain.launcher
import android.content.ComponentName
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.model.PluginInfo
interface ActivityLauncherProxy {
fun launchActivity(request: LaunchRequest, plugin: ComponentName? = null)
fun launchActivity(request: LaunchRequest)
fun hasMultipleHandlers(): Boolean
fun getPlugins(): List<PluginInfo>

View file

@ -1,8 +1,10 @@
package de.szalkowski.activitylauncher.domain.launcher
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.model.ShortcutRequest
interface IntentSigner {
fun signRequest(request: ShortcutRequest): String
fun validateRequestSignature(request: ShortcutRequest, signature: String): Boolean
fun signRequest(request: LaunchRequest): String
fun validateRequestSignature(request: LaunchRequest, signature: String): Boolean
}

View file

@ -6,7 +6,6 @@ import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.model.ShortcutRequest
interface ViewIntentParser {
fun packageFromIntent(intent: Intent): String?
fun componentNameFromIntent(intent: Intent): ComponentName?
fun parseShortcutIntent(uri: String): Intent?
fun parseLaunchRequest(intent: Intent): LaunchRequest?

View file

@ -1,9 +1,9 @@
package de.szalkowski.activitylauncher.domain.model
import android.content.ComponentName
import android.os.Bundle
import android.content.Intent
data class LaunchRequest(
val component: ComponentName,
val extras: Bundle? = null,
val intent: Intent,
val launcherPlugin: ComponentName? = null,
)

View file

@ -1,13 +1,12 @@
package de.szalkowski.activitylauncher.domain.model
import android.content.ComponentName
import android.os.Bundle
import android.content.Intent
import androidx.core.graphics.drawable.IconCompat
data class ShortcutRequest(
val name: String,
val component: ComponentName,
val intent: Intent,
val icon: IconCompat,
val extras: Bundle? = null,
val launcherPlugin: ComponentName? = null,
)

View file

@ -13,7 +13,7 @@ class CreateShortcutUseCase @Inject constructor(
private val shortcutCreatorProxy: ShortcutCreatorProxy,
) {
operator fun invoke(request: ShortcutRequest, shortcutPlugin: ComponentName? = null) {
Log.i("CreateShortcutUseCase", "Creating shortcut: ${request.component.flattenToShortString()}")
Log.i("CreateShortcutUseCase", "Creating shortcut: ${request.intent.component?.flattenToShortString()}")
if (shortcutPlugin != null || shortcutCreatorProxy.hasMultipleHandlers()) {
shortcutCreatorProxy.createLauncherIcon(request, shortcutPlugin)
} else {

View file

@ -1,6 +1,5 @@
package de.szalkowski.activitylauncher.domain.usecase.launcher
import android.content.ComponentName
import android.util.Log
import de.szalkowski.activitylauncher.domain.launcher.ActivityLauncher
import de.szalkowski.activitylauncher.domain.launcher.ActivityLauncherProxy
@ -14,14 +13,15 @@ class LaunchActivityUseCase @Inject constructor(
private val activityLauncherProxy: ActivityLauncherProxy,
private val recentsRepository: RecentsRepository,
) {
operator fun invoke(request: LaunchRequest, launchPlugin: ComponentName? = null) {
Log.i("LaunchActivityUseCase", "Launching activity: ${request.component.flattenToShortString()}")
if (launchPlugin != null || activityLauncherProxy.hasMultipleHandlers()) {
activityLauncherProxy.launchActivity(request, launchPlugin)
operator fun invoke(request: LaunchRequest) {
val component = request.intent.component ?: return
Log.i("LaunchActivityUseCase", "Launching activity: ${component.flattenToShortString()}")
if (request.launcherPlugin != null || activityLauncherProxy.hasMultipleHandlers()) {
activityLauncherProxy.launchActivity(request)
} else {
activityLauncher.launchActivity(request)
}
recentsRepository.addActivity(request.component)
recentsRepository.addActivity(component)
}
fun hasMultipleHandlers(): Boolean = activityLauncherProxy.hasMultipleHandlers()

View file

@ -200,7 +200,7 @@ class MainActivity : AppCompatActivity(), ActionBarSearch {
val launchRequest = viewIntentParser.parseLaunchRequest(intent)
val componentNameFromExtra =
intent.getParcelableExtra<ComponentName>(EXTRA_ACTIVITY_COMPONENT_NAME)
val componentName = launchRequest?.component ?: componentNameFromExtra
val componentName = launchRequest?.intent?.component ?: componentNameFromExtra
if (componentName != null) {
val bundle = Bundle().apply {

View file

@ -13,7 +13,6 @@ import de.szalkowski.activitylauncher.domain.launcher.IntentSigner
import de.szalkowski.activitylauncher.domain.launcher.ShortcutCreator
import de.szalkowski.activitylauncher.domain.launcher.ShortcutCreatorProxy
import de.szalkowski.activitylauncher.domain.launcher.ViewIntentParser
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.usecase.launcher.LaunchActivityUseCase
import javax.inject.Inject
@ -98,16 +97,16 @@ class ShortcutActivity : AppCompatActivity() {
}
private fun handleLaunchShortcut() {
val request = viewIntentParser.parseShortcutRequest(intent) ?: return
val request = viewIntentParser.parseLaunchRequest(intent) ?: return
val signature = intent.getStringExtra(ShortcutCreator.INTENT_EXTRA_SIGNATURE).orEmpty()
if (!intentSigner.validateRequestSignature(request, signature)) {
Log.e("ShortcutActivity", "Invalid signature for shortcut")
redirectToMain(request.component)
request.intent.component?.let { redirectToMain(it) }
return
}
launchActivityUseCase.invoke(LaunchRequest(request.component, request.extras), request.launcherPlugin)
launchActivityUseCase.invoke(request)
}
private fun handleLaunchActivity() {

View file

@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.szalkowski.activitylauncher.R
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.favorites.FavoritesRepository
import de.szalkowski.activitylauncher.domain.launcher.IconLoader
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
@ -170,9 +171,8 @@ class ActivityDetailsViewModel @Inject constructor(
val icon = _editedIcon.value ?: getActivityIconUseCase(info.iconResourceName, info.componentName)
val request = ShortcutRequest(
name = info.name,
component = info.componentName,
intent = getActivityIntent(info.componentName, Bundle()),
icon = icon,
extras = Bundle(),
launcherPlugin = _selectedLaunchPlugin.value?.componentName,
)
createShortcutUseCase(request, _selectedShortcutPlugin.value?.componentName)
@ -189,10 +189,10 @@ class ActivityDetailsViewModel @Inject constructor(
fun launchActivity() {
val info = getEditedActivityInfo()
val request = LaunchRequest(
component = info.componentName,
extras = Bundle(),
intent = getActivityIntent(info.componentName, Bundle()),
launcherPlugin = _selectedLaunchPlugin.value?.componentName,
)
launchActivityUseCase(request, _selectedLaunchPlugin.value?.componentName)
launchActivityUseCase(request)
}
fun shareActivity() {

View file

@ -12,6 +12,7 @@ import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.launcher.IconLoader
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.packages.PackageRepository
@ -49,7 +50,7 @@ abstract class BaseActivityListFragment : Fragment() {
icon.loadDrawable(requireContext()) ?: requireContext().packageManager.defaultActivityIcon
}
adapter.onItemClick = { info ->
launchActivityUseCase(LaunchRequest(info.componentName))
launchActivityUseCase(LaunchRequest(getActivityIntent(info.componentName, null)))
}
adapter.onItemLongClick = { info ->
runCatching {

View file

@ -4,7 +4,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
import de.szalkowski.activitylauncher.domain.model.ShortcutRequest
import org.junit.Assert.*
import org.junit.Before
@ -47,69 +47,51 @@ class IntentSignerImplTest {
@Test
fun testSignAndValidateRequest() = withMockedBase64 {
val componentName = mock<ComponentName>()
val icon = mock<androidx.core.graphics.drawable.IconCompat>()
val request = ShortcutRequest("Test", componentName, icon)
val intent = mock<Intent>()
val shortcutRequest = ShortcutRequest("Test", intent, icon)
val launchRequest = LaunchRequest(intent)
val intentUtilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
mockStatic(intentUtilClass).use { mockedIntentUtil ->
mockedIntentUtil.`when`<Intent> {
getActivityIntent(eq(componentName), anyOrNull())
}.thenReturn(intent)
whenever(intent.toUri(anyInt())).thenReturn("intent:#Intent;action=com.test.ACTION;end")
whenever(intent.toUri(anyInt())).thenReturn("intent:#Intent;action=com.test.ACTION;end")
val signature = signer.signRequest(request)
val signature = signer.signRequest(shortcutRequest)
assertNotNull(signature)
assertTrue(signer.validateRequestSignature(request, signature))
}
assertNotNull(signature)
assertTrue(signer.validateRequestSignature(launchRequest, signature))
}
@Test
fun testSignatureWithPlugin() = withMockedBase64 {
val componentName = mock<ComponentName>()
val icon = mock<androidx.core.graphics.drawable.IconCompat>()
val plugin = mock<ComponentName>()
whenever(plugin.flattenToString()).thenReturn("com.example/.Plugin")
val requestWithPlugin = ShortcutRequest("Test", componentName, icon, launcherPlugin = plugin)
val requestWithoutPlugin = ShortcutRequest("Test", componentName, icon)
val intent = mock<Intent>()
whenever(intent.toUri(anyInt())).thenReturn("intent:#Intent;action=com.test.ACTION;end")
val intentUtilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
mockStatic(intentUtilClass).use { mockedIntentUtil ->
mockedIntentUtil.`when`<Intent> {
getActivityIntent(eq(componentName), anyOrNull())
}.thenReturn(intent)
whenever(intent.toUri(anyInt())).thenReturn("intent:#Intent;action=com.test.ACTION;end")
val shortcutRequestWithPlugin = ShortcutRequest("Test", intent, icon, launcherPlugin = plugin)
val shortcutRequestWithoutPlugin = ShortcutRequest("Test", intent, icon)
val launchRequestWithPlugin = LaunchRequest(intent, launcherPlugin = plugin)
val launchRequestWithoutPlugin = LaunchRequest(intent)
val signatureWithPlugin = signer.signRequest(requestWithPlugin)
val signatureWithoutPlugin = signer.signRequest(requestWithoutPlugin)
val signatureWithPlugin = signer.signRequest(shortcutRequestWithPlugin)
val signatureWithoutPlugin = signer.signRequest(shortcutRequestWithoutPlugin)
assertNotEquals(signatureWithPlugin, signatureWithoutPlugin)
assertTrue(signer.validateRequestSignature(requestWithPlugin, signatureWithPlugin))
assertFalse(signer.validateRequestSignature(requestWithPlugin, signatureWithoutPlugin))
assertFalse(signer.validateRequestSignature(requestWithoutPlugin, signatureWithPlugin))
}
assertNotEquals(signatureWithPlugin, signatureWithoutPlugin)
assertTrue(signer.validateRequestSignature(launchRequestWithPlugin, signatureWithPlugin))
assertFalse(signer.validateRequestSignature(launchRequestWithPlugin, signatureWithoutPlugin))
assertFalse(signer.validateRequestSignature(launchRequestWithoutPlugin, signatureWithPlugin))
}
@Test
fun testKnownSignature() = withMockedBase64 {
val componentName = mock<ComponentName>()
val icon = mock<androidx.core.graphics.drawable.IconCompat>()
val request = ShortcutRequest("Test", componentName, icon)
val intent = mock<Intent>()
val request = ShortcutRequest("Test", intent, icon)
val intentUtilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
mockStatic(intentUtilClass).use { mockedIntentUtil ->
mockedIntentUtil.`when`<Intent> {
getActivityIntent(eq(componentName), anyOrNull())
}.thenReturn(intent)
whenever(intent.toUri(anyInt())).thenReturn("intent:#Intent;action=com.test.ACTION;end")
whenever(intent.toUri(anyInt())).thenReturn("intent:#Intent;action=com.test.ACTION;end")
val signature = signer.signRequest(request)
// Verified known signature for "intent:#Intent;action=com.test.ACTION;end" with key "test_key"
assertEquals("kPThuLUm6BnZkfNuIuRuVZfj8IXOinD+dURnRv1Ytd8=", signature)
}
val signature = signer.signRequest(request)
// Verified known signature for "intent:#Intent;action=com.test.ACTION;end" with key "test_key"
assertEquals("kPThuLUm6BnZkfNuIuRuVZfj8IXOinD+dURnRv1Ytd8=", signature)
}
}

View file

@ -11,9 +11,16 @@ import org.mockito.kotlin.*
class CreateShortcutUseCaseTest {
private val shortcutCreator: ShortcutCreator = mock()
private val shortcutCreatorProxy: ShortcutCreatorProxy = mock()
private val componentName = ComponentName("com.test", "Activity")
private val componentName = mock<ComponentName> {
on { packageName } doReturn "com.test"
on { className } doReturn "Activity"
on { flattenToShortString() } doReturn "com.test/Activity"
}
private val icon = mock<androidx.core.graphics.drawable.IconCompat>()
private val request = ShortcutRequest("Test", componentName, icon)
private val intent = mock<android.content.Intent> {
on { component } doReturn componentName
}
private val request = ShortcutRequest("Test", intent, icon)
private lateinit var useCase: CreateShortcutUseCase
@Before

View file

@ -1,6 +1,7 @@
package de.szalkowski.activitylauncher.domain.usecase.launcher
import android.content.ComponentName
import android.content.Intent
import de.szalkowski.activitylauncher.domain.launcher.ActivityLauncher
import de.szalkowski.activitylauncher.domain.launcher.ActivityLauncherProxy
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
@ -16,7 +17,11 @@ class LaunchActivityUseCaseTest {
private val activityLauncherProxy: ActivityLauncherProxy = mock()
private val recentsRepository: RecentsRepository = mock()
private lateinit var useCase: LaunchActivityUseCase
private val componentName = ComponentName("com.test", "Activity")
private val componentName = mock<ComponentName> {
on { packageName } doReturn "com.test"
on { className } doReturn "Activity"
on { flattenToShortString() } doReturn "com.test/Activity"
}
@Before
fun setup() {
@ -25,7 +30,10 @@ class LaunchActivityUseCaseTest {
@Test
fun `should launch activity and add to recents`() {
val request = LaunchRequest(componentName)
val intent = mock<Intent> {
on { component } doReturn componentName
}
val request = LaunchRequest(intent)
useCase.invoke(request)
verify(activityLauncher).launchActivity(eq(request))
@ -35,10 +43,13 @@ class LaunchActivityUseCaseTest {
@Test
fun `should launch activity with plugin and add to recents`() {
val plugin = ComponentName("com.plugin", "Plugin")
val request = LaunchRequest(componentName)
useCase.invoke(request, launchPlugin = plugin)
val intent = mock<Intent> {
on { component } doReturn componentName
}
val request = LaunchRequest(intent, launcherPlugin = plugin)
useCase.invoke(request)
verify(activityLauncherProxy).launchActivity(eq(request), eq(plugin))
verify(activityLauncherProxy).launchActivity(eq(request))
verify(recentsRepository).addActivity(componentName)
}

View file

@ -4,6 +4,7 @@ import android.content.ComponentName
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.SavedStateHandle
import de.szalkowski.activitylauncher.R
import de.szalkowski.activitylauncher.core.util.getActivityIntent
import de.szalkowski.activitylauncher.domain.favorites.FavoritesRepository
import de.szalkowski.activitylauncher.domain.launcher.IconLoader
import de.szalkowski.activitylauncher.domain.model.LaunchRequest
@ -104,20 +105,34 @@ class ActivityDetailsViewModelTest {
@Test
fun `should launch activity`() {
viewModel.launchActivity()
val captor = argumentCaptor<LaunchRequest>()
verify(launchActivityUseCase).invoke(captor.capture(), isNull())
assertEquals("com.test", captor.firstValue.component.packageName)
assertEquals("Activity", captor.firstValue.component.className)
val mockIntent = mock<android.content.Intent>()
val utilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
org.mockito.Mockito.mockStatic(utilClass).use { mockedUtil ->
mockedUtil.`when`<android.content.Intent> {
getActivityIntent(eq(componentName), any())
}.thenReturn(mockIntent)
viewModel.launchActivity()
val captor = argumentCaptor<LaunchRequest>()
verify(launchActivityUseCase).invoke(captor.capture())
assertEquals(mockIntent, captor.firstValue.intent)
}
}
@Test
fun `should create shortcut`() {
viewModel.createShortcut()
val captor = argumentCaptor<ShortcutRequest>()
verify(createShortcutUseCase).invoke(captor.capture(), isNull())
assertEquals("com.test", captor.firstValue.component.packageName)
assertEquals("Activity", captor.firstValue.component.className)
val mockIntent = mock<android.content.Intent>()
val utilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
org.mockito.Mockito.mockStatic(utilClass).use { mockedUtil ->
mockedUtil.`when`<android.content.Intent> {
getActivityIntent(eq(componentName), any())
}.thenReturn(mockIntent)
viewModel.createShortcut()
val captor = argumentCaptor<ShortcutRequest>()
verify(createShortcutUseCase).invoke(captor.capture(), isNull())
assertEquals(mockIntent, captor.firstValue.intent)
}
}
@Test
@ -186,12 +201,20 @@ class ActivityDetailsViewModelTest {
newViewModel.selectLaunchPlugin(pluginComp)
newViewModel.launchActivity()
val mockIntent = mock<android.content.Intent>()
val utilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
org.mockito.Mockito.mockStatic(utilClass).use { mockedUtil ->
mockedUtil.`when`<android.content.Intent> {
getActivityIntent(eq(componentName), any())
}.thenReturn(mockIntent)
val captor = argumentCaptor<LaunchRequest>()
verify(launchActivityUseCase).invoke(captor.capture(), eq(pluginComp))
assertEquals("com.test", captor.firstValue.component.packageName)
assertEquals("Activity", captor.firstValue.component.className)
newViewModel.launchActivity()
val captor = argumentCaptor<LaunchRequest>()
verify(launchActivityUseCase).invoke(captor.capture())
assertEquals(mockIntent, captor.firstValue.intent)
assertEquals(pluginComp, captor.firstValue.launcherPlugin)
}
}
@Test
@ -208,12 +231,19 @@ class ActivityDetailsViewModelTest {
newViewModel.selectShortcutPlugin(pluginComp)
newViewModel.createShortcut()
val mockIntent = mock<android.content.Intent>()
val utilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
org.mockito.Mockito.mockStatic(utilClass).use { mockedUtil ->
mockedUtil.`when`<android.content.Intent> {
getActivityIntent(eq(componentName), any())
}.thenReturn(mockIntent)
val captor = argumentCaptor<ShortcutRequest>()
verify(createShortcutUseCase).invoke(captor.capture(), eq(pluginComp))
assertEquals("com.test", captor.firstValue.component.packageName)
assertEquals("Activity", captor.firstValue.component.className)
newViewModel.createShortcut()
val captor = argumentCaptor<ShortcutRequest>()
verify(createShortcutUseCase).invoke(captor.capture(), eq(pluginComp))
assertEquals(mockIntent, captor.firstValue.intent)
}
}
@Test
@ -230,13 +260,20 @@ class ActivityDetailsViewModelTest {
newViewModel.selectLaunchPlugin(pluginComp)
newViewModel.createShortcut()
val mockIntent = mock<android.content.Intent>()
val utilClass = Class.forName("de.szalkowski.activitylauncher.core.util.ActivityIntentKt")
org.mockito.Mockito.mockStatic(utilClass).use { mockedUtil ->
mockedUtil.`when`<android.content.Intent> {
getActivityIntent(eq(componentName), any())
}.thenReturn(mockIntent)
val captor = argumentCaptor<ShortcutRequest>()
verify(createShortcutUseCase).invoke(captor.capture(), isNull())
assertEquals("com.test", captor.firstValue.component.packageName)
assertEquals("Activity", captor.firstValue.component.className)
assertEquals(pluginComp, captor.firstValue.launcherPlugin)
newViewModel.createShortcut()
val captor = argumentCaptor<ShortcutRequest>()
verify(createShortcutUseCase).invoke(captor.capture(), isNull())
assertEquals(mockIntent, captor.firstValue.intent)
assertEquals(pluginComp, captor.firstValue.launcherPlugin)
}
}
@Test