Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ dependencies {
implementation(libs.coroutines.android)

implementation(libs.lifecycle.runtime)
// implementation(libs.lifecycle.viewmodel)
implementation(libs.lifecycle.viewmodel)

implementation(libs.koin)
implementation(libs.koin.viewmodel)

// test
testImplementation(libs.junit)
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".PasscodesApplication"
android:allowBackup="true"
android:icon="${appIcon}"
android:label="${appLabel}"
Expand Down Expand Up @@ -32,6 +33,7 @@
<activity android:name=".ui.SettingsActivity" />
<activity android:name=".ui.AboutUsActivity" />
<activity android:name=".ui.LicenseActivity" />
<activity android:name=".autofill.AutofillSettingsActivity" />

<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
Expand All @@ -41,6 +43,20 @@
android:name="autoStoreLocales"
android:value="true" />
</service>

<service
android:name=".autofill.PasswordAutofillService"
android:label="@string/app_name"
android:permission="android.permission.BIND_AUTOFILL_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.autofill.AutofillService" />
</intent-filter>

<meta-data
android:name="android.autofill"
android:resource="@xml/autofill_service" />
</service>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.jeeldobariya.passcodes

import android.app.Application
import com.jeeldobariya.passcodes.di.appModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class PasscodesApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@PasscodesApplication)
modules(appModule)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.jeeldobariya.passcodes.autofill

import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
import com.jeeldobariya.passcodes.R

class AutofillSettingsActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_autofill_settings)

val enableAutofillButton = findViewById<MaterialButton>(R.id.enable_autofill_button)
enableAutofillButton.setOnClickListener {
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data = android.net.Uri.parse("package:$packageName")
startActivity(intent)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.jeeldobariya.passcodes.autofill

import android.app.assist.AssistStructure
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import com.jeeldobariya.passcodes.R
import com.jeeldobariya.passcodes.data.Passcode
import com.jeeldobariya.passcodes.data.PasscodeDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

class PasswordAutofillService : AutofillService() {

private val serviceScope = CoroutineScope(Dispatchers.IO)

override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback
) {
val context = request.fillContexts
val structure = context.last().structure

val viewNodes = mutableMapOf<String, AssistStructure.ViewNode>()
parseStructure(structure.getWindowNodeAt(0).rootViewNode, viewNodes)

val usernameNode = viewNodes["username"] ?: viewNodes["emailAddress"]
val passwordNode = viewNodes["password"]

if (usernameNode?.autofillId == null || passwordNode?.autofillId == null) {
callback.onSuccess(null)
return
}

val usernameId = usernameNode.autofillId!!
val passwordId = passwordNode.autofillId!!

cancellationSignal.setOnCancelListener {
// Handle cancellation
}

serviceScope.launch {
val passcodes =
PasscodeDatabase.getDatabase(applicationContext).passcodeDao().getAllPasscodes()
.first()
val responseBuilder = FillResponse.Builder()

for (passcode in passcodes) {
val presentation = RemoteViews(packageName, R.layout.autofill_list_item).apply {
setTextViewText(R.id.autofill_username, passcode.name)
}

val dataset = android.service.autofill.Dataset.Builder(presentation)
.setValue(usernameId, AutofillValue.forText(passcode.name))
.setValue(passwordId, AutofillValue.forText(passcode.value))
.build()
responseBuilder.addDataset(dataset)
}

callback.onSuccess(responseBuilder.build())
}
}

override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
val context = request.fillContexts
val structure = context.last().structure

val viewNodes = mutableMapOf<String, AssistStructure.ViewNode>()
parseStructure(structure.getWindowNodeAt(0).rootViewNode, viewNodes)

val usernameNode = viewNodes["username"] ?: viewNodes["emailAddress"]
val passwordNode = viewNodes["password"]

val username = usernameNode?.text?.toString()
val password = passwordNode?.text?.toString()

if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) {
serviceScope.launch {
PasscodeDatabase.getDatabase(applicationContext).passcodeDao().insert(
Passcode(name = username, value = password)
)
}
callback.onSuccess()
} else {
callback.onFailure("Could not save credentials.")
}
}

private fun parseStructure(
node: AssistStructure.ViewNode,
viewNodes: MutableMap<String, AssistStructure.ViewNode>
) {
node.autofillHints?.forEach { hint ->
if (!viewNodes.containsKey(hint)) {
viewNodes[hint] = node
}
}

for (i in 0 until node.childCount) {
parseStructure(node.getChildAt(i), viewNodes)
}
}
}
12 changes: 12 additions & 0 deletions app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.jeeldobariya.passcodes.data

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "passcodes")
data class Passcode(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val value: String
)
28 changes: 28 additions & 0 deletions app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.jeeldobariya.passcodes.data

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface PasscodeDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(passcode: Passcode)

@Update
suspend fun update(passcode: Passcode)

@Delete
suspend fun delete(passcode: Passcode)

@Query("SELECT * FROM passcodes ORDER BY name ASC")
fun getAllPasscodes(): Flow<List<Passcode>>

@Query("SELECT * FROM passcodes WHERE id = :id")
fun getPasscode(id: Int): Flow<Passcode>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jeeldobariya.passcodes.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Passcode::class], version = 1, exportSchema = false)
abstract class PasscodeDatabase : RoomDatabase() {

abstract fun passcodeDao(): PasscodeDao

companion object {
@Volatile
private var INSTANCE: PasscodeDatabase? = null

fun getDatabase(context: Context): PasscodeDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
PasscodeDatabase::class.java,
"passcode_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.jeeldobariya.passcodes.database;
package com.jeeldobariya.passcodes.database

import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(
entities = [Password::class],
Expand All @@ -19,13 +19,13 @@ abstract class MasterDatabase : RoomDatabase() {

fun getDatabase(context: Context): MasterDatabase {
return INSTANCE ?: synchronized(this) {

val instance = Room.databaseBuilder(
context.applicationContext,
MasterDatabase::class.java,
"master"
)
.build()
.build()
INSTANCE = instance
instance
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.jeeldobariya.passcodes.database

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import androidx.room.Delete
import kotlinx.coroutines.flow.Flow

@Dao
Expand Down
34 changes: 34 additions & 0 deletions app/src/main/kotlin/com/jeeldobariya/passcodes/di/appModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.jeeldobariya.passcodes.di

import com.jeeldobariya.passcodes.ui.LoadPasswordViewModel
import com.jeeldobariya.passcodes.ui.SavePasswordViewModel
import com.jeeldobariya.passcodes.ui.UpdatePasswordViewModel
import com.jeeldobariya.passcodes.ui.ViewPasswordViewModel
import com.jeeldobariya.passcodes.utils.Controller
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module

val appModule = module {

single {
Controller(androidContext())
}

viewModel {
UpdatePasswordViewModel(get())
}

viewModel {
SavePasswordViewModel(get())
}

viewModel {
LoadPasswordViewModel(get())
}

viewModel {
ViewPasswordViewModel(get())
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import androidx.core.content.edit
import com.jeeldobariya.passcodes.utils.Constant

class FeatureFlagManager private constructor(context: Context) {
private val prefs = context.getSharedPreferences(Constant.FEATURE_FLAGS_PREFS_NAME, Context.MODE_PRIVATE)
private val prefs =
context.getSharedPreferences(Constant.FEATURE_FLAGS_PREFS_NAME, Context.MODE_PRIVATE)

var latestFeaturesEnabled: Boolean
get() = prefs.getBoolean(Constant.LATEST_FEATURES_KEY, false)
set(value) { prefs.edit { putBoolean(Constant.LATEST_FEATURES_KEY, value) } }
set(value) {
prefs.edit { putBoolean(Constant.LATEST_FEATURES_KEY, value) }
}

companion object {
@Volatile private var INSTANCE: FeatureFlagManager? = null
@Volatile
private var INSTANCE: FeatureFlagManager? = null

fun get(context: Context): FeatureFlagManager {
return INSTANCE ?: synchronized(this) {
Expand Down
Loading