Add API key validation, secure DB key storage, input field padding, scroll fixes
This commit is contained in:
parent
7eadec669c
commit
5d59c5e351
4 changed files with 71 additions and 30 deletions
|
|
@ -4,6 +4,8 @@ import android.content.Context
|
|||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import net.sqlcipher.database.SupportFactory
|
||||
|
||||
@Database(
|
||||
|
|
@ -44,14 +46,25 @@ abstract class ChatDatabase : RoomDatabase() {
|
|||
}
|
||||
|
||||
private fun getOrCreatePassphrase(context: Context): ByteArray {
|
||||
val prefs = context.getSharedPreferences("db_prefs", Context.MODE_PRIVATE)
|
||||
val existingKey = prefs.getString("db_key", null)
|
||||
val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
val securePrefs = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"db_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
val existingKey = securePrefs.getString("db_key", null)
|
||||
|
||||
return if (existingKey != null) {
|
||||
existingKey.toByteArray(Charsets.UTF_8)
|
||||
} else {
|
||||
val newKey = generateSecureKey()
|
||||
prefs.edit().putString("db_key", newKey).apply()
|
||||
securePrefs.edit().putString("db_key", newKey).apply()
|
||||
newKey.toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
private var userMessageCount = 0
|
||||
private var titleGenerationJob: kotlinx.coroutines.Job? = null
|
||||
private var isFirstLoad = true
|
||||
private var userScrolledAfterSend = false
|
||||
private var lastUserMessagePosition = -1
|
||||
private var apiKeyDialog: AlertDialog? = null
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "mistral_chat_prefs"
|
||||
|
|
@ -599,15 +602,24 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
stackFromEnd = true
|
||||
}
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val lastVisible = layoutManager.findLastVisibleItemPosition()
|
||||
val totalItems = layoutManager.itemCount
|
||||
|
||||
if (lastVisible < totalItems - 2) {
|
||||
userScrolledAfterSend = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupInput() {
|
||||
sendButton.setOnClickListener {
|
||||
if (currentJob?.isActive == true) {
|
||||
cancelRequest()
|
||||
} else {
|
||||
sendInput()
|
||||
}
|
||||
sendInput()
|
||||
}
|
||||
|
||||
inputField.setOnEditorActionListener { _, actionId, event ->
|
||||
|
|
@ -624,6 +636,13 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
val userInput = inputField.text?.toString()?.trim()
|
||||
if (userInput.isNullOrEmpty()) return
|
||||
|
||||
val apiKey = getApiKey()
|
||||
if (apiKey.isEmpty() || apiKey.length < 32 || !apiKey.matches(Regex("^[a-zA-Z0-9]+$"))) {
|
||||
Toast.makeText(this, getString(R.string.api_key_required), Toast.LENGTH_SHORT).show()
|
||||
showApiKeyDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (currentSessionId == null) {
|
||||
createNewSessionAndSend(userInput)
|
||||
return
|
||||
|
|
@ -632,6 +651,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
addMessage(Message(content = userInput, isUser = true, senderName = getCurrentProfileName()))
|
||||
|
||||
inputField.text?.clear()
|
||||
|
||||
userScrolledAfterSend = false
|
||||
lastUserMessagePosition = messages.size - 1
|
||||
|
||||
recyclerView.postDelayed({
|
||||
recyclerView.scrollToPosition(lastUserMessagePosition)
|
||||
}, 100)
|
||||
|
||||
sendMessage(userInput)
|
||||
}
|
||||
|
||||
|
|
@ -741,10 +768,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
messages.add(message)
|
||||
adapter.notifyItemInserted(newPosition)
|
||||
|
||||
// Scroll to show new AI response
|
||||
if (!message.isUser) {
|
||||
if (!message.isUser && !userScrolledAfterSend) {
|
||||
recyclerView.postDelayed({
|
||||
recyclerView.scrollToPosition(newPosition)
|
||||
if (!userScrolledAfterSend) {
|
||||
recyclerView.scrollToPosition(newPosition)
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +849,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
val newKey = inputField.text?.toString()?.trim()
|
||||
if (!newKey.isNullOrEmpty()) {
|
||||
if (newKey.length < 32 || !newKey.matches(Regex("^[a-zA-Z0-9]+$"))) {
|
||||
Toast.makeText(this, "Неверный формат API ключа (минимум 32 символа, только a-z, A-Z, 0-9)", Toast.LENGTH_LONG).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
saveApiKey(newKey)
|
||||
client = MistralClient(newKey)
|
||||
apiKeyDialog?.dismiss()
|
||||
Toast.makeText(this, getString(R.string.api_key_saved), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.enter_api_key), Toast.LENGTH_SHORT).show()
|
||||
|
|
@ -829,13 +864,17 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
}
|
||||
.apply {
|
||||
if (hasCustomKey) {
|
||||
setNegativeButton(R.string.cancel, null)
|
||||
setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(R.string.delete) { _, _ ->
|
||||
deleteApiKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
.also { apiKeyDialog = it }
|
||||
.show()
|
||||
}
|
||||
|
||||
|
|
@ -843,7 +882,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
val selectedModel = selectedModelName
|
||||
|
||||
sendButton.isEnabled = false
|
||||
sendButton.setImageResource(R.drawable.ic_stop)
|
||||
progressIndicator.isVisible = true
|
||||
|
||||
currentJob = lifecycleScope.launch {
|
||||
|
|
@ -889,34 +927,21 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||
}
|
||||
|
||||
sendButton.isEnabled = true
|
||||
sendButton.setImageResource(R.drawable.ic_mistral_logo)
|
||||
progressIndicator.isVisible = false
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
if (!isActive) return@launch
|
||||
addMessage(Message(content = "Запрос отменён", isUser = false, senderName = "System"))
|
||||
sendButton.isEnabled = true
|
||||
sendButton.setImageResource(R.drawable.ic_mistral_logo)
|
||||
progressIndicator.isVisible = false
|
||||
} catch (e: Exception) {
|
||||
if (!isActive) return@launch
|
||||
val userFriendlyMessage = getUserFriendlyError(e.message ?: "Unknown error")
|
||||
addMessage(Message(content = userFriendlyMessage, isUser = false, senderName = "Error"))
|
||||
sendButton.isEnabled = true
|
||||
sendButton.setImageResource(R.drawable.ic_mistral_logo)
|
||||
progressIndicator.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelRequest() {
|
||||
currentJob?.cancel()
|
||||
titleGenerationJob?.cancel()
|
||||
client?.cancelRequest()
|
||||
|
||||
sendButton.isEnabled = true
|
||||
sendButton.setImageResource(R.drawable.ic_mistral_logo)
|
||||
progressIndicator.isVisible = false
|
||||
}
|
||||
|
||||
private fun getSelectedProfileContext(): String {
|
||||
if (currentProfileId == null) return ""
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
|
|
@ -125,8 +125,8 @@
|
|||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:maxLines="5"
|
||||
android:minHeight="56dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
|
|
@ -134,6 +134,7 @@
|
|||
android:id="@+id/sendButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_mistral_logo"
|
||||
android:background="@drawable/bg_send_button"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:digits="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
android:maxLength="64"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue