Skip to content

Commit 9e0868f

Browse files
committed
release: prepare MG4Tasker 1.1.0
1 parent a8f20da commit 9e0868f

17 files changed

Lines changed: 371 additions & 79 deletions

‎.github/ISSUE_TEMPLATE/bug_report.yml‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,31 @@ body:
107107
- type: textarea
108108
id: logs
109109
attributes:
110-
label: "Logs or Screenshots"
110+
label: "Logs or Screenshots (mandatory)"
111111
description: |
112-
Attach:
113-
- Logcat output (from ADB: `adb logcat | grep -i mg4`)
114-
- Rule execution log from app diagnostic screen
115-
- Screenshots showing unexpected behavior
116-
placeholder: "Paste logcat or logs here"
112+
**Required.** How to get and attach the log:
113+
- App: Diagnostic tab → "Export" button → pick a folder. This writes a report file
114+
(`.txt`) to that folder. Pull the file off the device (file manager / USB / cloud),
115+
then drag-and-drop the file directly into this text box on GitHub — it uploads and
116+
inserts a link automatically. You can also paste the contents inline below.
117+
- ADB (if available): `adb logcat | grep -i mg4`, save to a file, attach the same way.
118+
- Screenshots: drag-and-drop image files into this box the same way.
119+
placeholder: "Drag the exported diagnostic file here, or paste logcat/logs"
117120
rows: 8
121+
validations:
122+
required: true
118123

119124
- type: textarea
120125
id: console
121126
attributes:
122-
label: "Console log"
123-
description: "The Console tab has a Share button. Paste the relevant lines here."
127+
label: "Console log / Diagnostic export (mandatory)"
128+
description: |
129+
**Required.** Console tab → Share button → save the file, then drag-and-drop it into
130+
this box (or paste the relevant lines below). This is separate from the Diagnostic
131+
export above — attach both.
124132
render: text
133+
validations:
134+
required: true
125135

126136
- type: dropdown
127137
id: vehicle_state

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to this project are documented here. Format follows
66

77
## [Unreleased]
88

9+
## [1.1.0] - 2026-08-10
10+
911
### Added
1012

1113
- **"Ask for a profile", an action that hands the choice back to the driver.** "Apply a

‎MG4Hardware‎

‎app/build.gradle.kts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ android {
1010
applicationId = "com.mg4.tasker"
1111
minSdk = 28
1212
targetSdk = 34
13-
versionCode = 1
14-
versionName = "1.0.0"
13+
versionCode = 2
14+
versionName = "1.1.0"
1515
}
1616

1717
// MG4Tasker doit être signé avec la MÊME clé plateforme que MG4Control : c'est la seule
@@ -116,6 +116,7 @@ dependencies {
116116
implementation(libs.androidx.constraintlayout)
117117
implementation(libs.material)
118118
implementation(libs.androidx.fragment.ktx)
119+
implementation(libs.androidx.viewpager2)
119120
implementation(libs.androidx.recyclerview)
120121
implementation(libs.gson)
121122
implementation(libs.androidx.lifecycle.viewmodel)

‎app/src/main/java/com/mg4/tasker/ui/MainActivity.kt‎

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,50 @@ import android.os.Bundle
44
import android.view.View
55
import androidx.appcompat.app.AppCompatActivity
66
import androidx.fragment.app.Fragment
7+
import androidx.viewpager2.adapter.FragmentStateAdapter
8+
import androidx.viewpager2.widget.ViewPager2
9+
import com.google.android.material.button.MaterialButton
710
import com.mg4.tasker.BuildConfig
811
import com.mg4.tasker.R
912
import com.mg4.tasker.databinding.ActivityMainBinding
1013
import com.mg4.tasker.store.UpdateLaunchStore
1114

1215
class MainActivity : AppCompatActivity() {
1316

17+
/**
18+
* One screen of the application, in the order the top bar lists them.
19+
*
20+
* [tabId] is the top-bar button that selects it — the same button the page-change
21+
* callback marks as current, and the id the adapter keys its fragments on. The pair
22+
* being declared once is what keeps the button and the swipe agreeing about where
23+
* they lead.
24+
*/
25+
private class Screen(val tabId: Int, val create: () -> Fragment)
26+
1427
private lateinit var binding: ActivityMainBinding
1528
private val consoleUnlock = ConsoleUnlock()
1629
private var consoleVisible = BuildConfig.CONSOLE_VISIBLE_BY_DEFAULT
1730

31+
/** Rebuilt by [rebuildScreens]; Console is absent until it is unlocked. */
32+
private var screens: List<Screen> = emptyList()
33+
34+
/**
35+
* Reads [screens] on every call rather than capturing it, so unlocking the console
36+
* only needs a notify — the pages already built keep their fragments.
37+
*
38+
* The ids are the tab ids, not the positions: inserting Console in the middle of the
39+
* list shifts Configuration by one, and a position-keyed adapter would hand
40+
* Configuration's fragment to the console's page.
41+
*/
42+
private val pagerAdapter by lazy {
43+
object : FragmentStateAdapter(this) {
44+
override fun getItemCount() = screens.size
45+
override fun createFragment(position: Int) = screens[position].create()
46+
override fun getItemId(position: Int) = screens[position].tabId.toLong()
47+
override fun containsItem(itemId: Long) = screens.any { it.tabId.toLong() == itemId }
48+
}
49+
}
50+
1851
override fun onCreate(savedInstanceState: Bundle?) {
1952
super.onCreate(savedInstanceState)
2053
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -23,30 +56,34 @@ class MainActivity : AppCompatActivity() {
2356
consoleVisible = BuildConfig.CONSOLE_VISIBLE_BY_DEFAULT ||
2457
(savedInstanceState?.getBoolean(STATE_CONSOLE_VISIBLE) == true)
2558
updateConsoleVisibility()
59+
rebuildScreens()
2660

27-
// Stable is the offline channel: keep its support console out of the normal driver
28-
// navigation, while still leaving it reachable to an on-vehicle diagnostician.
29-
binding.tabDiagnostic.setOnClickListener {
30-
if (!consoleVisible && consoleUnlock.onDiagnosticTap()) {
31-
consoleVisible = true
32-
updateConsoleVisibility()
33-
}
34-
}
61+
binding.content.adapter = pagerAdapter
62+
// Only the neighbours are kept alive. Holding all five would mean the diagnostic
63+
// and console fragments polling in the background of a screen nobody is reading.
64+
binding.content.offscreenPageLimit = 1
65+
binding.content.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
66+
override fun onPageSelected(position: Int) = markCurrentPage(position)
67+
})
3568

36-
binding.tabGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
37-
if (!isChecked) return@addOnButtonCheckedListener
38-
show(
39-
when (checkedId) {
40-
R.id.tabHistory -> HistoryFragment()
41-
R.id.tabDiagnostic -> DiagnosticFragment()
42-
R.id.tabConsole -> ConsoleFragment()
43-
R.id.tabConfig -> ConfigFragment()
44-
else -> RulesFragment()
69+
TAB_IDS.forEach { tabId ->
70+
tab(tabId).setOnClickListener {
71+
// Stable is the offline channel: keep its support console out of the normal
72+
// driver navigation, while still leaving it reachable to an on-vehicle
73+
// diagnostician. The tap that unlocks it still opens Diagnostic, so the
74+
// gesture never costs the diagnostician the screen they asked for.
75+
if (tabId == R.id.tabDiagnostic && !consoleVisible &&
76+
consoleUnlock.onDiagnosticTap()
77+
) {
78+
unlockConsole()
4579
}
46-
)
80+
// Animated, so the button does the same thing the swipe does: the direction
81+
// of travel is what tells the driver where they are in the row.
82+
goTo(tabId, smooth = true)
83+
}
4784
}
4885

49-
// Rotation restores the fragment via the FragmentManager: only re-select the
86+
// Rotation restores the page from the pager's own saved state: only pick the
5087
// initial tab on first show, otherwise the screen jumps back to the rules.
5188
if (savedInstanceState == null) {
5289
// After an upgrade, make the new build's live vehicle/capability verdicts the
@@ -57,8 +94,11 @@ class MainActivity : AppCompatActivity() {
5794
} else {
5895
R.id.tabRules
5996
}
60-
binding.tabGroup.check(initialTab)
97+
// Without animation: at startup there is no movement to explain, only a
98+
// starting screen.
99+
goTo(initialTab, smooth = false)
61100
}
101+
markCurrentPage(binding.content.currentItem)
62102

63103
// Unstable builds check for a newer pre-release; the stable flavor's UpdateHook is
64104
// a no-op and the stable APK contains no updater code at all.
@@ -70,17 +110,61 @@ class MainActivity : AppCompatActivity() {
70110
super.onSaveInstanceState(outState)
71111
}
72112

113+
private fun rebuildScreens() {
114+
screens = buildList {
115+
add(Screen(R.id.tabRules) { RulesFragment() })
116+
add(Screen(R.id.tabHistory) { HistoryFragment() })
117+
add(Screen(R.id.tabDiagnostic) { DiagnosticFragment() })
118+
if (consoleVisible) add(Screen(R.id.tabConsole) { ConsoleFragment() })
119+
add(Screen(R.id.tabConfig) { ConfigFragment() })
120+
}
121+
}
122+
123+
private fun unlockConsole() {
124+
consoleVisible = true
125+
updateConsoleVisibility()
126+
rebuildScreens()
127+
val index = screens.indexOfFirst { it.tabId == R.id.tabConsole }
128+
if (index >= 0) pagerAdapter.notifyItemInserted(index)
129+
}
130+
73131
private fun updateConsoleVisibility() {
74132
binding.tabConsole.visibility = if (consoleVisible) View.VISIBLE else View.GONE
75133
}
76134

77-
private fun show(fragment: Fragment) {
78-
supportFragmentManager.beginTransaction()
79-
.replace(R.id.content, fragment)
80-
.commit()
135+
/** Moves the pager to [tabId]'s page; a no-op for a tab that is not a page. */
136+
private fun goTo(tabId: Int, smooth: Boolean) {
137+
val index = screens.indexOfFirst { it.tabId == tabId }
138+
if (index >= 0) binding.content.setCurrentItem(index, smooth)
81139
}
82140

141+
/**
142+
* Marks the top-bar tab of the page on screen.
143+
*
144+
* Only `isSelected` is set: fill, text, icon and stroke come from the
145+
* res/color/nav_tab_*.xml selectors applied by the Widget.MG4.NavTab style. Painting
146+
* the colors here as well would declare them twice, and it is the copy in code that
147+
* would win — including on the states (pressed, disabled) it knows nothing about.
148+
* `isSelected` also serves TalkBack, which announces the current destination.
149+
*/
150+
private fun markCurrentPage(position: Int) {
151+
screens.forEachIndexed { index, screen ->
152+
tab(screen.tabId).isSelected = index == position
153+
}
154+
}
155+
156+
private fun tab(tabId: Int): MaterialButton = findViewById(tabId)
157+
83158
private companion object {
84159
const val STATE_CONSOLE_VISIBLE = "consoleVisible"
160+
161+
/** Every tab in the bar, including the console's while it is still hidden. */
162+
val TAB_IDS = listOf(
163+
R.id.tabRules,
164+
R.id.tabHistory,
165+
R.id.tabDiagnostic,
166+
R.id.tabConsole,
167+
R.id.tabConfig
168+
)
85169
}
86170
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Nav tab background. The state comes from View.isSelected, set by
3+
MainActivity.markCurrentPage on the one tab whose page is on screen: the color is
4+
never applied by hand in code, which guarantees a tab cannot stay painted "active"
5+
if the list of screens changes. -->
6+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
7+
<item android:state_selected="true" android:color="@color/mg4_accent" />
8+
<item android:color="@color/mg4_surface_raised" />
9+
</selector>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Nav tab text + icon. The selected tab sits on @color/mg4_accent, so its foreground
3+
is mg4_on_accent; unselected tabs use mg4_text_primary. Both pairs clear 7:1. -->
4+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
5+
<item android:state_selected="true" android:color="@color/mg4_on_accent" />
6+
<item android:color="@color/mg4_text_primary" />
7+
</selector>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Nav tab outline. Checked tab keeps the same stroke color as its fill so its size
3+
doesn't jump a dp on selection. -->
4+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
5+
<item android:state_selected="true" android:color="@color/mg4_accent" />
6+
<item android:color="@color/mg4_outline" />
7+
</selector>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Top bar surface + a 2dp accent underline, so the bar reads as a bar even against
3+
a light page background. -->
4+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
5+
<item>
6+
<shape android:shape="rectangle">
7+
<solid android:color="@color/mg4_surface" />
8+
</shape>
9+
</item>
10+
<item android:gravity="bottom" android:height="2dp">
11+
<shape android:shape="rectangle">
12+
<solid android:color="@color/mg4_accent" />
13+
</shape>
14+
</item>
15+
</layer-list>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Engrenage : onglet Configuration. Teinté par @color/nav_tab_foreground. -->
3+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:width="24dp"
5+
android:height="24dp"
6+
android:viewportWidth="24"
7+
android:viewportHeight="24">
8+
<path
9+
android:fillColor="#FFFFFFFF"
10+
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
11+
</vector>

0 commit comments

Comments
 (0)