Skip to content

Commit 4f1abd2

Browse files
committed
feat: Notification for new sites
1 parent 709c58b commit 4f1abd2

File tree

8 files changed

+559
-35
lines changed

8 files changed

+559
-35
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<!-- Biometric authentication -->
1010
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
1111

12+
<!-- Notification permissions for autosave prompts (Android 13+) -->
13+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
14+
1215
<application
1316
android:name=".PasswordManagerApplication"
1417
android:allowBackup="false"
@@ -77,6 +80,15 @@
7780
android:taskAffinity=""
7881
android:launchMode="singleTask" />
7982

83+
<!-- Notification Password Input Activity - Launched from save password notifications -->
84+
<activity
85+
android:name=".autofill.ui.NotificationPasswordInputActivity"
86+
android:exported="false"
87+
android:theme="@style/Theme.NexPass.Transparent"
88+
android:excludeFromRecents="true"
89+
android:taskAffinity=""
90+
android:launchMode="singleTask" />
91+
8092
<!-- Security Settings -->
8193
<meta-data
8294
android:name="android.security.KEYSTORE_PATH_RESTRICTIONS_ENABLED"
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package com.nexpass.passwordmanager.autofill.notification
2+
3+
import android.Manifest
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.app.PendingIntent
7+
import android.content.Context
8+
import android.content.Intent
9+
import android.content.pm.PackageManager
10+
import android.os.Build
11+
import android.util.Log
12+
import androidx.core.app.NotificationCompat
13+
import androidx.core.app.NotificationManagerCompat
14+
import androidx.core.content.ContextCompat
15+
import com.nexpass.passwordmanager.R
16+
17+
/**
18+
* Manages notifications for autosave feature.
19+
* Shows notifications prompting users to save passwords.
20+
*/
21+
class AutosaveNotificationManager(private val context: Context) {
22+
23+
companion object {
24+
private const val TAG = "AutosaveNotificationMgr"
25+
private const val CHANNEL_ID = "autosave_channel"
26+
private const val CHANNEL_NAME = "Password Save Requests"
27+
private const val NOTIFICATION_ID = 1001
28+
const val EXTRA_PACKAGE_NAME = "packageName"
29+
const val EXTRA_WEB_DOMAIN = "webDomain"
30+
}
31+
32+
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
33+
private val notificationManagerCompat = NotificationManagerCompat.from(context)
34+
35+
init {
36+
createNotificationChannel()
37+
}
38+
39+
/**
40+
* Create notification channel for autosave (Android 8.0+)
41+
*/
42+
private fun createNotificationChannel() {
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
44+
val channel = NotificationChannel(
45+
CHANNEL_ID,
46+
CHANNEL_NAME,
47+
NotificationManager.IMPORTANCE_HIGH
48+
).apply {
49+
description = "Notifications asking if you want to save passwords to NexPass"
50+
setShowBadge(true)
51+
enableLights(true)
52+
enableVibration(false)
53+
}
54+
notificationManager.createNotificationChannel(channel)
55+
}
56+
}
57+
58+
/**
59+
* Show a notification prompting to save password.
60+
*
61+
* @param packageName The package name of the app with the login form
62+
* @param webDomain The web domain (if browser), null otherwise
63+
*/
64+
fun showSavePasswordNotification(packageName: String, webDomain: String?) {
65+
Log.d(TAG, "=== showSavePasswordNotification called ===")
66+
Log.d(TAG, "Package: $packageName, Domain: $webDomain")
67+
68+
// Check for POST_NOTIFICATIONS permission on Android 13+
69+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
70+
val hasPermission = ContextCompat.checkSelfPermission(
71+
context,
72+
Manifest.permission.POST_NOTIFICATIONS
73+
) == PackageManager.PERMISSION_GRANTED
74+
75+
if (!hasPermission) {
76+
Log.w(TAG, "⚠️ POST_NOTIFICATIONS permission NOT GRANTED (Android 13+)")
77+
Log.w(TAG, "User needs to grant notification permission in app settings")
78+
return
79+
}
80+
Log.d(TAG, "✅ POST_NOTIFICATIONS permission granted (Android 13+)")
81+
}
82+
83+
// Check if notifications are enabled (for older Android versions)
84+
if (!notificationManagerCompat.areNotificationsEnabled()) {
85+
Log.w(TAG, "⚠️ Notifications are DISABLED for NexPass!")
86+
Log.w(TAG, "User needs to enable notifications in Settings > Apps > NexPass > Notifications")
87+
return
88+
}
89+
90+
Log.d(TAG, "✅ Notifications are enabled")
91+
92+
val displayName = webDomain ?: packageName
93+
Log.d(TAG, "Display name: $displayName")
94+
95+
// Create intent to launch password input activity when notification is tapped
96+
val intent = Intent(context, com.nexpass.passwordmanager.autofill.ui.NotificationPasswordInputActivity::class.java).apply {
97+
putExtra(EXTRA_PACKAGE_NAME, packageName)
98+
putExtra(EXTRA_WEB_DOMAIN, webDomain)
99+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
100+
}
101+
102+
val pendingIntent = PendingIntent.getActivity(
103+
context,
104+
0,
105+
intent,
106+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
107+
)
108+
109+
Log.d(TAG, "Building notification...")
110+
111+
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
112+
.setSmallIcon(R.drawable.ic_launcher_foreground)
113+
.setContentTitle("Save password to NexPass?")
114+
.setContentText("Tap to save password for $displayName")
115+
.setPriority(NotificationCompat.PRIORITY_HIGH)
116+
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
117+
.setAutoCancel(true) // Dismiss when tapped
118+
.setContentIntent(pendingIntent)
119+
.build()
120+
121+
try {
122+
Log.d(TAG, "Posting notification with ID: $NOTIFICATION_ID")
123+
notificationManagerCompat.notify(NOTIFICATION_ID, notification)
124+
Log.d(TAG, "✅ Notification posted successfully!")
125+
} catch (e: Exception) {
126+
Log.e(TAG, "❌ Failed to post notification", e)
127+
}
128+
}
129+
130+
/**
131+
* Cancel the save password notification.
132+
*/
133+
fun cancelSavePasswordNotification() {
134+
notificationManager.cancel(NOTIFICATION_ID)
135+
}
136+
}

app/src/main/java/com/nexpass/passwordmanager/autofill/service/AutofillResponseBuilder.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ class AutofillResponseBuilder(private val context: Context) {
4343
}
4444

4545
// Add "Save Password" manual trigger dataset
46+
android.util.Log.d("AutofillResponseBuilder", "=== Attempting to build manual save dataset ===")
47+
android.util.Log.d("AutofillResponseBuilder", "Fields count: ${fields.size}, Package: $packageName")
4648
val saveDataset = buildManualSaveDataset(fields, packageName)
47-
saveDataset?.let { fillResponseBuilder.addDataset(it) }
49+
if (saveDataset != null) {
50+
android.util.Log.d("AutofillResponseBuilder", "✅ Manual save dataset created successfully, adding to response")
51+
fillResponseBuilder.addDataset(saveDataset)
52+
} else {
53+
android.util.Log.w("AutofillResponseBuilder", "❌ Manual save dataset is NULL - not added to response")
54+
}
4855

4956
// Add save info to allow saving new credentials (still needed for native apps)
5057
val saveInfo = buildSaveInfo(fields)
@@ -70,10 +77,13 @@ class AutofillResponseBuilder(private val context: Context) {
7077
fields: List<AutofillField>,
7178
packageName: String
7279
): Dataset? {
80+
android.util.Log.d("AutofillResponseBuilder", "buildManualSaveDataset() called for package: $packageName")
81+
7382
// Create intent to launch manual save capture
7483
val saveIntent = Intent(context, com.nexpass.passwordmanager.autofill.ui.ManualSaveCaptureActivity::class.java).apply {
7584
putExtra("packageName", packageName)
76-
// Note: AssistStructure will be automatically added by Android when dataset is selected
85+
// NOTE: Dataset authentication does NOT provide EXTRA_ASSIST_STRUCTURE automatically!
86+
// This is a known Android limitation - only FillResponse.setAuthentication() provides it
7787
}
7888

7989
val savePendingIntent = PendingIntent.getActivity(
@@ -100,20 +110,26 @@ class AutofillResponseBuilder(private val context: Context) {
100110

101111
// We need at least one field to attach the authentication to
102112
passwordField?.let { field ->
113+
android.util.Log.d("AutofillResponseBuilder", "Found password field for manual save dataset")
103114
datasetBuilder.setValue(
104115
field.autofillId,
105116
null, // Don't actually fill any value
106117
presentation
107118
)
119+
} ?: run {
120+
android.util.Log.w("AutofillResponseBuilder", "⚠️ No password field found - manual save dataset may not work!")
108121
}
109122

110123
// Set authentication on the dataset
111124
datasetBuilder.setAuthentication(savePendingIntent.intentSender)
125+
android.util.Log.d("AutofillResponseBuilder", "Set authentication on manual save dataset")
112126

113127
return try {
114-
datasetBuilder.build()
128+
val dataset = datasetBuilder.build()
129+
android.util.Log.d("AutofillResponseBuilder", "✅ Manual save dataset built successfully")
130+
dataset
115131
} catch (e: Exception) {
116-
android.util.Log.e("AutofillResponseBuilder", "Failed to build manual save dataset", e)
132+
android.util.Log.e("AutofillResponseBuilder", "Failed to build manual save dataset", e)
117133
null
118134
}
119135
}

0 commit comments

Comments
 (0)