Create different module common to store business logic that is common for both xml and jetpack compose

This commit is contained in:
Yogesh Choudhary Paliyal
2021-11-13 15:17:09 +05:30
parent cfb4c598d9
commit bcae38a22e
80 changed files with 633 additions and 151 deletions
@@ -0,0 +1,24 @@
package com.yogeshpaliyal.common
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.yogeshpaliyal.common", appContext.packageName)
}
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yogeshpaliyal.common">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KeyPass">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,23 @@
package com.yogeshpaliyal.common
import androidx.room.Database
import androidx.room.RoomDatabase
import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.keypass.db.DbDao
/*
* @author Yogesh Paliyal
* yogeshpaliyal.foss@gmail.com
* https://techpaliyal.com
* created on 30-01-2021 20:37
*/
@Database(
entities = [AccountModel::class],
version = 5, exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
// define DAO start
abstract fun getDao(): DbDao
// define DAO end
}
@@ -0,0 +1,11 @@
package com.yogeshpaliyal.common
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
@@ -0,0 +1,11 @@
package com.yogeshpaliyal.keypass.constants
annotation class AccountType() {
companion object {
const val DEFAULT = 1 // used to store password and user information
const val TOTP = 2 // used to store Time base - One time Password
/* const val HOTP = 3
const val MOTP = 4
const val STEAM = 5*/
}
}
@@ -0,0 +1,5 @@
package com.yogeshpaliyal.keypass.constants
object IntentKeys {
const val SCANNED_TEXT = "scanned_text"
}
@@ -0,0 +1,5 @@
package com.yogeshpaliyal.keypass.constants
object RequestCodes {
const val SCANNER = 342
}
@@ -0,0 +1,68 @@
package com.yogeshpaliyal.common.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import com.yogeshpaliyal.common.utils.TOTPHelper
import com.yogeshpaliyal.common.utils.getRandomString
import com.yogeshpaliyal.keypass.constants.AccountType
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 30-01-2021 20:38
*/
@Entity(tableName = "account")
open class AccountModel(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
@SerializedName("id")
var id: Long? = null,
@ColumnInfo(name = "title")
@SerializedName("title")
var title: String? = null,
@ColumnInfo(name = "unique_id")
@SerializedName("unique_id")
var uniqueId: String? = getRandomString(),
@ColumnInfo(name = "username")
@SerializedName("username")
var username: String? = null,
@ColumnInfo(name = "password")
@SerializedName("password")
var password: String? = null, // TOTP secret when type is TOTP
@ColumnInfo(name = "site")
@SerializedName("site")
var site: String? = null,
@ColumnInfo(name = "notes")
@SerializedName("notes")
var notes: String? = null,
@ColumnInfo(name = "tags")
@SerializedName("tags")
var tags: String? = null,
@AccountType
@ColumnInfo(name = "type")
@SerializedName("type")
var type: Int? = AccountType.DEFAULT
) {
fun getInitials() = (
title?.firstOrNull() ?: username?.firstOrNull() ?: site?.firstOrNull()
?: notes?.firstOrNull() ?: 'K'
).toString()
fun getOtp() = TOTPHelper.generate(password)
fun getTOtpProgress() = TOTPHelper.getProgress().toInt()
}
@@ -0,0 +1,19 @@
package com.yogeshpaliyal.common.data
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 23-02-2021 20:48
*/
data class BackupData(
@SerializedName("version")
@Expose
val version: Int,
@SerializedName("data")
@Expose
val data: List<AccountModel>
)
@@ -0,0 +1,12 @@
package com.yogeshpaliyal.common.data
import androidx.recyclerview.widget.DiffUtil
/**
* Alias to represent a folder (a String title) into which emails can be placed.
*/
object StringDiffUtil : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
}
@@ -0,0 +1,50 @@
package com.yogeshpaliyal.keypass.db
import androidx.lifecycle.LiveData
import androidx.room.*
import com.yogeshpaliyal.common.data.AccountModel
import kotlinx.coroutines.flow.Flow
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 30-01-2021 21:43
*/
@Dao
abstract class DbDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insertOrUpdateAccount(vararg accountModel: AccountModel)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insertOrUpdateAccount(accountModel: List<AccountModel>)
@Query("SELECT * FROM account ORDER BY title ASC")
abstract fun getAllAccounts(): LiveData<List<AccountModel>>
@Query("SELECT * FROM account ORDER BY title ASC")
abstract suspend fun getAllAccountsList(): List<AccountModel>
@Query("SELECT * FROM account WHERE CASE WHEN :tag IS NOT NULL THEN tags = :tag ELSE 1 END AND ((username LIKE '%'||:query||'%' ) OR (title LIKE '%'||:query||'%' ) OR (notes LIKE '%'||:query||'%' )) ORDER BY title ASC")
abstract fun getAllAccounts(query: String?, tag: String?): LiveData<List<AccountModel>>
@Query("SELECT * FROM account WHERE id = :id")
abstract suspend fun getAccount(id: Long?): AccountModel?
@Query("SELECT * FROM account WHERE unique_id = :uniqueId")
abstract suspend fun getAccount(uniqueId: String?): AccountModel?
@Query("SELECT DISTINCT tags FROM account")
abstract fun getTags(): Flow<List<String>>
@Query("DELETE from account WHERE id = :id")
abstract suspend fun deleteAccount(id: Long?)
@Query("DELETE from account WHERE unique_id = :uniqueId")
abstract suspend fun deleteAccount(uniqueId: String?)
@Delete
abstract suspend fun deleteAccount(accountModel: AccountModel)
}
@@ -0,0 +1,13 @@
package com.yogeshpaliyal.keypass.db_helper
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 07-02-2021 18:52
*/
class CryptoException : Exception {
constructor() {}
constructor(message: String?, throwable: Throwable?) : super(message, throwable) {}
}
@@ -0,0 +1,73 @@
package com.yogeshpaliyal.common.db_helper
import android.content.ContentResolver
import android.net.Uri
import androidx.room.withTransaction
import com.google.gson.Gson
import com.yogeshpaliyal.common.AppDatabase
import com.yogeshpaliyal.common.data.BackupData
import com.yogeshpaliyal.common.utils.getRandomString
import com.yogeshpaliyal.keypass.constants.AccountType
import com.yogeshpaliyal.keypass.db_helper.EncryptionHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 20-02-2021 19:31
*/
suspend fun AppDatabase.createBackup(
key: String,
contentResolver: ContentResolver,
fileUri: Uri?
) =
withContext(Dispatchers.IO) {
fileUri ?: return@withContext false
val data = getDao().getAllAccountsList()
val json =
Gson().toJson(BackupData(this@createBackup.openHelper.readableDatabase.version, data))
val fileStream = contentResolver.openOutputStream(fileUri)
EncryptionHelper.doCryptoEncrypt(key, json, fileStream)
return@withContext true
}
suspend fun AppDatabase.restoreBackup(
key: String,
contentResolver: ContentResolver,
fileUri: Uri?
) = withContext(Dispatchers.IO) {
fileUri ?: return@withContext false
val restoredFile = try {
EncryptionHelper.doCryptoDecrypt(key, contentResolver.openInputStream(fileUri))
} catch (e: Exception) {
e.printStackTrace()
return@withContext false
}
return@withContext Gson().fromJson(restoredFile, BackupData::class.java)?.let { data ->
if (data.version == 3) {
for (datum in data.data) {
getRandomString().also { datum.uniqueId = it }
}
}
if (data.version < 5) {
for (datum in data.data) {
datum.type = AccountType.DEFAULT
}
}
data.data.forEach {
it.id = null
}
withTransaction {
getDao().insertOrUpdateAccount(data.data)
}
true
} ?: false
}
@@ -0,0 +1,148 @@
package com.yogeshpaliyal.keypass.db_helper
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UnsupportedEncodingException
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.Key
import java.security.NoSuchAlgorithmException
import java.security.spec.InvalidKeySpecException
import java.security.spec.InvalidParameterSpecException
import javax.crypto.*
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* https://yogeshpaliyal.com
* created on 07-02-2021 18:50
*/
object EncryptionHelper {
private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
// private const val TRANSFORMATION = "AES"
// private const val TRANSFORMATION = "DES/CBC/PKCS5Padding"
private val iV = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
@Throws(CryptoException::class)
fun doCryptoEncrypt(
key: String,
data: String,
outputFile: OutputStream?
) {
try {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iV))
data.byteInputStream().use {
val inputStream = it
outputFile?.use {
val outputStream = it
CipherOutputStream(outputStream, cipher).use {
inputStream.copyTo(it, 4096)
}
}
}
} catch (ex: NoSuchPaddingException) {
// Log.d("TestingEnc","NoSuchPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: NoSuchAlgorithmException) {
// Log.d("TestingEnc","NoSuchAlgorithmException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: InvalidKeyException) {
// Log.d("TestingEnc","InvalidKeyException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: BadPaddingException) {
// Log.d("TestingEnc","BadPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IllegalBlockSizeException) {
// Log.d("TestingEnc","IllegalBlockSizeException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IOException) {
// Log.d("TestingEnc","IOException")
throw CryptoException("Error encrypting/decrypting file", ex)
}
}
@Throws(CryptoException::class)
fun doCryptoDecrypt(
key: String,
inputFile: InputStream?
): String {
var data = ""
try {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iV))
inputFile.use {
val inputStream = it
CipherInputStream(inputStream, cipher).use {
data = String(it.readBytes())
}
}
} catch (ex: NoSuchPaddingException) {
// Log.d("TestingEnc","NoSuchPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: NoSuchAlgorithmException) {
// Log.d("TestingEnc","NoSuchAlgorithmException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: InvalidKeyException) {
// Log.d("TestingEnc","InvalidKeyException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: BadPaddingException) {
// Log.d("TestingEnc","BadPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IllegalBlockSizeException) {
// Log.d("TestingEnc","IllegalBlockSizeException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IOException) {
// Log.d("TestingEnc","IOException")
throw CryptoException("Error encrypting/decrypting file", ex)
}
return data
}
fun encryptPassword(message: String, password: String): String {
/* Encrypt the message. */
var cipher: Cipher? = null
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, generateKey(password))
return String(cipher.doFinal(message.encodeToByteArray()))
}
@Throws(
NoSuchPaddingException::class,
NoSuchAlgorithmException::class,
InvalidParameterSpecException::class,
InvalidAlgorithmParameterException::class,
InvalidKeyException::class,
BadPaddingException::class,
IllegalBlockSizeException::class,
UnsupportedEncodingException::class
)
fun decryptMsg(encryptedMessage: String, password: String): String {
/* Decrypt the message, given derived encContentValues and initialization vector. */
var cipher: Cipher? = null
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, generateKey(password))
return String(cipher.doFinal(encryptedMessage.toByteArray()))
}
@Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
fun generateKey(passLockKey: String): SecretKey {
return SecretKeySpec(passLockKey.encodeToByteArray(), "AES")
}
}
@@ -0,0 +1,54 @@
package com.yogeshpaliyal.common.di.module
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.yogeshpaliyal.common.AppDatabase
import com.yogeshpaliyal.common.R
import com.yogeshpaliyal.common.utils.MySharedPreferences
import com.yogeshpaliyal.common.utils.getRandomString
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun getDb(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.app_name)
).addMigrations(object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `account` ADD COLUMN `unique_id` TEXT")
database.query("select id,unique_id from `account` where unique_id IS NULL")
?.use {
while (it.moveToNext()) {
val id = it.getInt(0)
database.execSQL("update `account` set `unique_id` = '${getRandomString()}' where `id` = '$id'")
}
}
}
}).addMigrations(object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `account` ADD COLUMN `type` INT DEFAULT 0")
}
})
.build()
}
@Provides
@Singleton
fun getSharedPre(@ApplicationContext context: Context): SharedPreferences {
return MySharedPreferences(context).sharedPref
}
}
@@ -0,0 +1,135 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yogeshpaliyal.common.utils
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import com.google.android.material.animation.ArgbEvaluatorCompat
import kotlin.math.roundToInt
/**
* Linearly interpolate between two values
*/
fun lerp(
startValue: Float,
endValue: Float,
@FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) fraction: Float
): Float {
return startValue + fraction * (endValue - startValue)
}
/**
* Linearly interpolate between two values
*/
fun lerp(
startValue: Int,
endValue: Int,
@FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) fraction: Float
): Int {
return (startValue + fraction * (endValue - startValue)).roundToInt()
}
/**
* Linearly interpolate between two values when the fraction is in a given range.
*/
fun lerp(
startValue: Float,
endValue: Float,
@FloatRange(
from = 0.0,
fromInclusive = true,
to = 1.0,
toInclusive = false
) startFraction: Float,
@FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = true) endFraction: Float,
@FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) fraction: Float
): Float {
if (fraction < startFraction) return startValue
if (fraction > endFraction) return endValue
return lerp(startValue, endValue, (fraction - startFraction) / (endFraction - startFraction))
}
/**
* Linearly interpolate between two values when the fraction is in a given range.
*/
fun lerp(
startValue: Int,
endValue: Int,
@FloatRange(
from = 0.0,
fromInclusive = true,
to = 1.0,
toInclusive = false
) startFraction: Float,
@FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = true) endFraction: Float,
@FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) fraction: Float
): Int {
if (fraction < startFraction) return startValue
if (fraction > endFraction) return endValue
return lerp(startValue, endValue, (fraction - startFraction) / (endFraction - startFraction))
}
/**
* Linearly interpolate between two colors when the fraction is in a given range.
*/
@ColorInt
fun lerpArgb(
@ColorInt startColor: Int,
@ColorInt endColor: Int,
@FloatRange(
from = 0.0,
fromInclusive = true,
to = 1.0,
toInclusive = false
) startFraction: Float,
@FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = true) endFraction: Float,
@FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) fraction: Float
): Int {
if (fraction < startFraction) return startColor
if (fraction > endFraction) return endColor
return ArgbEvaluatorCompat.getInstance().evaluate(
(fraction - startFraction) / (endFraction - startFraction),
startColor,
endColor
)
}
/**
* Coerce the receiving Float between inputMin and inputMax and linearly interpolate to the
* outputMin to outputMax scale. This function is able to handle ranges which span negative and
* positive numbers.
*
* This differs from [lerp] as the input values are not required to be between 0 and 1.
*/
fun Float.normalize(
inputMin: Float,
inputMax: Float,
outputMin: Float,
outputMax: Float
): Float {
if (this < inputMin) {
return outputMin
} else if (this > inputMax) {
return outputMax
}
return outputMin * (1 - (this - inputMin) / (inputMax - inputMin)) +
outputMax * ((this - inputMin) / (inputMax - inputMin))
}
@@ -0,0 +1,18 @@
package com.yogeshpaliyal.common.utils
import android.content.Context
import android.view.autofill.AutofillManager
import androidx.core.content.ContextCompat.getSystemService
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 31-01-2021 15:27
*/
fun Context?.getAutoFillService() = if (this != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
getSystemService(this, AutofillManager::class.java)
} else {
null
}
@@ -0,0 +1,83 @@
package com.yogeshpaliyal.common.utils
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.text.TextUtils
import androidx.documentfile.provider.DocumentFile
import com.yogeshpaliyal.common.AppDatabase
import com.yogeshpaliyal.common.db_helper.createBackup
import java.security.SecureRandom
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 20-02-2021 22:10
*/
fun getRandomString(sizeOfRandomString: Int): String {
val ALLOWED_CHARACTERS =
"0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM!@#$%&*_+"
val random = SecureRandom()
val sb = StringBuilder(sizeOfRandomString)
for (i in 0 until sizeOfRandomString) sb.append(
ALLOWED_CHARACTERS[
random.nextInt(
ALLOWED_CHARACTERS.length
)
]
)
return sb.toString()
}
fun Context?.canUserAccessBackupDirectory(sp: SharedPreferences): Boolean {
this ?: return false
val backupDirectoryUri = getUri(getBackupDirectory(sp)) ?: return false
val backupDirectory = DocumentFile.fromTreeUri(this, backupDirectoryUri)
return backupDirectory != null && backupDirectory.exists() && backupDirectory.canRead() && backupDirectory.canWrite()
}
/**
* @return Pair (Boolean to check if backup is for first time, is backup is for first time show user alert to save encryption key)
* Second Value contains the encryption key
*/
suspend fun Context?.backupAccounts(
sp: SharedPreferences,
appDb: AppDatabase,
selectedDirectory: Uri,
fileName: String? = null
): Pair<Boolean, String>? {
this ?: return null
val keyPair = getOrCreateBackupKey(sp)
val fileName = (fileName ?: "key_pass_backup_${System.currentTimeMillis()}") + ".keypass"
val directory = DocumentFile.fromTreeUri(this, selectedDirectory)
var docFile = directory?.findFile(fileName)
if (docFile == null)
docFile = DocumentFile.fromTreeUri(this, selectedDirectory)?.createFile(
"*/*",
fileName
)
val response = appDb.createBackup(
keyPair.second,
contentResolver,
docFile?.uri
)
setBackupTime(sp, System.currentTimeMillis())
return keyPair
}
private fun getUri(string: String?): Uri? {
val uri = string
return if (TextUtils.isEmpty(uri)) {
null
} else {
Uri.parse(uri)
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yogeshpaliyal.common.utils
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.animation.AnimationUtils
import android.view.animation.Interpolator
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StyleRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.res.use
/**
* Retrieve a color from the current [android.content.res.Resources.Theme].
*/
@ColorInt
@SuppressLint("Recycle")
fun Context.themeColor(
@AttrRes themeAttrId: Int
): Int {
return obtainStyledAttributes(
intArrayOf(themeAttrId)
).use {
it.getColor(0, Color.MAGENTA)
}
}
/**
* Retrieve a style from the current [android.content.res.Resources.Theme].
*/
@StyleRes
fun Context.themeStyle(@AttrRes attr: Int): Int {
val tv = TypedValue()
theme.resolveAttribute(attr, tv, true)
return tv.data
}
@SuppressLint("Recycle")
fun Context.themeInterpolator(@AttrRes attr: Int): Interpolator {
return AnimationUtils.loadInterpolator(
this,
obtainStyledAttributes(intArrayOf(attr)).use {
it.getResourceId(0, android.R.interpolator.fast_out_slow_in)
}
)
}
fun Context.getDrawableOrNull(@DrawableRes id: Int?): Drawable? {
return if (id == null || id == 0) null else AppCompatResources.getDrawable(this, id)
}
@@ -0,0 +1,18 @@
package com.yogeshpaliyal.common.utils
import java.text.SimpleDateFormat
import java.util.*
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 23-03-2021 22:30
*/
fun Long.formatCalendar(dateTimeFormat: String?): String? {
val calendar: Calendar = Calendar.getInstance()
calendar.timeInMillis = this
val simpleDateFormat = SimpleDateFormat(dateTimeFormat, Locale.US)
return simpleDateFormat.format(calendar.getTime())
}
@@ -0,0 +1,27 @@
package com.yogeshpaliyal.common.utils
/*Used as a wrapper for data that is exposed via a LiveData that represents an
event.*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
@@ -0,0 +1,82 @@
package com.yogeshpaliyal.common.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 26-12-2020 19:46
*/
@JvmName("IntentHelper")
fun Context.email(
chooserTitle: String,
email: String = "",
subject: String = "",
text: String = ""
) {
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:")
if (email.isNotEmpty())
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
if (subject.isNotEmpty())
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
if (text.isNotEmpty())
intent.putExtra(Intent.EXTRA_TEXT, text)
startActivity(Intent.createChooser(intent, chooserTitle))
}
fun Context.makeCall(chooserTitle: String, number: String): Boolean {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("tel:$number")
startActivity(Intent.createChooser(intent, chooserTitle))
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
fun Context.sendSMS(chooserTitle: String, number: String, text: String = ""): Boolean {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("sms:$number"))
intent.putExtra("sms_body", text)
startActivity(Intent.createChooser(intent, chooserTitle))
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
fun Context.share(chooserTitle: String, text: String): Boolean {
try {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.type = "text/plain"
startActivity(Intent.createChooser(intent, chooserTitle))
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
fun Context.navigate(address: String) {
val gmmIntentUri =
Uri.parse("google.navigation:q=$address")
val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri)
mapIntent.setPackage("com.google.android.apps.maps")
mapIntent.resolveActivity(packageManager)?.let {
startActivity(mapIntent)
}
}
@@ -0,0 +1,36 @@
package com.yogeshpaliyal.common.utils
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class MySharedPreferences(context: Context) {
private val masterKeyAlias = MasterKey.Builder(context).also {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
// this is equivalent to using deprecated MasterKeys.AES256_GCM_SPEC
// this is equivalent to using deprecated MasterKeys.AES256_GCM_SPEC
val spec = KeyGenParameterSpec.Builder(
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
it.setKeyGenParameterSpec(spec)
}
// it.setUserAuthenticationRequired(true)
}
.build()
val sharedPref = EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
@@ -0,0 +1,42 @@
package com.yogeshpaliyal.common.utils
class PasswordGenerator(
private var length: Int,
private var includeUpperCaseLetters: Boolean,
private var includeLowerCaseLetters: Boolean,
private var includeSymbols: Boolean,
private var includeNumbers: Boolean
) {
constructor() : this(10, true, true, true, true)
private val UPPER_CASE = 0
private val LOWER_CASE = 1
private val NUMBERS = 2
private val SYMBOLS = 3
public fun generatePassword(): String {
var password = ""
val list = ArrayList<Int>()
if (includeUpperCaseLetters)
list.add(UPPER_CASE)
if (includeLowerCaseLetters)
list.add(LOWER_CASE)
if (includeNumbers)
list.add(NUMBERS)
if (includeSymbols)
list.add(SYMBOLS)
for (i in 1..length) {
if (list.isNotEmpty()) {
when (list.random()) {
UPPER_CASE -> password += ('A'..'Z').random().toString()
LOWER_CASE -> password += ('a'..'z').random().toString()
NUMBERS -> password += ('0'..'9').random().toString()
SYMBOLS -> password += listOf('!', '@', '#', '$', '%', '&', '*', '+', '=', '-', '~', '?', '/', '_').random().toString()
}
}
}
return password
}
}
@@ -0,0 +1,82 @@
package com.yogeshpaliyal.common.utils
import android.content.SharedPreferences
import androidx.core.content.edit
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 21-02-2021 11:18
*/
/**
* Pair
* 1st => true if key is created now & false if key is created previously
*
*/
fun getOrCreateBackupKey(sp: SharedPreferences, reset: Boolean = false): Pair<Boolean, String> {
return if (sp.contains(BACKUP_KEY) && reset.not()) {
Pair(false, sp.getString(BACKUP_KEY, "") ?: "")
} else {
val randomKey = getRandomString(16)
sp.edit {
putString(BACKUP_KEY, randomKey)
}
Pair(true, randomKey)
}
}
fun clearBackupKey(sp: SharedPreferences) {
sp.edit {
remove(BACKUP_KEY)
}
}
fun setBackupDirectory(sp: SharedPreferences, string: String) {
sp.edit {
putString(BACKUP_DIRECTORY, string)
}
}
fun setBackupTime(sp: SharedPreferences, time: Long) {
sp.edit {
putLong(BACKUP_DATE_TIME, time)
}
}
fun getBackupDirectory(sp: SharedPreferences,): String {
return sp.getString(BACKUP_DIRECTORY, "") ?: ""
}
fun SharedPreferences?.isAutoBackupEnabled(): Boolean {
return this?.getBoolean(AUTO_BACKUP, false) ?: false
}
fun SharedPreferences?.overrideAutoBackup(): Boolean {
return this?.getBoolean(OVERRIDE_AUTO_BACKUP, false) ?: false
}
fun SharedPreferences?.setOverrideAutoBackup(value: Boolean) {
this?.edit {
putBoolean(OVERRIDE_AUTO_BACKUP, value)
}
}
fun SharedPreferences?.setAutoBackupEnabled(value: Boolean) {
this?.edit {
putBoolean(AUTO_BACKUP, value)
}
}
fun getBackupTime(sp: SharedPreferences,): Long {
return sp.getLong(BACKUP_DATE_TIME, -1) ?: -1L
}
private const val BACKUP_KEY = "backup_key"
private const val BACKUP_DIRECTORY = "backup_directory"
private const val BACKUP_DATE_TIME = "backup_date_time"
private const val AUTO_BACKUP = "auto_backup"
private const val OVERRIDE_AUTO_BACKUP = "override_auto_backup"
@@ -0,0 +1,12 @@
package com.yogeshpaliyal.common.utils
import java.util.*
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 22-01-2021 23:14
*/
fun getRandomString() = UUID.randomUUID().toString()
@@ -0,0 +1,65 @@
package com.yogeshpaliyal.common.utils;
import android.net.Uri;
import org.apache.commons.codec.binary.Base32;
import java.net.URL;
import java.util.Locale;
public class TOTPHelper {
public static final String SHA1 = "HmacSHA1";
public static String generate(String secret) {
return String.format(Locale.getDefault(), "%06d", generate(new Base32().decode(secret.toUpperCase()), System.currentTimeMillis() / 1000, 6));
}
public static long getProgress() {
return 30 - ((System.currentTimeMillis() / 1000) % 30);
}
public static String getSecretKey(String contents) throws Exception {
contents = contents.replaceFirst("otpauth", "http");
Uri uri = Uri.parse(contents);
URL url = new URL(contents);
if (!url.getProtocol().equals("http")) {
throw new Exception("Invalid Protocol");
}
if (!url.getHost().equals("totp")) {
throw new Exception("unknown otp type");
}
String secret = uri.getQueryParameter("secret");
if (secret == null)
throw new Exception("Empty secret");
return secret;
}
public static int generate(byte[] key, long t, int digits) {
return TokenCalculator.TOTP_RFC6238(key, 30, t, digits, TokenCalculator.DEFAULT_ALGORITHM, 0);
}
/**
* Returns the label with issuer prefix removed (if present)
*
* @param issuer - Name of the issuer to remove from the label
* @param label - Full label from which the issuer should be removed
* @return - label with the issuer removed
*/
private static String getStrippedLabel(String issuer, String label) {
if (issuer == null || issuer.isEmpty() || !label.startsWith(issuer + ":")) {
return label.trim();
} else {
return label.substring(issuer.length() + 1).trim();
}
}
}
@@ -0,0 +1,123 @@
package com.yogeshpaliyal.common.utils;
import org.apache.commons.codec.binary.Hex;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class TokenCalculator {
public static final int TOTP_DEFAULT_PERIOD = 30;
public static final int TOTP_DEFAULT_DIGITS = 6;
public static final int HOTP_INITIAL_COUNTER = 1;
public static final int STEAM_DEFAULT_DIGITS = 5;
private static final char[] STEAMCHARS = new char[]{
'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
'R', 'T', 'V', 'W', 'X', 'Y'
};
public enum HashAlgorithm {
SHA1, SHA256, SHA512
}
public static final HashAlgorithm DEFAULT_ALGORITHM = HashAlgorithm.SHA1;
private static byte[] generateHash(HashAlgorithm algorithm, byte[] key, byte[] data)
throws NoSuchAlgorithmException, InvalidKeyException {
String algo = "Hmac" + algorithm.toString();
Mac mac = Mac.getInstance(algo);
mac.init(new SecretKeySpec(key, algo));
return mac.doFinal(data);
}
// TODO: Rewrite tests so this compatibility wrapper can be removed
public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits, HashAlgorithm algorithm) {
return TOTP_RFC6238(secret, period, time, digits, algorithm, 0);
}
public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits, HashAlgorithm algorithm, int offset) {
int fullToken = TOTP(secret, period, time, algorithm, offset);
int div = (int) Math.pow(10, digits);
return fullToken % div;
}
public static String TOTP_RFC6238(byte[] secret, int period, int digits, HashAlgorithm algorithm, int offset) {
return Tools.formatTokenString(TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits, algorithm, offset), digits);
}
public static String TOTP_Steam(byte[] secret, int period, int digits, HashAlgorithm algorithm, int offset) {
int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000, algorithm, offset);
StringBuilder tokenBuilder = new StringBuilder();
for (int i = 0; i < digits; i++) {
tokenBuilder.append(STEAMCHARS[fullToken % STEAMCHARS.length]);
fullToken /= STEAMCHARS.length;
}
return tokenBuilder.toString();
}
public static String HOTP(byte[] secret, long counter, int digits, HashAlgorithm algorithm) {
int fullToken = HOTP(secret, counter, algorithm);
int div = (int) Math.pow(10, digits);
return Tools.formatTokenString(fullToken % div, digits);
}
private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm, int offset) {
return HOTP(key, (time / period) + offset, algorithm);
}
private static int HOTP(byte[] key, long counter, HashAlgorithm algorithm) {
int r = 0;
try {
byte[] data = ByteBuffer.allocate(8).putLong(counter).array();
byte[] hash = generateHash(algorithm, key, data);
int offset = hash[hash.length - 1] & 0xF;
int binary = (hash[offset] & 0x7F) << 0x18;
binary |= (hash[offset + 1] & 0xFF) << 0x10;
binary |= (hash[offset + 2] & 0xFF) << 0x08;
binary |= (hash[offset + 3] & 0xFF);
r = binary;
} catch (Exception e) {
e.printStackTrace();
}
return r;
}
public static String MOTP(String PIN, String secret, long epoch, int offset) {
String epochText = String.valueOf((epoch / 10) + offset);
String hashText = epochText + secret + PIN;
String otp = "";
try {
// Create MD5 Hash
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(hashText.getBytes());
byte[] messageDigest = digest.digest();
// Create Hex String
String hexString = new String(Hex.encodeHex(messageDigest));
otp = hexString.substring(0, 6);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return otp;
}
}
@@ -0,0 +1,96 @@
package com.yogeshpaliyal.common.utils;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.os.Environment;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
public class Tools {
/* Checks if external storage is available for read and write */
public static boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state);
}
/* Checks if external storage is available to at least read */
public static boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
}
/* Get a color based on the current theme */
public static int getThemeColor(Context context, int colorAttr) {
Resources.Theme theme = context.getTheme();
TypedArray arr = theme.obtainStyledAttributes(new int[]{colorAttr});
int colorValue = arr.getColor(0, -1);
arr.recycle();
return colorValue;
}
public static int getThemeResource(Context context, int styleAttr) {
Resources.Theme theme = context.getTheme();
TypedArray arr = theme.obtainStyledAttributes(new int[]{styleAttr});
int styleValue = arr.getResourceId(0, -1);
arr.recycle();
return styleValue;
}
/* Create a ColorFilter based on the current theme */
public static ColorFilter getThemeColorFilter(Context context, int colorAttr) {
return new PorterDuffColorFilter(getThemeColor(context, colorAttr), PorterDuff.Mode.SRC_IN);
}
@SuppressWarnings("deprecation")
public static Locale getSystemLocale() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Resources.getSystem().getConfiguration().getLocales().get(0);
} else {
return Resources.getSystem().getConfiguration().locale;
}
}
public static String formatTokenString(int token, int digits) {
NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH);
numberFormat.setMinimumIntegerDigits(digits);
numberFormat.setGroupingUsed(false);
return numberFormat.format(token);
}
public static String formatToken(String s, int chunkSize) {
if (chunkSize == 0 || s == null)
return s;
StringBuilder ret = new StringBuilder();
int index = s.length();
while (index > 0) {
ret.insert(0, s.substring(Math.max(index - chunkSize, 0), index));
ret.insert(0, " ");
index = index - chunkSize;
}
return ret.toString().trim();
}
public static String getDateTimeString() {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH);
Date now = Calendar.getInstance().getTime();
return df.format(now);
}
}
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.KeyPass" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">common</string>
</resources>
+16
View File
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.KeyPass" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,17 @@
package com.yogeshpaliyal.common
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}