Skip to content

Commit 1db7242

Browse files
authored
ADFA-4333: add profiler as an experimental feature (#1482)
1 parent c9d237d commit 1db7242

108 files changed

Lines changed: 8625 additions & 894 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/.gitignore‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Local, personal Claude Code settings — should not be shared
2+
settings.local.json
3+
4+
# Local plan/scratch artifacts, if any
5+
/plans/
6+
7+
# Local git worktrees — must never be committed
8+
/worktrees/

‎.editorconfig‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22

33
# Enable IntelliJ formatter tags to allow enabling/disabling ktlint for a
44
# specific part of code.
5-
ij_formatter_tags_enabled = true
5+
ij_formatter_tags_enabled = true
6+
7+
# @Composable functions are PascalCase by convention, which conflicts with the
8+
# standard:function-naming rule. Let the compose-rules ruleset (wired into
9+
# spotless) validate Composable naming instead.
10+
ktlint_function_naming_ignore_when_annotated_with = Composable

‎app/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ dependencies {
295295
implementation(projects.idetooltips)
296296
implementation(projects.floatingWindow)
297297
implementation(projects.gitCore)
298+
implementation(projects.profiler)
298299

299300
// This is to build the tooling-api-impl project before the app is built
300301
// So we always copy the latest JAR file to assets
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package com.itsaky.androidide.actions
2+
3+
import android.app.AppOpsManager
4+
import android.app.ForegroundServiceStartNotAllowedException
5+
import android.app.NotificationManager
6+
import android.content.ActivityNotFoundException
7+
import android.content.Context
8+
import android.content.Intent
9+
import android.graphics.Color
10+
import android.net.Uri
11+
import android.os.Build
12+
import android.provider.Settings
13+
import android.text.SpannableStringBuilder
14+
import android.text.method.LinkMovementMethod
15+
import androidx.annotation.RequiresApi
16+
import androidx.appcompat.app.AlertDialog
17+
import androidx.core.content.ContextCompat.startForegroundService
18+
import androidx.core.view.setPadding
19+
import com.google.android.material.textview.MaterialTextView
20+
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
21+
import com.itsaky.androidide.activities.editor.HelpActivity
22+
import com.itsaky.androidide.resources.R
23+
import com.itsaky.androidide.utils.DialogUtils
24+
import com.itsaky.androidide.utils.PermissionsHelper
25+
import com.itsaky.androidide.utils.appendHtmlWithLinks
26+
import com.itsaky.androidide.utils.appendOrderedList
27+
import com.itsaky.androidide.utils.flashError
28+
import com.itsaky.androidide.utils.isAtLeastS
29+
import kotlinx.coroutines.Dispatchers
30+
import kotlinx.coroutines.withContext
31+
import moe.shizuku.manager.adb.AdbPairingService
32+
import org.adfa.constants.CONTENT_KEY
33+
import org.slf4j.Logger
34+
35+
@RequiresApi(Build.VERSION_CODES.R)
36+
fun canShowPairingNotification(context: Context): Boolean {
37+
val nm = context.getSystemService(NotificationManager::class.java)
38+
val channel = nm.getNotificationChannel(AdbPairingService.NOTIFICATION_CHANNEL)
39+
return nm.areNotificationsEnabled() &&
40+
(channel == null || channel.importance != NotificationManager.IMPORTANCE_NONE)
41+
}
42+
43+
fun showNotificationPermissionDialog(
44+
context: Context,
45+
onError: (ex: Exception) -> Unit,
46+
): AlertDialog? =
47+
DialogUtils
48+
.newMaterialDialogBuilder(context)
49+
.setTitle(R.string.adb_pairing_action_enable_notifications)
50+
.setMessage(
51+
context.getString(
52+
R.string.adb_pairing_tutorial_content_notification,
53+
context.getString(R.string.notification_channel_adb_pairing),
54+
),
55+
).setPositiveButton(R.string.title_grant) { dialog, _ ->
56+
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
57+
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
58+
try {
59+
context.startActivity(intent)
60+
} catch (e: ActivityNotFoundException) {
61+
onError(e)
62+
}
63+
64+
dialog.dismiss()
65+
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
66+
dialog.dismiss()
67+
}.show()
68+
69+
fun showDebuggerNotReadyMessage(context: Context): AlertDialog? =
70+
DialogUtils
71+
.newMaterialDialogBuilder(context)
72+
.setMessage(
73+
context.getString(R.string.debugger_not_ready) +
74+
System
75+
.lineSeparator()
76+
.repeat(2) + context.getString(R.string.debugger_error_suggestion_network_restriction),
77+
).setPositiveButton(android.R.string.ok, null)
78+
.show()
79+
80+
suspend fun handleMissingOverlayPermission(
81+
activity: EditorHandlerActivity,
82+
state: PermissionsHelper.OverlayPermissionState,
83+
onError: (ex: Exception) -> Unit,
84+
) {
85+
withContext(Dispatchers.Main.immediate) {
86+
when (state) {
87+
PermissionsHelper.OverlayPermissionState.UNSUPPORTED -> {
88+
activity.flashError(activity.getString(R.string.permission_overlay_unsupported_hint))
89+
}
90+
PermissionsHelper.OverlayPermissionState.REQUESTABLE -> {
91+
val intent =
92+
Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
93+
putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName)
94+
setData(Uri.fromParts("package", activity.packageName, null))
95+
}
96+
try {
97+
activity.startActivity(intent)
98+
} catch (e: Exception) {
99+
onError(e)
100+
activity.flashError(
101+
activity.getString(R.string.err_no_activity_to_handle_action, Settings.ACTION_MANAGE_OVERLAY_PERMISSION),
102+
)
103+
}
104+
}
105+
else -> {}
106+
}
107+
}
108+
}
109+
110+
@RequiresApi(Build.VERSION_CODES.R)
111+
fun showPairingDialog(
112+
context: Context,
113+
log: Logger,
114+
onError: (ex: Exception) -> Unit,
115+
): AlertDialog? {
116+
val launchHelp = { url: String ->
117+
context.startActivity(
118+
Intent(context, HelpActivity::class.java).apply {
119+
putExtra(CONTENT_KEY, url)
120+
},
121+
)
122+
}
123+
124+
val ssb = SpannableStringBuilder()
125+
ssb.appendHtmlWithLinks(
126+
context.getString(R.string.debugger_setup_description_header),
127+
launchHelp,
128+
)
129+
130+
ssb.append(System.lineSeparator())
131+
ssb.append(System.lineSeparator())
132+
133+
ssb.appendOrderedList(*context.resources.getStringArray(R.array.debugger_setup_pairing_steps))
134+
ssb.append(System.lineSeparator())
135+
136+
ssb.appendHtmlWithLinks(
137+
context.getString(R.string.debugger_setup_description_footer),
138+
launchHelp,
139+
)
140+
141+
val text = MaterialTextView(context)
142+
text.setPadding(context.resources.getDimensionPixelSize(R.dimen.content_padding_double))
143+
text.movementMethod = LinkMovementMethod.getInstance()
144+
text.highlightColor = Color.TRANSPARENT
145+
text.text = ssb
146+
text.setLineSpacing(text.lineSpacingExtra, 1.1f)
147+
148+
return DialogUtils
149+
.newMaterialDialogBuilder(context)
150+
.setTitle(R.string.debugger_setup_title)
151+
.setView(text)
152+
.setPositiveButton(R.string.adb_pairing_action_start) { dialog, _ ->
153+
dialog.dismiss()
154+
155+
val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
156+
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
157+
intent.putExtra(":settings:fragment_args_key", "toggle_adb_wireless")
158+
try {
159+
if (startPairingService(context, onError = {
160+
log.error("Failed to start pairing service", it)
161+
})
162+
) {
163+
context.startActivity(intent)
164+
}
165+
} catch (e: ActivityNotFoundException) {
166+
onError(e)
167+
}
168+
}.setNegativeButton(android.R.string.cancel, null)
169+
.show()
170+
}
171+
172+
@RequiresApi(Build.VERSION_CODES.R)
173+
fun startPairingService(
174+
context: Context,
175+
onError: (err: Throwable) -> Unit,
176+
): Boolean {
177+
val intent = AdbPairingService.startIntent(context)
178+
return try {
179+
startForegroundService(context, intent)
180+
true
181+
} catch (e: Throwable) {
182+
onError(e)
183+
if (isAtLeastS() && e is ForegroundServiceStartNotAllowedException) {
184+
val mode =
185+
context
186+
.getSystemService(AppOpsManager::class.java)
187+
.noteOpNoThrow(
188+
"android:start_foreground",
189+
android.os.Process.myUid(),
190+
context.packageName,
191+
null,
192+
null,
193+
)
194+
if (mode == AppOpsManager.MODE_ERRORED) {
195+
flashError(context.getString(R.string.err_foreground_service_denial))
196+
}
197+
198+
context.startService(intent)
199+
true
200+
} else {
201+
false
202+
}
203+
}
204+
}

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.annotation.DrawableRes
66
import androidx.annotation.StringRes
77
import com.itsaky.androidide.actions.ActionData
88
import com.itsaky.androidide.actions.openApplicationModuleChooser
9+
import com.itsaky.androidide.actions.profiler.ProfilerAction
910
import com.itsaky.androidide.project.AndroidModels
1011
import com.itsaky.androidide.projects.IProjectManager
1112
import com.itsaky.androidide.projects.api.AndroidModule
@@ -23,6 +24,25 @@ abstract class AbstractModuleAssemblerAction(
2324
@StringRes private val labelRes: Int,
2425
@DrawableRes private val iconRes: Int,
2526
) : AbstractCancellableRunAction(context, labelRes, iconRes) {
27+
/**
28+
* Extra Gradle arguments (e.g. `-P` properties) to pass for this action's build. Subclasses
29+
* override this to influence the build; for example, the profiler action enables a profileable APK.
30+
*/
31+
protected open val gradleArgs: List<String>
32+
get() = emptyList()
33+
34+
/**
35+
* Resolves the variant that should actually be built for this action, given the user's
36+
* [selectedVariant]. The default returns [selectedVariant] unchanged. Subclasses may override
37+
* to build a different variant (e.g. the profiler builds the release counterpart). Returning
38+
* `null` aborts the build; an overriding implementation must surface its own error first.
39+
*/
40+
protected open fun resolveBuildVariant(
41+
data: ActionData,
42+
module: AndroidModule,
43+
selectedVariant: AndroidModels.AndroidVariant,
44+
): AndroidModels.AndroidVariant? = selectedVariant
45+
2646
override fun doExec(data: ActionData): Boolean {
2747
val projectManager = IProjectManager.getInstance()
2848

@@ -61,10 +81,17 @@ abstract class AbstractModuleAssemblerAction(
6181
variant: AndroidModels.AndroidVariant,
6282
) {
6383
val activity = data.requireActivity()
84+
val resolvedVariant = resolveBuildVariant(data, module, variant) ?: return
6485
val buildViewModel: BuildViewModel by activity.viewModels()
6586
actionScope.launch {
6687
activity.saveAllResult()
6788
}
68-
buildViewModel.runQuickBuild(module, variant, launchInDebugMode = id == DebugAction.ID)
89+
buildViewModel.runQuickBuild(
90+
module,
91+
resolvedVariant,
92+
launchInDebugMode = id == DebugAction.ID,
93+
launchProfilerAfterInstall = id == ProfilerAction.ID,
94+
gradleArgs = gradleArgs,
95+
)
6996
}
7097
}

0 commit comments

Comments
 (0)