Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cc116fc
feat(core/app,core/resources): redesign Home screen and simplify onbo…
Sep 21, 2026
5511d47
ci: fix stale absolute paths in missing-dirs workaround
Sep 21, 2026
4929dcf
fix(core/app): use standard debug keystore for Debug builds
Sep 22, 2026
13ce1c8
feat(core/app): polish home screen layout
Sep 22, 2026
06176eb
fix(core/app): point in-app updater at ACS Zen fork metadata
Sep 22, 2026
c026760
Merge pull request #1 from xeriomy/feature/home-redesign
xeriomy Sep 22, 2026
3fbd6fe
feat(core/app): redesign new project template picker
Sep 23, 2026
2320409
feat(core/app): redesign new project options
Sep 23, 2026
42e1d66
feat(core/app): redesign project management
Sep 23, 2026
af8cf37
fix(core/app): use appcompat attrs for project actions sheet
Sep 23, 2026
6b932f3
feat(core/app): redesign settings root
Sep 23, 2026
600333d
fix(core/app): remove duplicate settings header
Sep 23, 2026
07d9ef4
Merge pull request #2 from xeriomy/feature/home-redesign
xeriomy Sep 23, 2026
8907cfc
ci: clean up GitHub Actions build warnings
Sep 23, 2026
8fccfe3
ci: use upload-artifact v6 for Node 24 runtime
Sep 23, 2026
606e990
clean github build warnings
xeriomy Sep 23, 2026
c9def82
fix(git): expose all remote branches after clone and allow checking t…
Sep 26, 2026
a74f749
fix(build): keep desugar_jdk_libs runtime packaged in the APK
Sep 26, 2026
c88326d
fix(core/app): sign Debug builds with a stable committed debug keystore
Sep 26, 2026
a4a61a7
Merge pull request #5 from xeriomy/fix/git-clone-branch-visibility
xeriomy Sep 27, 2026
5ebadd3
Merge pull request #6 from xeriomy/fix/git-branch-desugar-crash
xeriomy Sep 27, 2026
e2e4e61
Merge pull request #7 from xeriomy/fix/debug-apk-install-conflict
xeriomy Sep 27, 2026
9d0e597
ui(git): unify the Git client screens around one list-screen scaffold
xeriomy Sep 27, 2026
4025087
fix(ui): resolve colorError from appcompat for destructive dialogs
xeriomy Sep 27, 2026
df0c1b3
ui(git): balance the remotes actions and compact the branch list
xeriomy Sep 27, 2026
db092c3
feat(ui): add expand and collapse for the Git side panel
xeriomy Sep 27, 2026
5dd5d55
ui(git): unify git feedback, headers and destructive dialogs
xeriomy Sep 27, 2026
24fbd66
fix(ui): use the framework popup menu for branch row actions
xeriomy Sep 27, 2026
8b47f65
fix(ui): prevent outer git client scrolling
xeriomy Sep 27, 2026
77ab21c
Merge pull request #8 from xeriomy/ui/git-ui
xeriomy Sep 27, 2026
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
38 changes: 11 additions & 27 deletions .github/workflows/asm_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,13 @@ on:
jobs:
build_release_apk:
name: Build APK
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
concurrency:
group: build-apk-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ github.token }}

- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: 'recursive'
fetch-depth: 0
Expand All @@ -54,19 +52,19 @@ jobs:
echo "BUILD_TYPE_LOWER=$(echo "$BUILD_TYPE" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'adopt'
distribution: 'temurin'

- name: Generating missing dirs
run: mkdir -p /home/runner/work/android-code-studio/android-code-studio/core/app/build/intermediates/external_file_lib_dex_archives/debug/desugarDebugFileDependencies && mkdir -p /home/runner/work/android-code-studio/android-code-studio/core/app/build/intermediates/l8_art_profile/release/l8DexDesugarLibRelease
run: mkdir -p core/app/build/intermediates/external_file_lib_dex_archives/debug/desugarDebugFileDependencies && mkdir -p core/app/build/intermediates/l8_art_profile/release/l8DexDesugarLibRelease

- name: Change Gradle wrapper permissions
run: chmod +x ./gradlew

- name: Restore cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
Expand All @@ -92,29 +90,15 @@ jobs:
run: ls -la core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/ || echo "Directory not found"

- name: Upload arm64-v8a APK
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: apk-arm64-v8a-${{ env.BUILD_TYPE_LOWER }}
path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*arm64-v8a*.apk
if: always()

- name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: apk-armeabi-v7a-${{ env.BUILD_TYPE_LOWER }}
path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*armeabi-v7a*.apk
if: always()

- name: Upload x86_64 APK
uses: actions/upload-artifact@v4
with:
name: apk-x86_64-${{ env.BUILD_TYPE_LOWER }}
path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*x86_64*.apk
if: always()

- name: Upload universal APK
uses: actions/upload-artifact@v4
with:
name: apk-universal-${{ env.BUILD_TYPE_LOWER }}
path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*universal*.apk
if: always()
47 changes: 45 additions & 2 deletions core/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,20 @@ buildscript {
}
}

// AGP's file-dependency desugaring task (`desugar<Variant>FileDependencies`) is disabled
// in this project; CI pre-creates its output directory instead
// (see the "Generating missing dirs" step in .github/workflows/asm_build.yml).
//
// The match below is deliberately narrow. AGP also has tasks named
// `l8DexDesugarLib<Variant>`, which contain "Desugar" as well, and those must keep
// running: they produce the `desugar_jdk_libs` runtime dex that PackageAndroidArtifact
// packs into the APK whenever core library desugaring is enabled. With them disabled the
// APK contains no `j$` classes at all, so every desugared JDK call throws at runtime,
// e.g. JGit's `java.util.stream.Collectors.toUnmodifiableList()` (used by
// `RefDirectory.getRefsByPrefix(varargs)`, i.e. `ListMode.ALL` branch listing) fails with
// `NoClassDefFoundError: j$.util.stream.DesugarCollectors`.
tasks.configureEach {
if (name.contains("desugar", ignoreCase = true)) {
if (name.startsWith("desugar") && name.endsWith("FileDependencies")) {
enabled = false
}
}
Expand Down Expand Up @@ -74,6 +86,23 @@ android {


signingConfigs {
// One stable, repo-committed Android debug keystore, used by CI and by local builds
// alike. AGP otherwise defaults to ~/.android/debug.keystore (see AGP's
// SigningConfigFactory, which applies DefaultSigningConfig.DebugSigningConfig for the
// "debug" name). That file does not exist on a GitHub Actions runner, so AGP
// generated a brand-new random key on every run: each Debug APK then carried a
// different signer certificate (verified on runs 36273739542 vs 35924946644) and
// Android refused to install the new build over the previous one, forcing an
// uninstall for every update.
// The credentials below are the stock Android debug ones (androiddebugkey/android),
// so Debug builds still need no developer secrets.
getByName("debug") {
storeFile = file("${rootProject.projectDir}/signing/debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}

create("custom") {
val keyStorePath = "${rootProject.projectDir}/signing/signing-key.jks"
val keyStoreFile = file(keyStorePath)
Expand All @@ -97,7 +126,11 @@ android {

buildTypes {
debug {
signingConfig = signingConfigs.getByName("custom")
// Standard Android debug keystore (signing/debug.keystore, committed with the repo so
// CI and local builds keep one stable signing identity across installs).
// Debug builds must not require the release keystore credentials,
// so CI forks without signing secrets can still assembleDebug.
signingConfig = signingConfigs.getByName("debug")
}

release {
Expand Down Expand Up @@ -302,3 +335,13 @@ dependencies {
compileOnly(projects.tooling.impl)

}

// SigningConfigPlugin (applied to all Android modules via the root build script's
// afterEvaluate hook) assigns its own signing config to every build type, which would
// override the debug assignment above. Re-assert the standard debug keystore here:
// this block runs after the root script's afterEvaluate hook.
afterEvaluate {
extensions.getByType<com.android.build.gradle.AppExtension>().run {
buildTypes.getByName("debug").signingConfig = signingConfigs.getByName("debug")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.tom.rv2ide.R
import com.tom.rv2ide.R.string
import com.tom.rv2ide.app.configuration.IDEBuildConfigProvider
import com.tom.rv2ide.app.configuration.IJdkDistributionProvider
import com.tom.rv2ide.fragments.onboarding.GreetingFragment
import com.tom.rv2ide.fragments.onboarding.IdeSetupConfigurationFragment
import com.tom.rv2ide.fragments.onboarding.OnboardingInfoFragment
import com.tom.rv2ide.fragments.onboarding.PermissionsFragment
Expand Down Expand Up @@ -86,8 +85,6 @@ class OnboardingActivity : AppIntro2() {
isIndicatorEnabled = true
isWizardMode = true

addSlide(GreetingFragment())

if (!PackageUtils.isCurrentUserThePrimaryUser(this)) {
val errorMessage =
getString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
super.onCreate(savedInstanceState)

setSupportActionBar(binding.toolbar)
supportActionBar!!.setTitle(R.string.ide_preferences)
supportActionBar!!.setTitle(R.string.settings_title)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)

binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
Expand All @@ -62,6 +62,7 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {

val args = Bundle()
args.putParcelableArrayList(IDEPreferencesFragment.EXTRA_CHILDREN, ArrayList(prefs.children))
args.putBoolean(IDEPreferencesFragment.EXTRA_IS_ROOT, true)

rootFragment.arguments = args
loadFragment(rootFragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class TomIDEUpdater(private val context: Context) {
companion object {
private const val TAG = "TomIDEUpdater"
private const val UPDATE_JSON_URL =
"https://raw.githubusercontent.com/AndroidCSOfficial/android-code-studio/refs/heads/dev/updater.json"
"https://raw.githubusercontent.com/xeriomy/acs-zen/refs/heads/dev/updater.json"
private const val DOWNLOAD_NOTIFICATION_ID = 1001
}

Expand Down
71 changes: 50 additions & 21 deletions core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@
*/
package com.tom.rv2ide.adapters

import android.content.res.ColorStateList
import android.graphics.Typeface
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tom.rv2ide.R
import com.tom.rv2ide.databinding.ItemBranchBinding
import android.util.TypedValue

/**
* @author Mohammed-baqer-null @ https://github.com/Mohammed-baqer-null
Expand Down Expand Up @@ -55,33 +59,58 @@ class BranchesAdapter(

fun bind(branch: String) {
val isCurrentBranch = branch == getCurrentBranch()
val view = binding.root

// Branch names are shown verbatim - the UI never parses or rewrites them.
binding.textBranchName.text = branch
binding.textBranchName.setTypeface(null, if (isCurrentBranch) Typeface.BOLD else Typeface.NORMAL)
binding.textBranchName.setTextColor(
resolveThemeColor(view, if (isCurrentBranch) R.attr.colorPrimary else R.attr.colorOnSurface)
?: binding.textBranchName.currentTextColor
)

if (isCurrentBranch) {
val typedValue = TypedValue()
binding.root.context.theme.resolveAttribute(
android.R.attr.colorPrimary,
typedValue,
true
)
binding.cardBranch.strokeColor = typedValue.data
binding.cardBranch.strokeWidth = 4
} else {
binding.cardBranch.strokeWidth = 0
// A dot marks the checked out branch; other rows keep the same
// space reserved so every branch name lines up in one column.
binding.viewCurrentDot.visibility = if (isCurrentBranch) View.VISIBLE else View.INVISIBLE
resolveThemeColor(view, R.attr.colorPrimary)?.let { color ->
binding.viewCurrentDot.backgroundTintList = ColorStateList.valueOf(color)
}

binding.buttonCheckout.setOnClickListener {
if (!isCurrentBranch) {
onCheckoutClick(branch)
// Nothing to check out or delete on the branch that is already
// checked out, so that row stays a clean single line.
binding.buttonMore.visibility = if (isCurrentBranch) View.GONE else View.VISIBLE

binding.buttonMore.setOnClickListener { anchor ->
val popup = PopupMenu(view.context, anchor)
popup.inflate(R.menu.branch_row_actions)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_checkout -> {
if (!isCurrentBranch) {
onCheckoutClick(branch)
}
true
}
R.id.action_delete -> {
onDeleteClick(branch)
true
}
else -> false
}
}
popup.show()
}

binding.buttonDelete.setOnClickListener {
onDeleteClick(branch)
}

private fun resolveThemeColor(view: View, attr: Int): Int? {
val context = view.context
val typedValue = TypedValue()
if (!context.theme.resolveAttribute(attr, typedValue, true)) return null
return if (typedValue.resourceId != 0) {
ContextCompat.getColor(context, typedValue.resourceId)
} else {
typedValue.data
}

binding.buttonCheckout.isEnabled = !isCurrentBranch
}
}

Expand All @@ -94,4 +123,4 @@ class BranchesAdapter(
return oldItem == newItem
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,44 @@
package com.tom.rv2ide.adapters

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
import com.tom.rv2ide.databinding.LayoutMainActionItemBinding
import com.google.android.material.button.MaterialButton
import com.tom.rv2ide.R
import com.tom.rv2ide.models.MainScreenAction

/**
* Adapter for the actions available on the main screen.
*
* @param itemLayoutRes item layout to inflate. Any layout with a
* [MaterialButton] with ID `R.id.actionButton` works (row or grid tile).
*
* @author Akash Yadav
*/
class MainActionsListAdapter
@JvmOverloads
constructor(val actions: List<MainScreenAction> = emptyList()) :
RecyclerView.Adapter<MainActionsListAdapter.VH>() {
class VH(val binding: LayoutMainActionItemBinding) : RecyclerView.ViewHolder(binding.root)
constructor(
val actions: List<MainScreenAction> = emptyList(),
@LayoutRes private val itemLayoutRes: Int = R.layout.layout_main_action_item,
) : RecyclerView.Adapter<MainActionsListAdapter.VH>() {
class VH(val root: View, val actionButton: MaterialButton) :
RecyclerView.ViewHolder(root)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
VH(LayoutMainActionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context).inflate(itemLayoutRes, parent, false)
return VH(view, view.findViewById(R.id.actionButton))
}

override fun getItemCount(): Int = actions.size

fun getAction(index: Int) = actions[index]

override fun onBindViewHolder(holder: VH, position: Int) {
val action = getAction(index = position)
val binding = holder.binding

binding.actionButton.apply {
holder.actionButton.apply {
setText(action.text)
setIconResource(action.icon)
setOnClickListener { action.onClick?.invoke(action, it) }
Expand Down
Loading