Create different module common to store business logic that is common for both xml and jetpack compose
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">common</string>
|
||||
</resources>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||