Skip to content

Commit 26b90ed

Browse files
Add automatic https:// prefix to links without scheme (#131)
* Initial plan * Add automatic https:// prefix to links without scheme Co-authored-by: yogeshpaliyal <[email protected]> * feat: lint fixes --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: yogeshpaliyal <[email protected]> Co-authored-by: Yogesh Choudhary Paliyal <[email protected]>
1 parent c18be32 commit 26b90ed

File tree

5 files changed

+110
-13
lines changed

5 files changed

+110
-13
lines changed

app/src/main/java/com/yogeshpaliyal/deepr/ui/components/EditDeeplinkDialog.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
1919
import androidx.compose.ui.unit.dp
2020
import com.yogeshpaliyal.deepr.Deepr
2121
import com.yogeshpaliyal.deepr.util.isValidDeeplink
22+
import com.yogeshpaliyal.deepr.util.normalizeLink
2223

2324
@Composable
2425
fun EditDeeplinkDialog(
@@ -70,8 +71,9 @@ fun EditDeeplinkDialog(
7071
confirmButton = {
7172
Button(
7273
onClick = {
73-
if (isValidDeeplink(link)) {
74-
onSave(link, name)
74+
val normalizedLink = normalizeLink(link)
75+
if (isValidDeeplink(normalizedLink)) {
76+
onSave(normalizedLink, name)
7577
} else {
7678
isError = true
7779
}

app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import com.yogeshpaliyal.deepr.ui.components.QrCodeDialog
6969
import com.yogeshpaliyal.deepr.ui.screens.Settings
7070
import com.yogeshpaliyal.deepr.util.QRScanner
7171
import com.yogeshpaliyal.deepr.util.isValidDeeplink
72+
import com.yogeshpaliyal.deepr.util.normalizeLink
7273
import com.yogeshpaliyal.deepr.util.openDeeplink
7374
import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel
7475
import compose.icons.TablerIcons
@@ -125,8 +126,9 @@ fun HomeScreen(
125126
if (result.contents == null) {
126127
Toast.makeText(context, "No Data found", Toast.LENGTH_SHORT).show()
127128
} else {
128-
if (isValidDeeplink(result.contents)) {
129-
selectedLink = createDeeprObject(link = result.contents)
129+
val normalizedLink = normalizeLink(result.contents)
130+
if (isValidDeeplink(normalizedLink)) {
131+
selectedLink = createDeeprObject(link = normalizedLink)
130132
} else {
131133
Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show()
132134
}
@@ -136,8 +138,9 @@ fun HomeScreen(
136138
// Handle shared text from other apps
137139
LaunchedEffect(sharedText) {
138140
if (!sharedText?.url.isNullOrBlank() && selectedLink == null) {
139-
if (isValidDeeplink(sharedText.url)) {
140-
selectedLink = createDeeprObject(link = sharedText.url, name = sharedText.title ?: "")
141+
val normalizedLink = normalizeLink(sharedText.url)
142+
if (isValidDeeplink(normalizedLink)) {
143+
selectedLink = createDeeprObject(link = normalizedLink, name = sharedText.title ?: "")
141144
} else {
142145
Toast
143146
.makeText(context, "Invalid deeplink from shared content", Toast.LENGTH_SHORT)

app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/HomeBottomContent.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import com.yogeshpaliyal.deepr.GetLinksAndTags
5151
import com.yogeshpaliyal.deepr.R
5252
import com.yogeshpaliyal.deepr.Tags
5353
import com.yogeshpaliyal.deepr.util.isValidDeeplink
54+
import com.yogeshpaliyal.deepr.util.normalizeLink
5455
import com.yogeshpaliyal.deepr.util.openDeeplink
5556
import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel
5657
import compose.icons.TablerIcons
@@ -110,6 +111,9 @@ fun HomeBottomContent(
110111
}
111112

112113
val save: (executeAfterSave: Boolean) -> Unit = { executeAfterSave ->
114+
// Normalize the link before saving
115+
val normalizedLink = normalizeLink(deeprInfo.link)
116+
113117
// Remove unselected tags
114118
val initialTagIds = initialSelectedTags.map { it.id }.toSet()
115119
val currentTagIds = selectedTags.map { it.id }.toSet()
@@ -120,12 +124,12 @@ fun HomeBottomContent(
120124

121125
if (deeprInfo.id == 0L) {
122126
// New Account
123-
viewModel.insertAccount(deeprInfo.link, deeprInfo.name, executeAfterSave, selectedTags)
127+
viewModel.insertAccount(normalizedLink, deeprInfo.name, executeAfterSave, selectedTags)
124128
} else {
125129
// Edit
126-
viewModel.updateDeeplink(deeprInfo.id, deeprInfo.link, deeprInfo.name, selectedTags)
130+
viewModel.updateDeeplink(deeprInfo.id, normalizedLink, deeprInfo.name, selectedTags)
127131
}
128-
onSaveDialogInfoChange(SaveDialogInfo(deeprInfo, executeAfterSave))
132+
onSaveDialogInfoChange(SaveDialogInfo(deeprInfo.copy(link = normalizedLink), executeAfterSave))
129133
}
130134

131135
val modalBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)

app/src/main/java/com/yogeshpaliyal/deepr/util/Utils.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ fun openDeeplink(
1616
link: String,
1717
): Boolean {
1818
if (!isValidDeeplink(link)) return false
19+
val normalizedLink = normalizeLink(link)
1920
return try {
2021
Log.d("Anas", "opened")
21-
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
22+
val intent = Intent(Intent.ACTION_VIEW, normalizedLink.toUri())
2223
context.startActivity(intent)
2324
true
2425
} catch (e: Exception) {
2526
e.printStackTrace()
2627
Toast
2728
.makeText(
2829
context,
29-
context.getString(R.string.invalid_deeplink_toast, link),
30+
context.getString(R.string.invalid_deeplink_toast, normalizedLink),
3031
Toast.LENGTH_SHORT,
3132
).show()
3233
// Optionally, show a toast or a dialog to the user that the link is invalid
@@ -44,7 +45,8 @@ fun getShortcutAppIcon(
4445
return IconCompat.createWithResource(context, R.mipmap.ic_launcher)
4546
}
4647
try {
47-
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
48+
val normalizedLink = normalizeLink(link)
49+
val intent = Intent(Intent.ACTION_VIEW, normalizedLink.toUri())
4850
val appIconDrawable = context.packageManager.getActivityIcon(intent)
4951

5052
// Convert the Drawable to a Bitmap
@@ -62,10 +64,31 @@ fun getShortcutAppIcon(
6264
}
6365
}
6466

67+
fun normalizeLink(link: String): String {
68+
if (link.isBlank()) return link
69+
70+
val trimmedLink = link.trim()
71+
72+
// Check if the link already has a scheme
73+
if (trimmedLink.contains("://")) {
74+
return trimmedLink
75+
}
76+
77+
// If it looks like a URL (contains a dot and doesn't start with a scheme),
78+
// prepend https://
79+
if (trimmedLink.contains(".")) {
80+
return "https://$trimmedLink"
81+
}
82+
83+
// Return as-is for other cases (like custom schemes without ://)
84+
return trimmedLink
85+
}
86+
6587
fun isValidDeeplink(link: String): Boolean {
6688
if (link.isBlank()) return false
6789
return try {
68-
val uri = link.toUri()
90+
val normalizedLink = normalizeLink(link)
91+
val uri = normalizedLink.toUri()
6992
val hasValidScheme = uri.scheme != null && uri.scheme!!.isNotBlank()
7093
val hasValidAuthority = uri.authority != null && uri.authority!!.isNotBlank()
7194
hasValidScheme && hasValidAuthority
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.yogeshpaliyal.deepr
2+
3+
import com.yogeshpaliyal.deepr.util.normalizeLink
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Test
6+
7+
/**
8+
* Test for utility functions, specifically link normalization
9+
*/
10+
class UtilsTest {
11+
@Test
12+
fun normalizeLink_addsHttpsToUrlWithoutScheme() {
13+
assertEquals("https://cnn.com", normalizeLink("cnn.com"))
14+
assertEquals("https://www.google.com", normalizeLink("www.google.com"))
15+
assertEquals("https://example.com/path", normalizeLink("example.com/path"))
16+
}
17+
18+
@Test
19+
fun normalizeLink_preservesExistingHttpsScheme() {
20+
assertEquals("https://cnn.com", normalizeLink("https://cnn.com"))
21+
assertEquals("https://www.google.com", normalizeLink("https://www.google.com"))
22+
}
23+
24+
@Test
25+
fun normalizeLink_preservesExistingHttpScheme() {
26+
assertEquals("http://example.com", normalizeLink("http://example.com"))
27+
}
28+
29+
@Test
30+
fun normalizeLink_preservesCustomSchemes() {
31+
assertEquals("app://deeplink", normalizeLink("app://deeplink"))
32+
assertEquals("myapp://open", normalizeLink("myapp://open"))
33+
assertEquals("intent://action", normalizeLink("intent://action"))
34+
}
35+
36+
@Test
37+
fun normalizeLink_handlesBlankInput() {
38+
assertEquals("", normalizeLink(""))
39+
assertEquals("", normalizeLink(" "))
40+
}
41+
42+
@Test
43+
fun normalizeLink_trimsWhitespace() {
44+
assertEquals("https://cnn.com", normalizeLink(" cnn.com "))
45+
assertEquals("https://example.com", normalizeLink(" example.com "))
46+
}
47+
48+
@Test
49+
fun normalizeLink_handlesUrlsWithoutDots() {
50+
// For custom schemes without dots, return as-is
51+
assertEquals("localhost", normalizeLink("localhost"))
52+
}
53+
54+
@Test
55+
fun normalizeLink_handlesComplexUrls() {
56+
assertEquals(
57+
"https://www.example.com/path?query=value",
58+
normalizeLink("www.example.com/path?query=value"),
59+
)
60+
assertEquals(
61+
"https://example.com:8080/path",
62+
normalizeLink("example.com:8080/path"),
63+
)
64+
}
65+
}

0 commit comments

Comments
 (0)