Skip to content

Commit 160a55d

Browse files
committed
added support for external urls
1 parent 66656e0 commit 160a55d

File tree

5 files changed

+208
-24
lines changed

5 files changed

+208
-24
lines changed

DEEP_LINKING_GUIDE.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Deep Linking & External URL Support
2+
3+
## Overview
4+
The PrivacyFirst app now supports opening external URLs from other apps, browsers, and links. When you click a link anywhere in the system, you'll see an option to "Open with PrivacyFirst".
5+
6+
## Features Added
7+
8+
### 1. Intent Filters
9+
- **HTTP/HTTPS URLs**: The app can now handle all http:// and https:// links
10+
- **Custom Deep Links**: Support for custom `privacyfirst://open` URLs
11+
- **Launch Mode**: Set to `singleTask` to prevent multiple instances
12+
13+
### 2. URL Handling
14+
When a user clicks a link from:
15+
- A web browser
16+
- Email client
17+
- Messaging app
18+
- Any other app
19+
20+
The system will show "Open with PrivacyFirst" as an option.
21+
22+
## How It Works
23+
24+
### Android Manifest Changes
25+
Added intent filters to `MainActivity`:
26+
```xml
27+
<!-- Deep link support for http/https URLs -->
28+
<intent-filter android:autoVerify="true">
29+
<action android:name="android.intent.action.VIEW" />
30+
<category android:name="android.intent.category.DEFAULT" />
31+
<category android:name="android.intent.category.BROWSABLE" />
32+
<data android:scheme="http" />
33+
<data android:scheme="https" />
34+
</intent-filter>
35+
36+
<!-- Custom app deep links -->
37+
<intent-filter>
38+
<action android:name="android.intent.action.VIEW" />
39+
<category android:name="android.intent.category.DEFAULT" />
40+
<category android:name="android.intent.category.BROWSABLE" />
41+
<data android:scheme="privacyfirst" android:host="open" />
42+
</intent-filter>
43+
```
44+
45+
### MainActivity Updates
46+
- Extracts URLs from incoming `ACTION_VIEW` intents
47+
- Passes URL to the navigation system via `pendingUrlState`
48+
- Handles both fresh app launches and already-running app scenarios via `onNewIntent()`
49+
50+
### Navigation Updates
51+
- `AppNavigation` accepts optional `pendingUrlState` parameter
52+
- Passes the pending URL to `WebViewScreen`
53+
54+
### WebViewScreen Updates
55+
- Accepts optional `externalUrl` parameter
56+
- Loads external URL if provided, otherwise loads default HTML page
57+
- Maintains all existing security features (whitelist checking, SSL verification, etc.)
58+
59+
## Usage Examples
60+
61+
### From Other Apps
62+
1. Click any web link in an email, message, or document
63+
2. Select "Open with PrivacyFirst" from the dialog
64+
3. The URL opens in PrivacyFirst's secure WebView
65+
66+
### Custom Deep Links
67+
Create a deep link in your content:
68+
```html
69+
<a href="privacyfirst://open?url=https://example.com">Open in PrivacyFirst</a>
70+
```
71+
72+
### Testing
73+
You can test deep linking using ADB:
74+
```bash
75+
# Test HTTP link
76+
adb shell am start -a android.intent.action.VIEW -d "https://www.example.com" com.secure.privacyfirst
77+
78+
# Test custom deep link
79+
adb shell am start -a android.intent.action.VIEW -d "privacyfirst://open" com.secure.privacyfirst
80+
```
81+
82+
## Security Considerations
83+
84+
### Whitelist Protection
85+
- External URLs still respect the whitelist settings
86+
- Non-whitelisted domains show a warning dialog
87+
- Users must explicitly approve non-whitelisted URLs
88+
89+
### Security Levels
90+
- **HIGH**: Screenshots blocked, downloads disabled
91+
- **MEDIUM**: HTTPS enforced, SSL strictly verified
92+
- **LOW**: HTTP allowed, more permissive SSL handling
93+
94+
### SSL Verification
95+
All external URLs go through the same SSL verification process as internally navigated URLs.
96+
97+
## User Experience
98+
99+
1. **First Launch**: User clicks link → System shows "Open with" dialog → User selects PrivacyFirst
100+
2. **App Running**: URL loads immediately in existing WebView
101+
3. **Non-whitelisted URLs**: Warning dialog appears, user can approve or cancel
102+
103+
## Notes
104+
- The app uses `singleTask` launch mode to prevent duplicate instances
105+
- Existing navigation and authentication flows are preserved
106+
- External URLs clear when user navigates away or closes the app

app/src/main/AndroidManifest.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,30 @@
2929
<activity
3030
android:name=".MainActivity"
3131
android:exported="true"
32+
android:launchMode="singleTask"
3233
android:configChanges="orientation|screenSize"
3334
android:theme="@style/Theme.PrivacyFirst">
3435
<intent-filter>
3536
<action android:name="android.intent.action.MAIN" />
3637
<category android:name="android.intent.category.LAUNCHER" />
3738
</intent-filter>
39+
40+
<!-- Deep link support for http/https URLs -->
41+
<intent-filter android:autoVerify="true">
42+
<action android:name="android.intent.action.VIEW" />
43+
<category android:name="android.intent.category.DEFAULT" />
44+
<category android:name="android.intent.category.BROWSABLE" />
45+
<data android:scheme="http" />
46+
<data android:scheme="https" />
47+
</intent-filter>
48+
49+
<!-- Custom app deep links -->
50+
<intent-filter>
51+
<action android:name="android.intent.action.VIEW" />
52+
<category android:name="android.intent.category.DEFAULT" />
53+
<category android:name="android.intent.category.BROWSABLE" />
54+
<data android:scheme="privacyfirst" android:host="open" />
55+
</intent-filter>
3856
</activity>
3957

4058
<activity

app/src/main/java/com/secure/privacyfirst/MainActivity.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.secure.privacyfirst
22

3+
import android.content.Intent
4+
import android.net.Uri
35
import android.os.Bundle
6+
import android.util.Log
47
import android.webkit.WebView
58
import androidx.appcompat.app.AppCompatActivity
69
import androidx.activity.OnBackPressedCallback
@@ -9,6 +12,7 @@ import androidx.activity.enableEdgeToEdge
912
import androidx.compose.foundation.layout.fillMaxSize
1013
import androidx.compose.material3.MaterialTheme
1114
import androidx.compose.material3.Surface
15+
import androidx.compose.runtime.mutableStateOf
1216
import androidx.compose.ui.Modifier
1317
import androidx.compose.ui.graphics.Color
1418
import androidx.compose.ui.graphics.toArgb
@@ -20,8 +24,13 @@ import com.secure.privacyfirst.auth.AuthStateManager
2024
import com.secure.privacyfirst.navigation.AppNavigation
2125
import com.secure.privacyfirst.ui.theme.PrivacyFirstTheme
2226

27+
private const val TAG = "MainActivity"
28+
2329
class MainActivity : AppCompatActivity() {
2430

31+
// Mutable state to hold the incoming URL from external sources
32+
private val pendingUrl = mutableStateOf<String?>(null)
33+
2534
private val appLifecycleObserver = object : DefaultLifecycleObserver {
2635
override fun onStop(owner: LifecycleOwner) {
2736
super.onStop(owner)
@@ -42,6 +51,9 @@ class MainActivity : AppCompatActivity() {
4251
// Enable WebView debugging
4352
WebView.setWebContentsDebuggingEnabled(true)
4453

54+
// Handle incoming intent URL
55+
handleIncomingIntent(intent)
56+
4557
enableEdgeToEdge()
4658

4759
// Edge-to-edge automatically handles system bar colors
@@ -68,7 +80,27 @@ class MainActivity : AppCompatActivity() {
6880
modifier = Modifier.fillMaxSize(),
6981
color = MaterialTheme.colorScheme.background
7082
) {
71-
AppNavigation()
83+
AppNavigation(pendingUrlState = pendingUrl)
84+
}
85+
}
86+
}
87+
}
88+
89+
override fun onNewIntent(intent: Intent) {
90+
super.onNewIntent(intent)
91+
// Handle new intents when app is already running
92+
handleIncomingIntent(intent)
93+
}
94+
95+
private fun handleIncomingIntent(intent: Intent?) {
96+
intent?.let {
97+
when (it.action) {
98+
Intent.ACTION_VIEW -> {
99+
val url = it.data?.toString()
100+
if (!url.isNullOrEmpty()) {
101+
Log.d(TAG, "Received external URL: $url")
102+
pendingUrl.value = url
103+
}
72104
}
73105
}
74106
}

app/src/main/java/com/secure/privacyfirst/navigation/AppNavigation.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.secure.privacyfirst.navigation
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.MutableState
56
import androidx.compose.runtime.collectAsState
67
import androidx.compose.runtime.getValue
78
import androidx.compose.ui.platform.LocalContext
@@ -19,7 +20,7 @@ import com.secure.privacyfirst.ui.screens.PasswordManagerScreen
1920
import com.secure.privacyfirst.ui.screens.SetupPinScreen
2021

2122
@Composable
22-
fun AppNavigation() {
23+
fun AppNavigation(pendingUrlState: MutableState<String?>? = null) {
2324
val navController = rememberNavController()
2425
val context = LocalContext.current
2526
val database = AppDatabase.getDatabase(context)
@@ -91,7 +92,7 @@ fun AppNavigation() {
9192
}
9293

9394
composable(Screen.WebView.route) {
94-
WebViewScreen()
95+
WebViewScreen(externalUrl = pendingUrlState?.value)
9596
}
9697

9798
composable(Screen.Auth.route) {

app/src/main/java/com/secure/privacyfirst/ui/screens/WebViewScreen.kt

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ import kotlinx.coroutines.launch
5656
private const val TAG = "WebViewScreen"
5757
private const val CAMERA_PERMISSION_REQUEST_CODE = 100
5858
private const val MIC_PERMISSION_REQUEST_CODE = 101
59+
private const val DEFAULT_URL = "file:///android_asset/index.html"
5960

6061
@Composable
61-
fun WebViewScreen() {
62+
fun WebViewScreen(externalUrl: String? = null) {
6263
val context = LocalContext.current
6364
val scope = rememberCoroutineScope()
6465
val preferencesManager = remember { UserPreferencesManager(context) }
@@ -72,12 +73,16 @@ fun WebViewScreen() {
7273
var whitelistUrls by remember { mutableStateOf<List<String>>(emptyList()) }
7374
var isLoadingWhitelist by remember { mutableStateOf(true) }
7475

76+
// External URL state
77+
var urlToLoad by remember { mutableStateOf(externalUrl) }
78+
var initialUrlLoaded by remember { mutableStateOf(false) }
79+
7580
// Warning dialog states
7681
var showCameraWarning by remember { mutableStateOf(false) }
7782
var showMicWarning by remember { mutableStateOf(false) }
7883
var showExternalAppWarning by remember { mutableStateOf(false) }
7984
var showExitDialog by remember { mutableStateOf(false) }
80-
var externalUrl by remember { mutableStateOf("") }
85+
var externalUrlDialog by remember { mutableStateOf("") }
8186
var pendingPermissionRequest by remember { mutableStateOf<PermissionRequest?>(null) }
8287

8388
// Fetch whitelist on start
@@ -358,7 +363,7 @@ fun WebViewScreen() {
358363

359364
if (!isAllowed) {
360365
// Show external app warning instead of blocking directly
361-
externalUrl = url
366+
externalUrlDialog = url
362367
showExternalAppWarning = true
363368
Log.w(TAG, "Non-whitelisted domain attempted: $host")
364369
return true
@@ -519,29 +524,44 @@ fun WebViewScreen() {
519524
}
520525
}
521526

522-
// Load custom HTML from assets with user's name
527+
// Load initial URL (external or default)
523528
try {
524-
val htmlContent = context.assets.open("index.html").bufferedReader().use { it.readText() }
525-
val displayName = if (userName.isNotBlank()) userName else "User"
526-
Log.d(TAG, "Loading HTML with userName: '$displayName'")
527-
val personalizedHtml = htmlContent.replace("Welcome, Nimit", "Welcome, $displayName")
528-
loadDataWithBaseURL(
529-
"file:///android_asset/",
530-
personalizedHtml,
531-
"text/html",
532-
"UTF-8",
533-
null
534-
)
529+
val initialUrl = urlToLoad ?: DEFAULT_URL
530+
if (initialUrl.startsWith("http://") || initialUrl.startsWith("https://")) {
531+
// Load external URL
532+
Log.d(TAG, "Loading external URL: $initialUrl")
533+
loadUrl(initialUrl)
534+
} else {
535+
// Load custom HTML from assets with user's name
536+
val htmlContent = context.assets.open("index.html").bufferedReader().use { it.readText() }
537+
val displayName = if (userName.isNotBlank()) userName else "User"
538+
Log.d(TAG, "Loading HTML with userName: '$displayName'")
539+
val personalizedHtml = htmlContent.replace("Welcome, Nimit", "Welcome, $displayName")
540+
loadDataWithBaseURL(
541+
"file:///android_asset/",
542+
personalizedHtml,
543+
"text/html",
544+
"UTF-8",
545+
null
546+
)
547+
}
535548
} catch (e: Exception) {
536-
Log.e(TAG, "Error loading HTML: ${e.message}", e)
537-
loadUrl("file:///android_asset/index.html")
549+
Log.e(TAG, "Error loading content: ${e.message}", e)
550+
loadUrl(DEFAULT_URL)
538551
}
539552
}.also { webView = it }
540553
},
541554
update = { view ->
542555
webView = view
543-
// Reload with updated userName when it changes
544-
if (userName.isNotBlank()) {
556+
557+
// Handle external URL when it changes
558+
if (!initialUrlLoaded && urlToLoad != null &&
559+
(urlToLoad!!.startsWith("http://") || urlToLoad!!.startsWith("https://"))) {
560+
Log.d(TAG, "Loading external URL in update: $urlToLoad")
561+
view.loadUrl(urlToLoad!!)
562+
initialUrlLoaded = true
563+
} else if (userName.isNotBlank() && urlToLoad == null) {
564+
// Reload with updated userName when it changes (only for default HTML)
545565
try {
546566
val htmlContent = context.assets.open("index.html").bufferedReader().use { it.readText() }
547567
val personalizedHtml = htmlContent.replace("Welcome, Nimit", "Welcome, $userName")
@@ -561,6 +581,13 @@ fun WebViewScreen() {
561581
)
562582
}
563583

584+
// LaunchedEffect to handle external URL changes
585+
LaunchedEffect(externalUrl) {
586+
if (externalUrl != null && !initialUrlLoaded) {
587+
urlToLoad = externalUrl
588+
}
589+
}
590+
564591
// Warning Dialogs
565592
if (showCameraWarning) {
566593
CameraAccessWarningDialog(
@@ -634,14 +661,14 @@ fun WebViewScreen() {
634661

635662
if (showExternalAppWarning) {
636663
ExternalAppWarningDialog(
637-
url = externalUrl,
664+
url = externalUrlDialog,
638665
onDismiss = {
639666
showExternalAppWarning = false
640667
},
641668
onProceed = {
642669
showExternalAppWarning = false
643670
// User chose to proceed - load the external URL
644-
webView?.loadUrl(externalUrl)
671+
webView?.loadUrl(externalUrlDialog)
645672
Log.w(TAG, "User proceeded to external URL: $externalUrl")
646673
},
647674
onCancel = {

0 commit comments

Comments
 (0)