Skip to content

Commit 2bf361c

Browse files
committed
feat(plugins): dynamic toolbar icons, mic permission, and API dump
Add ToolbarAction.iconProvider and IdeUIService.refreshToolbarActions so plugins can drive state-based animated toolbar icons; declare RECORD_AUDIO for voice plugins. Collapse the duplicate editor renderer overrides into a single GhostTextRenderer that extends TracingEditorRenderer (keeps the ADFA-2468 block-line data-race guard), and regenerate the plugin-api golden file so apiCheck passes.
1 parent 9c7dd37 commit 2bf361c

12 files changed

Lines changed: 122 additions & 10 deletions

File tree

‎app/src/main/AndroidManifest.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
<uses-permission android:name="android.permission.INTERNET" />
2727
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
2828
<uses-permission android:name="android.permission.CAMERA" />
29+
<!-- Required so voice/speech plugins (e.g. speech-to-text) can record audio. Plugins run
30+
under this app's UID, so the runtime permission must be declared here — a plugin
31+
manifest's uses-permission is not merged into the host APK. -->
32+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
2933
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3034
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
3135
<uses-permission

‎app/src/main/java/com/itsaky/androidide/actions/PluginToolbarActionItem.kt‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
2121
* so the toolbar fully removes it (instead of greying it) when not applicable.
2222
*/
2323
class PluginToolbarActionItem(
24-
context: Context,
24+
private val context: Context,
2525
private val toolbarAction: ToolbarAction,
2626
val pluginId: String
2727
) : EditorActivityAction() {
@@ -55,6 +55,13 @@ class PluginToolbarActionItem(
5555
runCatching {
5656
enabled = toolbarAction.isEnabledProvider?.invoke() ?: toolbarAction.isEnabled
5757
visible = toolbarAction.isVisibleProvider?.invoke() ?: toolbarAction.isVisible
58+
// Re-resolve the icon on every rebuild when the plugin drives it dynamically.
59+
// When iconProvider is null the icon stays as resolved in init (static behavior).
60+
if (visible) {
61+
toolbarAction.iconProvider?.invoke()?.let { resId ->
62+
PluginDrawableResolver.resolve(resId, pluginId, context)?.let { icon = it }
63+
}
64+
}
5865
}.onFailure { e ->
5966
// A throwing/disposed plugin must never keep a stale icon on the toolbar.
6067
Log.w("PluginToolbarActionItem", "prepare failed for '${toolbarAction.id}'", e)

‎app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
8080
import com.itsaky.androidide.preferences.internal.EditorPreferences
8181
import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
8282
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
83+
import com.itsaky.androidide.plugins.manager.ui.PluginToolbarHost
8384
import com.itsaky.androidide.plugins.manager.ui.PluginUiActionManager
8485
import com.itsaky.androidide.projects.ProjectManagerImpl
8586
import com.itsaky.androidide.projects.builder.BuildResult
@@ -120,7 +121,8 @@ import com.itsaky.androidide.utils.hasVisibleDialog
120121
*/
121122
open class EditorHandlerActivity :
122123
ProjectHandlerActivity(),
123-
IEditorHandler {
124+
IEditorHandler,
125+
PluginToolbarHost {
124126
private val singleBuildListeners = CopyOnWriteArrayList<Consumer<BuildResult>>()
125127

126128
companion object {
@@ -491,6 +493,15 @@ open class EditorHandlerActivity :
491493
}
492494
}
493495

496+
/**
497+
* [PluginToolbarHost] entry point. Lets plugin services (via IdeUIService) request a
498+
* toolbar rebuild so dynamic [com.itsaky.androidide.plugins.extensions.ToolbarAction]
499+
* providers (icon/enabled/visible) are re-evaluated. Marshalled to the UI thread.
500+
*/
501+
override fun refreshPluginToolbarActions() {
502+
runOnUiThread { prepareOptionsMenu() }
503+
}
504+
494505
fun prepareOptionsMenu() {
495506
val registry = getInstance() as DefaultActionsRegistry
496507
val data = createToolbarActionData()

‎common/src/main/java/com/itsaky/androidide/ui/ProjectActionsToolbar.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class ProjectActionsToolbar @JvmOverloads constructor(
5252
}
5353
contentDescription = hint
5454
setImageDrawable(icon)
55+
// Start animation for animated drawables (e.g. AnimatedVectorDrawable used by a
56+
// plugin's dynamic toolbar icon). No-op for ordinary static drawables.
57+
(icon as? android.graphics.drawable.Animatable)?.start()
5558
addCircleRipple()
5659
// Set layout params for width and height
5760
layoutParams = LinearLayout.LayoutParams(

‎editor/src/main/java/com/itsaky/androidide/editor/ui/GhostTextRenderer.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ import io.github.rosemoe.sora.widget.EditorRenderer
2727
* at a fixed anchor position, after the editor's normal content. This backs the host side of the
2828
* inline-suggestion plugin pipeline (`IdeEditorService.showInlineSuggestion`).
2929
*
30+
* Extends [TracingEditorRenderer] so it inherits the block-line data-race guards (ADFA-2468) while
31+
* layering ghost text on top of the normal draw pass — the editor only supports a single renderer,
32+
* so the two behaviours have to share one class.
33+
*
3034
* State is mutated from the main thread (via [IDEEditor]); [draw] reads it defensively and must
3135
* never throw — a bad suggestion or transient layout state can never take the editor's draw pass
3236
* down with it.
3337
*/
34-
class GhostTextRenderer(private val editor: CodeEditor) : EditorRenderer(editor) {
38+
class GhostTextRenderer(private val editor: CodeEditor) : TracingEditorRenderer(editor = editor) {
3539

3640
@Volatile
3741
private var suggestion: String? = null

‎editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -524,12 +524,6 @@ constructor(
524524
final override fun <T : EditorBuiltinComponent?> getComponent(clazz: Class<T>): T & Any =
525525
super.getComponent(clazz)
526526

527-
/** Uses [TracingEditorRenderer], which guards block-line drawing against the
528-
* styles.blocks data race that otherwise crashes onDraw. */
529-
override fun onCreateRenderer(): EditorRenderer {
530-
return TracingEditorRenderer(editor = this)
531-
}
532-
533527
override fun release() {
534528
ensureWindowsDismissed()
535529

@@ -976,6 +970,8 @@ constructor(
976970

977971
// --- Inline suggestions (ghost text) ------------------------------------
978972

973+
/** [GhostTextRenderer] extends [TracingEditorRenderer], so this keeps the block-line
974+
* data-race guards while also drawing inline ghost text. */
979975
override fun onCreateRenderer(): EditorRenderer = GhostTextRenderer(this)
980976

981977
private val ghostRenderer: GhostTextRenderer?

‎editor/src/main/java/com/itsaky/androidide/editor/ui/TracingEditorRenderer.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import org.slf4j.LoggerFactory
3939
*
4040
* @author Akash Yadav
4141
*/
42-
class TracingEditorRenderer(
42+
open class TracingEditorRenderer(
4343
private val enabled: Boolean = BuildConfig.DEBUG,
4444
editor: CodeEditor
4545
) : EditorRenderer(editor) {

‎plugin-api/api/plugin-api.api‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,25 @@ public abstract interface class com/itsaky/androidide/plugins/IPlugin {
66
}
77

88
public abstract interface class com/itsaky/androidide/plugins/PluginContext {
9+
public abstract fun addPluginLifecycleListener (Lcom/itsaky/androidide/plugins/PluginLifecycleListener;)V
910
public abstract fun getAndroidContext ()Landroid/content/Context;
1011
public abstract fun getAppFilesDir ()Ljava/io/File;
12+
public abstract fun getAppSharedPreferences (Ljava/lang/String;)Landroid/content/SharedPreferences;
1113
public abstract fun getEventBus ()Ljava/lang/Object;
1214
public abstract fun getLogger ()Lcom/itsaky/androidide/plugins/PluginLogger;
1315
public abstract fun getPluginDataDir ()Ljava/io/File;
1416
public abstract fun getPluginFilesDir ()Ljava/io/File;
1517
public abstract fun getPluginId ()Ljava/lang/String;
18+
public abstract fun getPluginService (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
1619
public abstract fun getPluginSharedPreferences (Ljava/lang/String;)Landroid/content/SharedPreferences;
20+
public abstract fun getPluginVersion (Ljava/lang/String;)Ljava/lang/String;
21+
public abstract fun getProvidedServices ()Ljava/util/List;
1722
public abstract fun getResources ()Lcom/itsaky/androidide/plugins/ResourceManager;
1823
public abstract fun getServices ()Lcom/itsaky/androidide/plugins/ServiceRegistry;
24+
public abstract fun isPluginActive (Ljava/lang/String;)Z
25+
public abstract fun registerService (Ljava/lang/Class;Ljava/lang/Object;)V
26+
public abstract fun removePluginLifecycleListener (Lcom/itsaky/androidide/plugins/PluginLifecycleListener;)V
27+
public abstract fun unregisterService (Ljava/lang/Class;)V
1928
}
2029

2130
public final class com/itsaky/androidide/plugins/PluginInfo {
@@ -36,6 +45,12 @@ public final class com/itsaky/androidide/plugins/PluginInfo {
3645
public fun toString ()Ljava/lang/String;
3746
}
3847

48+
public abstract interface class com/itsaky/androidide/plugins/PluginLifecycleListener {
49+
public abstract fun onPluginActivated (Ljava/lang/String;)V
50+
public abstract fun onPluginDeactivated (Ljava/lang/String;)V
51+
public abstract fun onPluginUninstalled (Ljava/lang/String;)V
52+
}
53+
3954
public abstract interface class com/itsaky/androidide/plugins/PluginLogger {
4055
public abstract fun debug (Ljava/lang/String;)V
4156
public abstract fun debug (Ljava/lang/String;Ljava/lang/Throwable;)V
@@ -1053,6 +1068,7 @@ public final class com/itsaky/androidide/plugins/extensions/ToolbarAction {
10531068
public fun equals (Ljava/lang/Object;)Z
10541069
public final fun getAction ()Lkotlin/jvm/functions/Function0;
10551070
public final fun getIcon ()Ljava/lang/Integer;
1071+
public final fun getIconProvider ()Lkotlin/jvm/functions/Function0;
10561072
public final fun getId ()Ljava/lang/String;
10571073
public final fun getOrder ()I
10581074
public final fun getShowAsAction ()Lcom/itsaky/androidide/plugins/extensions/ShowAsAction;
@@ -1063,6 +1079,7 @@ public final class com/itsaky/androidide/plugins/extensions/ToolbarAction {
10631079
public final fun isVisible ()Z
10641080
public final fun isVisibleProvider ()Lkotlin/jvm/functions/Function0;
10651081
public final fun setEnabledProvider (Lkotlin/jvm/functions/Function0;)V
1082+
public final fun setIconProvider (Lkotlin/jvm/functions/Function0;)V
10661083
public final fun setVisibleProvider (Lkotlin/jvm/functions/Function0;)V
10671084
public fun toString ()Ljava/lang/String;
10681085
}
@@ -1151,6 +1168,10 @@ public final class com/itsaky/androidide/plugins/services/CursorPosition {
11511168
public fun toString ()Ljava/lang/String;
11521169
}
11531170

1171+
public abstract interface class com/itsaky/androidide/plugins/services/EditorContentChangeListener {
1172+
public abstract fun onContentChanged (Ljava/lang/String;IILjava/lang/String;)V
1173+
}
1174+
11541175
public abstract class com/itsaky/androidide/plugins/services/ExtractResult {
11551176
}
11561177

@@ -1224,9 +1245,11 @@ public final class com/itsaky/androidide/plugins/services/IdeCommandService$Defa
12241245
}
12251246

12261247
public abstract interface class com/itsaky/androidide/plugins/services/IdeEditorService {
1248+
public abstract fun addContentChangeListener (Lcom/itsaky/androidide/plugins/services/EditorContentChangeListener;)V
12271249
public abstract fun addFileChangeListener (Lcom/itsaky/androidide/plugins/services/FileChangeListener;)V
12281250
public abstract fun appendToLine (Ljava/io/File;ILjava/lang/String;)Z
12291251
public abstract fun deleteLine (Ljava/io/File;I)Z
1252+
public abstract fun dismissInlineSuggestion ()V
12301253
public abstract fun getCurrentCursorPosition ()Lcom/itsaky/androidide/plugins/services/CursorPosition;
12311254
public abstract fun getCurrentFile ()Ljava/io/File;
12321255
public abstract fun getCurrentFileContent ()Ljava/lang/String;
@@ -1248,11 +1271,20 @@ public abstract interface class com/itsaky/androidide/plugins/services/IdeEditor
12481271
public abstract fun openFile (Ljava/io/File;)Z
12491272
public abstract fun openFileAt (Ljava/io/File;II)Z
12501273
public abstract fun prependToLine (Ljava/io/File;ILjava/lang/String;)Z
1274+
public abstract fun removeContentChangeListener (Lcom/itsaky/androidide/plugins/services/EditorContentChangeListener;)V
12511275
public abstract fun removeFileChangeListener (Lcom/itsaky/androidide/plugins/services/FileChangeListener;)V
12521276
public abstract fun replaceLine (Ljava/io/File;ILjava/lang/String;)Z
12531277
public abstract fun replaceRange (Ljava/io/File;Lcom/itsaky/androidide/plugins/services/SelectionRange;Ljava/lang/String;)Z
12541278
public abstract fun replaceSelection (Ljava/lang/String;)Z
12551279
public abstract fun saveCurrentFile ()Z
1280+
public abstract fun showInlineSuggestion (Ljava/lang/String;)V
1281+
}
1282+
1283+
public final class com/itsaky/androidide/plugins/services/IdeEditorService$DefaultImpls {
1284+
public static fun addContentChangeListener (Lcom/itsaky/androidide/plugins/services/IdeEditorService;Lcom/itsaky/androidide/plugins/services/EditorContentChangeListener;)V
1285+
public static fun dismissInlineSuggestion (Lcom/itsaky/androidide/plugins/services/IdeEditorService;)V
1286+
public static fun removeContentChangeListener (Lcom/itsaky/androidide/plugins/services/IdeEditorService;Lcom/itsaky/androidide/plugins/services/EditorContentChangeListener;)V
1287+
public static fun showInlineSuggestion (Lcom/itsaky/androidide/plugins/services/IdeEditorService;Ljava/lang/String;)V
12561288
}
12571289

12581290
public abstract interface class com/itsaky/androidide/plugins/services/IdeEditorTabService {
@@ -1354,6 +1386,7 @@ public abstract interface class com/itsaky/androidide/plugins/services/IdeUIServ
13541386
public abstract fun getCurrentActivity ()Landroid/app/Activity;
13551387
public abstract fun isUIAvailable ()Z
13561388
public abstract fun openPluginScreen (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z
1389+
public abstract fun refreshToolbarActions ()V
13571390
}
13581391

13591392
public final class com/itsaky/androidide/plugins/services/IdeUIService$Companion {
@@ -1366,6 +1399,7 @@ public final class com/itsaky/androidide/plugins/services/IdeUIService$Companion
13661399
public final class com/itsaky/androidide/plugins/services/IdeUIService$DefaultImpls {
13671400
public static fun openPluginScreen (Lcom/itsaky/androidide/plugins/services/IdeUIService;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z
13681401
public static synthetic fun openPluginScreen$default (Lcom/itsaky/androidide/plugins/services/IdeUIService;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Z
1402+
public static fun refreshToolbarActions (Lcom/itsaky/androidide/plugins/services/IdeUIService;)V
13691403
}
13701404

13711405
public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService {

‎plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ data class ToolbarAction(
159159
* from the toolbar when this resolves to false.
160160
*/
161161
var isVisibleProvider: (() -> Boolean)? = null
162+
163+
/**
164+
* Optional callback to compute the icon dynamically at render time, returning a
165+
* drawable resource id from the plugin's own resources (e.g. `R.drawable.ic_mic`).
166+
* When null, the static [icon] is used — existing plugins keep their fixed icon.
167+
*
168+
* The host re-invokes this each time the toolbar is (re)built, so a plugin can
169+
* change the icon in response to its own state. Ask the host to rebuild the
170+
* toolbar after a state change via
171+
* [com.itsaky.androidide.plugins.services.IdeUIService.refreshToolbarActions].
172+
* If the returned resource is an animated drawable (e.g. `AnimatedVectorDrawable`),
173+
* the host starts it automatically; author it to loop for a continuous animation.
174+
* Returning null (or a failure) falls back to the static [icon].
175+
*/
176+
var iconProvider: (() -> Int?)? = null
162177
}
163178

164179
enum class ShowAsAction {

‎plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeServices.kt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ interface IdeUIService {
217217
title: String? = null
218218
): Boolean = false
219219

220+
/**
221+
* Asks the IDE to rebuild the editor toolbar, re-evaluating each plugin
222+
* [com.itsaky.androidide.plugins.extensions.ToolbarAction]'s dynamic providers
223+
* ([com.itsaky.androidide.plugins.extensions.ToolbarAction.iconProvider],
224+
* `isEnabledProvider`, `isVisibleProvider`). Call this after changing plugin state
225+
* that those providers depend on — e.g. to swap a toolbar icon between
226+
* idle/active/processing states.
227+
*
228+
* Safe to call from any thread; the rebuild is marshalled to the UI thread. A no-op
229+
* when no editor is in the foreground. Default implementation does nothing so older
230+
* hosts remain source/binary compatible.
231+
*/
232+
fun refreshToolbarActions() {}
233+
220234
companion object {
221235
const val ACTION_OPEN_PLUGIN_SCREEN = "com.itsaky.androidide.plugins.OPEN_PLUGIN_SCREEN"
222236
const val EXTRA_PLUGIN_ID = "com.itsaky.androidide.plugins.extra.PLUGIN_ID"

0 commit comments

Comments
 (0)