Skip to content

Commit 12788fd

Browse files
Feat: Add biometric authentication for locked apps and enable locking apps from app drawer
- Implemented biometric authentication for apps marked as locked, requiring fingerprint verification before launch. - Added functionality to lock apps directly from the app drawer, allowing users to easily secure apps with biometric protection.
1 parent 9dd058f commit 12788fd

File tree

17 files changed

+372
-104
lines changed

17 files changed

+372
-104
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
1717
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
1818
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
19-
19+
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
2020
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
2121
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
2222

app/src/main/java/com/github/droidworksstudio/common/ContextExtensions.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.content.Intent
99
import android.content.pm.ApplicationInfo
1010
import android.content.pm.LauncherApps
1111
import android.content.pm.PackageManager
12+
import android.content.res.Configuration
1213
import android.net.Uri
1314
import android.os.Bundle
1415
import android.os.UserHandle
@@ -20,6 +21,7 @@ import android.util.Log.d
2021
import android.view.LayoutInflater
2122
import android.view.View
2223
import android.view.ViewGroup
24+
import android.view.inputmethod.InputMethodManager
2325
import android.widget.Toast
2426
import androidx.biometric.BiometricManager
2527
import androidx.core.content.ContextCompat
@@ -41,6 +43,14 @@ fun Context.showShortToast(message: String) {
4143
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
4244
}
4345

46+
fun Context.hasSoftKeyboard(): Boolean {
47+
val config = resources.configuration
48+
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
49+
50+
// True if the device does not have physical keys AND at least one soft input method is installed
51+
return config.keyboard == Configuration.KEYBOARD_NOKEYS && imm.inputMethodList.isNotEmpty()
52+
}
53+
4454
fun Context.openSearch(query: String? = null) {
4555
val intent = Intent(Intent.ACTION_WEB_SEARCH)
4656
intent.putExtra(SearchManager.QUERY, query ?: "")

app/src/main/java/com/github/droidworksstudio/common/FragmentExtensions.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.github.droidworksstudio.common
22

3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.view.View
6+
import android.view.inputmethod.InputMethodManager
37
import android.widget.Toast
48
import androidx.fragment.app.Fragment
59

@@ -9,4 +13,13 @@ fun Fragment.showLongToast(message: String) {
913

1014
fun Fragment.showShortToast(message: String) {
1115
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
12-
}
16+
}
17+
18+
@SuppressLint("ServiceCast")
19+
fun Fragment.hideKeyboard() {
20+
val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
21+
val view = activity?.currentFocus ?: View(requireContext()) // Use current focus or a new view
22+
if (inputMethodManager.isActive) { // Check if IME is active before hiding
23+
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
24+
}
25+
}

app/src/main/java/com/github/droidworksstudio/mlauncher/MainViewModel.kt

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import android.app.Application
44
import android.content.ComponentName
55
import android.content.Context
66
import android.content.pm.LauncherApps
7+
import android.os.Process
8+
import android.os.UserHandle
9+
import androidx.biometric.BiometricPrompt
10+
import androidx.fragment.app.Fragment
711
import androidx.lifecycle.AndroidViewModel
812
import androidx.lifecycle.MutableLiveData
913
import androidx.lifecycle.viewModelScope
14+
import com.github.droidworksstudio.common.CrashHandler
15+
import com.github.droidworksstudio.common.hideKeyboard
16+
import com.github.droidworksstudio.common.showLongToast
1017
import com.github.droidworksstudio.common.showShortToast
1118
import com.github.droidworksstudio.mlauncher.data.AppListItem
1219
import com.github.droidworksstudio.mlauncher.data.Constants
@@ -16,9 +23,12 @@ import com.github.droidworksstudio.mlauncher.helper.analytics.AppUsageMonitor
1623
import com.github.droidworksstudio.mlauncher.helper.getAppsList
1724
import com.github.droidworksstudio.mlauncher.helper.ismlauncherDefault
1825
import com.github.droidworksstudio.mlauncher.helper.setDefaultHomeScreen
26+
import com.github.droidworksstudio.mlauncher.helper.utils.BiometricHelper
1927
import kotlinx.coroutines.launch
2028

2129
class MainViewModel(application: Application) : AndroidViewModel(application) {
30+
private lateinit var biometricHelper: BiometricHelper
31+
2232
private val appContext by lazy { application.applicationContext }
2333
private val prefs = Prefs(appContext)
2434

@@ -48,10 +58,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
4858
val recentCounter = MutableLiveData(prefs.recentCounter)
4959
val iconPack = MutableLiveData(prefs.iconPack)
5060

51-
fun selectedApp(app: AppListItem, flag: AppDrawerFlag, n: Int = 0) {
61+
fun selectedApp(fragment: Fragment, app: AppListItem, flag: AppDrawerFlag, n: Int = 0) {
5262
when (flag) {
53-
AppDrawerFlag.LaunchApp, AppDrawerFlag.HiddenApps, AppDrawerFlag.PrivateApps -> {
54-
launchApp(app)
63+
AppDrawerFlag.LaunchApp,
64+
AppDrawerFlag.HiddenApps,
65+
AppDrawerFlag.LockedApps,
66+
AppDrawerFlag.PrivateApps -> {
67+
launchApp(app, fragment)
5568
}
5669

5770
AppDrawerFlag.SetHomeApp -> {
@@ -98,36 +111,72 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
98111
showFloating.value = visibility
99112
}
100113

101-
private fun launchApp(appListItem: AppListItem) {
114+
fun launchApp(appListItem: AppListItem, fragment: Fragment) {
115+
biometricHelper = BiometricHelper(fragment)
116+
117+
val packageName = appListItem.activityPackage
118+
val currentLockedApps = prefs.lockedApps
119+
120+
if (currentLockedApps.contains(packageName)) {
121+
fragment.hideKeyboard()
122+
biometricHelper.startBiometricAuth(appListItem, object : BiometricHelper.CallbackApp {
123+
override fun onAuthenticationSucceeded(appListItem: AppListItem) {
124+
launchUnlockedApp(appListItem)
125+
}
126+
127+
override fun onAuthenticationFailed() {
128+
appContext.showLongToast(
129+
appContext.getString(R.string.text_authentication_failed)
130+
)
131+
}
132+
133+
override fun onAuthenticationError(errorCode: Int, errorMessage: CharSequence?) {
134+
when (errorCode) {
135+
BiometricPrompt.ERROR_USER_CANCELED -> appContext.showLongToast(
136+
appContext.getString(R.string.text_authentication_cancel)
137+
)
138+
139+
else -> appContext.showLongToast(
140+
appContext.getString(R.string.text_authentication_error).format(
141+
errorMessage,
142+
errorCode
143+
)
144+
)
145+
}
146+
}
147+
})
148+
} else {
149+
launchUnlockedApp(appListItem)
150+
}
151+
}
152+
153+
private fun launchUnlockedApp(appListItem: AppListItem) {
102154
val packageName = appListItem.activityPackage
103-
val appActivityName = appListItem.activityClass
104155
val userHandle = appListItem.user
105156
val launcher = appContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
106157
val activityInfo = launcher.getActivityList(packageName, userHandle)
107158

108-
val component = when (activityInfo.size) {
109-
0 -> {
110-
appContext.showShortToast("App not found")
111-
return
112-
}
113-
114-
1 -> ComponentName(packageName, activityInfo[0].name)
115-
else -> if (appActivityName.isNotEmpty()) {
116-
ComponentName(packageName, appActivityName)
117-
} else {
118-
ComponentName(packageName, activityInfo[activityInfo.size - 1].name)
119-
}
159+
if (activityInfo.isNotEmpty()) {
160+
val component = ComponentName(packageName, activityInfo.first().name)
161+
launchAppWithPermissionCheck(component, packageName, userHandle, launcher)
162+
} else {
163+
appContext.showShortToast("App not found")
120164
}
165+
}
166+
121167

168+
private fun launchAppWithPermissionCheck(component: ComponentName, packageName: String, userHandle: UserHandle, launcher: LauncherApps) {
122169
try {
123170
val appUsageTracker = AppUsageMonitor.createInstance(appContext)
124171
appUsageTracker.updateLastUsedTimestamp(packageName)
125172
launcher.startMainActivity(component, userHandle, null, null)
173+
CrashHandler.logUserAction("${component.packageName} App Launched")
126174
} catch (_: SecurityException) {
127175
try {
128176
val appUsageTracker = AppUsageMonitor.createInstance(appContext)
129177
appUsageTracker.updateLastUsedTimestamp(packageName)
130-
launcher.startMainActivity(component, android.os.Process.myUserHandle(), null, null)
178+
launcher.startMainActivity(component, Process.myUserHandle(), null, null)
179+
CrashHandler.logUserAction("${component.packageName} App Launched")
131180
} catch (_: Exception) {
132181
appContext.showShortToast("Unable to launch app")
133182
}

app/src/main/java/com/github/droidworksstudio/mlauncher/data/Constants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ object Constants {
108108
enum class AppDrawerFlag {
109109
LaunchApp,
110110
HiddenApps,
111+
LockedApps,
111112
PrivateApps,
112113
SetHomeApp,
113114
SetShortSwipeUp,

app/src/main/java/com/github/droidworksstudio/mlauncher/data/Prefs.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ private const val CLICK_FLOATING_ACTION = "CLICK_FLOATING_ACTION"
6565
private const val CLICK_DATE_ACTION = "CLICK_DATE_ACTION"
6666
private const val DOUBLE_TAP_ACTION = "DOUBLE_TAP_ACTION"
6767
private const val HIDDEN_APPS = "HIDDEN_APPS"
68+
private const val LOCKED_APPS = "LOCKED_APPS"
6869
private const val SEARCH_ENGINE = "SEARCH_ENGINE"
6970
private const val LAUNCHER_FONT = "LAUNCHER_FONT"
7071
private const val APP_NAME = "APP_NAME"
@@ -686,6 +687,10 @@ class Prefs(val context: Context) {
686687
get() = prefs.getStringSet(HIDDEN_APPS, mutableSetOf()) as MutableSet<String>
687688
set(value) = prefs.edit().putStringSet(HIDDEN_APPS, value).apply()
688689

690+
var lockedApps: MutableSet<String>
691+
get() = prefs.getStringSet(LOCKED_APPS, mutableSetOf()) as MutableSet<String>
692+
set(value) = prefs.edit().putStringSet(LOCKED_APPS, value).apply()
693+
689694
/**
690695
* By the number in home app list, get the list item.
691696
* TODO why not just save it as a list?
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.github.droidworksstudio.mlauncher.helper.utils
2+
3+
import androidx.biometric.BiometricManager
4+
import androidx.biometric.BiometricPrompt
5+
import androidx.core.content.ContextCompat
6+
import androidx.fragment.app.Fragment
7+
import com.github.droidworksstudio.mlauncher.R
8+
import com.github.droidworksstudio.mlauncher.data.AppListItem
9+
10+
class BiometricHelper(private val fragment: Fragment) {
11+
private lateinit var callbackApp: CallbackApp
12+
private lateinit var callbackSettings: CallbackSettings
13+
14+
interface CallbackApp {
15+
fun onAuthenticationSucceeded(appListItem: AppListItem)
16+
fun onAuthenticationFailed()
17+
fun onAuthenticationError(errorCode: Int, errorMessage: CharSequence?)
18+
}
19+
20+
interface CallbackSettings {
21+
fun onAuthenticationSucceeded()
22+
fun onAuthenticationFailed()
23+
fun onAuthenticationError(errorCode: Int, errorMessage: CharSequence?)
24+
}
25+
26+
fun startBiometricAuth(appListItem: AppListItem, callbackApp: CallbackApp) {
27+
this.callbackApp = callbackApp
28+
29+
val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
30+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
31+
callbackApp.onAuthenticationSucceeded(appListItem)
32+
}
33+
34+
override fun onAuthenticationFailed() {
35+
callbackApp.onAuthenticationFailed()
36+
}
37+
38+
override fun onAuthenticationError(errorCode: Int, errorMessage: CharSequence) {
39+
callbackApp.onAuthenticationError(errorCode, errorMessage)
40+
}
41+
}
42+
43+
val executor = ContextCompat.getMainExecutor(fragment.requireContext())
44+
val biometricPrompt = BiometricPrompt(fragment, executor, authenticationCallback)
45+
46+
val authenticators =
47+
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
48+
val canAuthenticate =
49+
BiometricManager.from(fragment.requireContext()).canAuthenticate(authenticators)
50+
51+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
52+
.setTitle(fragment.getString(R.string.text_biometric_login))
53+
.setSubtitle(fragment.getString(R.string.text_biometric_login_app, appListItem.activityLabel))
54+
.setAllowedAuthenticators(authenticators)
55+
.setConfirmationRequired(false)
56+
.build()
57+
58+
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
59+
biometricPrompt.authenticate(promptInfo)
60+
}
61+
}
62+
63+
fun startBiometricSettingsAuth(callbackApp: CallbackSettings) {
64+
this.callbackSettings = callbackApp
65+
66+
val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
67+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
68+
callbackSettings.onAuthenticationSucceeded()
69+
}
70+
71+
override fun onAuthenticationFailed() {
72+
callbackSettings.onAuthenticationFailed()
73+
}
74+
75+
override fun onAuthenticationError(errorCode: Int, errorMessage: CharSequence) {
76+
callbackSettings.onAuthenticationError(errorCode, errorMessage)
77+
}
78+
}
79+
80+
val executor = ContextCompat.getMainExecutor(fragment.requireContext())
81+
val biometricPrompt = BiometricPrompt(fragment, executor, authenticationCallback)
82+
83+
val authenticators =
84+
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
85+
val canAuthenticate =
86+
BiometricManager.from(fragment.requireContext()).canAuthenticate(authenticators)
87+
88+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
89+
.setTitle(fragment.getString(R.string.text_biometric_login))
90+
.setSubtitle(fragment.getString(R.string.text_biometric_login_sub))
91+
.setAllowedAuthenticators(authenticators)
92+
.setConfirmationRequired(false)
93+
.build()
94+
95+
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
96+
biometricPrompt.authenticate(promptInfo)
97+
}
98+
}
99+
}

app/src/main/java/com/github/droidworksstudio/mlauncher/helper/utils/PrivateSpaceManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class PrivateSpaceManager(private val context: Context) {
5757
/**
5858
* Check whether the user has created a private space and whether mLauncher can access it.
5959
*/
60-
private fun isPrivateSpaceSetUp(
60+
fun isPrivateSpaceSetUp(
6161
showToast: Boolean = false,
6262
launchSettings: Boolean = false
6363
): Boolean {

0 commit comments

Comments
 (0)