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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.method.HideReturnsTransformationMethod
import android.text.method.PasswordTransformationMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.itsaky.androidide.plugins.PluginContext
import com.itsaky.androidide.plugins.aiassistant.AiAssistantPlugin
import com.itsaky.androidide.plugins.aiassistant.R
Expand All @@ -21,6 +24,7 @@ import com.itsaky.androidide.plugins.aiassistant.viewmodel.AiBackend
import com.itsaky.androidide.plugins.aiassistant.viewmodel.AiSettingsViewModel
import com.itsaky.androidide.plugins.aiassistant.viewmodel.EngineState
import com.itsaky.androidide.plugins.aiassistant.viewmodel.ModelLoadingState
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
Expand Down Expand Up @@ -290,6 +294,7 @@ class AiSettingsFragment : DialogFragment() {
private fun setupGeminiApiUi(view: View) {
val apiKeyLayout = view.findViewById<LinearLayout>(R.id.gemini_api_key_layout)
val apiKeyInput = view.findViewById<EditText>(R.id.gemini_api_key_input)
val toggleVisibilityButton = view.findViewById<ImageButton>(R.id.btn_toggle_api_key_visibility)
val saveButton = view.findViewById<Button>(R.id.btn_save_api_key)
val editButton = view.findViewById<Button>(R.id.btn_edit_api_key)
val clearButton = view.findViewById<Button>(R.id.btn_clear_api_key)
Expand Down Expand Up @@ -318,47 +323,93 @@ class AiSettingsFragment : DialogFragment() {
}
}

val savedApiKey = viewModel.getGeminiApiKey()
if (savedApiKey.isNullOrBlank()) {
updateUiState(isEditing = true)
apiKeyInput.setText("")
} else {
updateUiState(isEditing = false)
val timestamp = viewModel.getGeminiApiKeySaveTimestamp()
if (timestamp > 0) {
val sdf = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault())
val savedDate = sdf.format(Date(timestamp))
statusTextView.text = "API Key saved on: $savedDate"
viewLifecycleOwner.lifecycleScope.launch {
val savedApiKey = viewModel.getGeminiApiKey()
val hasKey = !savedApiKey.isNullOrBlank()
updateUiState(isEditing = !hasKey)
if (hasKey) {
statusTextView.text = savedApiKeyStatusText()
} else {
statusTextView.text = "API Key is saved"
apiKeyInput.setText("")
}
}

saveButton.setOnClickListener {
val apiKey = apiKeyInput.text.toString()
if (apiKey.isNotBlank()) {
viewModel.saveGeminiApiKey(apiKey)
Toast.makeText(requireContext(), "API Key saved", Toast.LENGTH_SHORT).show()
toggleVisibilityButton.setColorFilter(apiKeyInput.currentHintTextColor)

updateUiState(isEditing = false)
val timestamp = viewModel.getGeminiApiKeySaveTimestamp()
val sdf = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault())
val savedDate = sdf.format(Date(timestamp))
statusTextView.text = "API Key saved on: $savedDate"
var isKeyVisible = false

fun applyKeyVisibility() {
apiKeyInput.transformationMethod = if (isKeyVisible) {
HideReturnsTransformationMethod.getInstance()
} else {
Toast.makeText(requireContext(), "API Key cannot be empty", Toast.LENGTH_SHORT).show()
PasswordTransformationMethod.getInstance()
}
toggleVisibilityButton.setImageResource(
if (isKeyVisible) R.drawable.ic_visibility_off else R.drawable.ic_visibility
)
toggleVisibilityButton.contentDescription = getString(
if (isKeyVisible) R.string.cd_hide_api_key else R.string.cd_show_api_key
)
toggleVisibilityButton.setColorFilter(apiKeyInput.currentHintTextColor)
apiKeyInput.setSelection(apiKeyInput.text?.length ?: 0)
}

editButton.setOnClickListener {
applyKeyVisibility()

toggleVisibilityButton.setOnClickListener {
isKeyVisible = !isKeyVisible
applyKeyVisibility()
}

saveButton.setOnClickListener {
val apiKey = apiKeyInput.text.toString().trim()
if (apiKey.isBlank()) {
Toast.makeText(requireContext(), getString(R.string.msg_api_key_empty), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
saveButton.isEnabled = false
viewLifecycleOwner.lifecycleScope.launch {
val saved = try {
viewModel.saveGeminiApiKey(apiKey)
} finally {
saveButton.isEnabled = true
}
if (!saved) {
Toast.makeText(requireContext(), getString(R.string.msg_api_key_save_failed), Toast.LENGTH_LONG).show()
return@launch
}
Toast.makeText(requireContext(), getString(R.string.msg_api_key_saved), Toast.LENGTH_SHORT).show()
updateUiState(isEditing = false)
statusTextView.text = savedApiKeyStatusText()
}
}

// Reveal the (already-fetched) key in an editable, focused field. Kept separate from
// the click handler so the listener does one thing: fetch, then hand off.
fun revealEditMode(apiKey: String) {
apiKeyInput.setText(apiKey)
apiKeyInput.setSelection(apiKey.length)
updateUiState(isEditing = true)
apiKeyInput.setText("••••••••••••••••")
isKeyVisible = false
applyKeyVisibility()
apiKeyInput.requestFocus()
}

editButton.setOnClickListener {
editButton.isEnabled = false
viewLifecycleOwner.lifecycleScope.launch {
val apiKey = try {
viewModel.getGeminiApiKey().orEmpty()
} finally {
editButton.isEnabled = true
}
revealEditMode(apiKey)
}
}

clearButton.setOnClickListener {
viewModel.clearGeminiApiKey()
Toast.makeText(requireContext(), "API Key cleared", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), getString(R.string.msg_api_key_cleared), Toast.LENGTH_SHORT).show()
updateUiState(isEditing = true)
apiKeyInput.setText("")
}
Expand All @@ -367,6 +418,14 @@ class AiSettingsFragment : DialogFragment() {
setupGeminiModelSelection(modelContainer)
}

/** Status line for a stored key: dated when the save time is known, generic otherwise. */
private fun savedApiKeyStatusText(): String {
val timestamp = viewModel.getGeminiApiKeySaveTimestamp()
if (timestamp <= 0) return getString(R.string.msg_api_key_is_saved)
val savedDate = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()).format(Date(timestamp))
return getString(R.string.msg_api_key_saved_on, savedDate)
}

private fun createModelSelectionUi(parent: View): LinearLayout {
val context = requireContext()
val container = LinearLayout(context).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.chip.Chip
import com.itsaky.androidide.plugins.PluginContext
import com.itsaky.androidide.plugins.aiassistant.adapters.ChatAdapter
import com.itsaky.androidide.plugins.aiassistant.tool.handlers.PathGuard
import com.itsaky.androidide.plugins.aiassistant.databinding.FragmentChatBinding
import com.itsaky.androidide.plugins.aiassistant.models.AgentState
import com.itsaky.androidide.plugins.aiassistant.viewmodel.ChatViewModel
import com.itsaky.androidide.plugins.services.IdeProjectService
import io.noties.markwon.Markwon
import kotlinx.coroutines.launch
import java.io.File
Expand Down Expand Up @@ -334,9 +336,16 @@ class ChatFragment : Fragment() {
dialog.show(parentFragmentManager, "approval_dialog")
}

/**
* Opens the file picker rooted at the open project. The host's
* [IdeProjectService] is the source of truth for the project root; the
* `System.getProperty` chain in [PathGuard] resolves to "/" at runtime (the
* IDE process cwd), which lists nothing, so it is only a last-resort fallback.
*/
private fun showFilePicker() {
// Start from AndroidIDE projects directory by default
val startPath = "/storage/emulated/0/AndroidIDEProjects"
val projectService = getPluginContext()?.services?.get(IdeProjectService::class.java)
val startPath = projectService?.getCurrentProject()?.rootDir?.absolutePath
?: PathGuard.projectRoot()

val dialog = FilePickerDialogFragment.newInstance(startPath) { files ->
addContextFiles(files)
Expand Down
Loading