Skip to content

Commit 3508b90

Browse files
Merge pull request #219 from appdevforall/redesign/ui
ADFA-4725: K2Go UX/UI redesign — new content-first UI
2 parents b32d045 + 6f09a43 commit 3508b90

69 files changed

Lines changed: 3552 additions & 35 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.

‎controller/app/build.gradle‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ dependencies {
167167
// 10. Core dependencies
168168
implementation 'androidx.appcompat:appcompat:1.7.1'
169169
implementation 'com.google.android.material:material:1.13.0'
170+
implementation 'com.airbnb.android:lottie:6.1.0'
170171
implementation 'androidx.recyclerview:recyclerview:1.3.2'
171172
implementation 'androidx.biometric:biometric:1.1.0'
172173
implementation 'androidx.webkit:webkit:1.12.0'

‎controller/app/src/main/AndroidManifest.xml‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
2121
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
2222
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
23-
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
2423

2524
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
2625
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
@@ -80,6 +79,25 @@
8079
tools:replace="screenOrientation" />
8180

8281
<activity android:name=".SetupActivity" />
82+
83+
<!-- ADFA-4725: new content-first UI shell (scoped Material 3 theme). -->
84+
<activity
85+
android:name=".redesign.LibraryActivity"
86+
android:exported="false"
87+
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
88+
android:theme="@style/Theme.K2Go" />
89+
90+
<activity
91+
android:name=".redesign.SetupLibraryActivity"
92+
android:exported="false"
93+
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
94+
android:theme="@style/Theme.K2Go" />
95+
96+
<activity
97+
android:name=".redesign.WizardActivity"
98+
android:exported="false"
99+
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
100+
android:theme="@style/Theme.K2Go" />
83101
<activity android:name=".feedback.crash.presentation.CrashReportActivity" />
84102
<activity android:name=".QrActivity" />
85103

‎controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class AdbPairingReceiver extends BroadcastReceiver {
1919

2020
public static final String KEY_PIN_REPLY = "key_pin_reply";
2121
public static final int NOTIFICATION_ID = 9401;
22+
public static final int RETURN_NOTIFICATION_ID = 9402;
2223
private static final String TAG = "AdbPairingNative";
2324

2425

@@ -73,10 +74,25 @@ private void performNativePairing(Context context, String hostIp, int pairingPor
7374
.putBoolean("pairing_just_succeeded", true)
7475
.apply();
7576

76-
// CRITICAL: Bring our app back to the screen automatically!
77-
Intent bringToFront = new Intent(context, MainActivity.class);
78-
bringToFront.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
79-
context.startActivity(bringToFront);
77+
// ADFA-4725 (overlay decision, Option B): no SAW / no background
78+
// startActivity. Post an OS-sanctioned "tap to return" notification that
79+
// opens the new UI (LibraryActivity). Reliable on restrictive phones.
80+
Intent open = new Intent(context, org.iiab.controller.redesign.LibraryActivity.class);
81+
open.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
82+
int piFlags = android.app.PendingIntent.FLAG_UPDATE_CURRENT
83+
| (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M
84+
? android.app.PendingIntent.FLAG_IMMUTABLE : 0);
85+
android.app.PendingIntent pi = android.app.PendingIntent.getActivity(context, 0, open, piFlags);
86+
NotificationManager nmReturn = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
87+
if (nmReturn != null) {
88+
NotificationCompat.Builder rb = new NotificationCompat.Builder(context, "adb_pairing_channel")
89+
.setSmallIcon(android.R.drawable.ic_dialog_info)
90+
.setContentTitle("Paired")
91+
.setContentText("Tap to return to K2Go")
92+
.setAutoCancel(true)
93+
.setContentIntent(pi);
94+
nmReturn.notify(RETURN_NOTIFICATION_ID, rb.build());
95+
}
8096

8197
} else {
8298
Log.e(TAG, "Native pairing failed.");

‎controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java‎

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,54 @@ public static void wipeCache(Context context) {
8080
if (cacheFile.exists()) cacheFile.delete();
8181
}
8282

83+
/** Canonical Wikipedia variants, ordered coverage-desc then detail-desc. */
84+
public static final String[] CANONICAL_VARIANTS = {
85+
"all_maxi", "all_nopic", "all_mini",
86+
"top1m_maxi", "top1m_nopic", "top1m_mini",
87+
"top_maxi", "top_nopic", "top_mini"
88+
};
89+
90+
/** Variants actually present for a language, in canonical order (for the UI to render). */
91+
public static java.util.List<String> availableVariants(JSONObject langData) {
92+
java.util.List<String> out = new java.util.ArrayList<>();
93+
if (langData == null) return out;
94+
for (String key : CANONICAL_VARIANTS) if (langData.has(key)) out.add(key);
95+
return out;
96+
}
97+
98+
private static String coverageOf(String variant) {
99+
if (variant.startsWith("all_")) return "all";
100+
if (variant.startsWith("top1m_")) return "top1m";
101+
return "top";
102+
}
103+
private static String detailOf(String variant) {
104+
if (variant.endsWith("_maxi")) return "maxi";
105+
if (variant.endsWith("_mini")) return "mini";
106+
return "nopic";
107+
}
108+
109+
/**
110+
* Resolve an explicit variant against what the language actually offers, degrading only
111+
* DOWNWARD within the coverage family (top1m -> top) and across detail, never escalating.
112+
* Returns a present key or null.
113+
*/
114+
private static String resolveInFamily(JSONObject langData, String requested) {
115+
if (langData == null) return null;
116+
if (langData.has(requested)) return requested;
117+
String cov = coverageOf(requested);
118+
String det = detailOf(requested);
119+
String[] covLadder = cov.equals("all") ? new String[]{"all"}
120+
: cov.equals("top1m") ? new String[]{"top1m", "top"}
121+
: new String[]{"top"};
122+
String[] detLadder = det.equals("maxi") ? new String[]{"maxi", "nopic", "mini"}
123+
: det.equals("nopic") ? new String[]{"nopic", "mini", "maxi"}
124+
: new String[]{"mini", "nopic", "maxi"};
125+
for (String cc : covLadder)
126+
for (String dd : detLadder)
127+
if (langData.has(cc + "_" + dd)) return cc + "_" + dd;
128+
return null;
129+
}
130+
83131
public static void getOrFetchCatalog(Context context, CacheListener listener) {
84132
new Thread(() -> {
85133
File cacheFile = new File(context.getCacheDir(), CACHE_FILE_NAME);
@@ -134,12 +182,19 @@ public static void getOrFetchCatalog(Context context, CacheListener listener) {
134182

135183
if (!database.has(lang)) database.put(lang, new JSONObject());
136184

137-
// NEW: Store size AND exact filename
138-
JSONObject variantData = new JSONObject();
139-
variantData.put("size", sizeGb);
140-
variantData.put("file", "wikipedia_" + lang + "_" + variant + "_" + date + ".zim");
141-
142-
database.getJSONObject(lang).put(variant, variantData);
185+
// Store size, exact filename AND date. The listing can hold more than
186+
// one date for the same variant; keep ONLY the newest by declared
187+
// intent ("YYYY-MM" compares lexicographically = chronologically),
188+
// not by whichever row happens to come last in the HTML.
189+
JSONObject langObj = database.getJSONObject(lang);
190+
JSONObject existing = langObj.optJSONObject(variant);
191+
if (existing == null || date.compareTo(existing.optString("date", "")) > 0) {
192+
JSONObject variantData = new JSONObject();
193+
variantData.put("size", sizeGb);
194+
variantData.put("date", date);
195+
variantData.put("file", "wikipedia_" + lang + "_" + variant + "_" + date + ".zim");
196+
langObj.put(variant, variantData);
197+
}
143198
}
144199
}
145200

@@ -223,30 +278,40 @@ private static void computeProjection(Context context, Tier tier, boolean pullCo
223278
@Override
224279
public void onReady(JSONObject catalog) {
225280
double kiwixSize = 0.0;
226-
String resolvedLang = langCode;
281+
String resolvedLang = org.iiab.controller.applang.data.ContentLanguage.normalize(langCode);
227282
String resolvedFile = null;
228283

229-
JSONObject langData = catalog.optJSONObject(langCode);
284+
JSONObject langData = catalog.optJSONObject(resolvedLang);
230285

231286
if (langData == null) {
232287
langData = catalog.optJSONObject("en");
233288
resolvedLang = "en";
234289
}
235290

236291
if (langData != null) {
237-
if (overrideVariant != null && langData.has(overrideVariant)) {
238-
JSONObject vData = langData.optJSONObject(overrideVariant);
239-
// VDATA NULL PROTECTION
240-
if (vData != null) {
241-
kiwixSize = vData.optDouble("size", 0.0);
242-
resolvedFile = vData.optString("file", null);
292+
if (overrideVariant != null && overrideVariant.isEmpty()) {
293+
// Explicit "no Wikipedia" (skip): leave kiwixSize 0 / resolvedFile
294+
// null. Skipping must mean ZERO — never a tier-coupled auto-pick.
295+
} else if (overrideVariant != null) {
296+
// Explicit pick from the UI. Honor it exactly, or degrade WITHIN
297+
// the same coverage family (e.g. top1m_maxi -> top_maxi), NEVER
298+
// escalating — a "Popular" pick must not silently become the full
299+
// "Everything" ZIM. If nothing in-family exists, install no
300+
// Wikipedia rather than substituting a different corpus.
301+
String resolved = resolveInFamily(langData, overrideVariant);
302+
if (resolved != null) {
303+
JSONObject vData = langData.optJSONObject(resolved);
304+
if (vData != null) {
305+
kiwixSize = vData.optDouble("size", 0.0);
306+
resolvedFile = vData.optString("file", null);
307+
}
243308
}
244309
} else {
310+
// Legacy callers with no explicit pick: priority auto-select.
245311
String[] priorities = {"all_maxi", "top1m_maxi", "all_nopic", "top1m_nopic", "top_maxi", "all_mini", "top_nopic"};
246312
for (String p : priorities) {
247313
if (langData.has(p)) {
248314
JSONObject vData = langData.optJSONObject(p);
249-
// VDATA NULL PROTECTION
250315
if (vData != null) {
251316
double size = vData.optDouble("size", 0.0);
252317
if (size <= finalLimit) {
@@ -280,6 +345,12 @@ public void onError(String error) {
280345
* <p>Performs a (cached) network call; must run off the main thread — it does,
281346
* because every caller is inside {@link #calculateProjectedSize}'s worker thread.
282347
*/
348+
/** No-network last-known OS size (RootfsCatalog fallback) — instant, for placeholders. */
349+
public static double fallbackOsSizeGb(Tier tier) {
350+
RootfsCatalog catalog = new RootfsCatalog();
351+
return ByteFormatter.toGiB(catalog.fallbackBytes(toDomainTier(tier), catalog.detectAbi()));
352+
}
353+
283354
private static double resolveOsSizeGb(Context context, Tier tier) {
284355
RootfsCatalog catalog = new RootfsCatalog(context);
285356
RootfsRepository repository =

‎controller/app/src/main/java/org/iiab/controller/MainActivity.java‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ public boolean isTermuxInstalled() {
111111
/** Notification-tap flag: open the full terminal directly (ADFA-4696). */
112112
public static final String EXTRA_OPEN_TERMINAL = "org.iiab.controller.OPEN_TERMINAL";
113113

114+
/** Launched only to show the terminal (from the new UI): finish on close instead of
115+
* revealing the old dashboard. */
116+
public static final String EXTRA_TERMINAL_ONLY = "org.iiab.controller.TERMINAL_ONLY";
117+
private boolean terminalOnlyMode = false;
118+
114119
private TerminalController terminalController;
115120

116121
public void invalidateModuleStateTrust() {
@@ -581,10 +586,35 @@ protected void onNewIntent(Intent intent) {
581586
private void maybeOpenTerminalFromIntent(Intent intent) {
582587
if (intent == null || terminalController == null) return;
583588
if (!intent.getBooleanExtra(EXTRA_OPEN_TERMINAL, false)) return;
589+
terminalOnlyMode = intent.getBooleanExtra(EXTRA_TERMINAL_ONLY, false);
584590
intent.removeExtra(EXTRA_OPEN_TERMINAL); // consume so it fires once
591+
intent.removeExtra(EXTRA_TERMINAL_ONLY);
585592
View root = findViewById(android.R.id.content);
586-
if (root != null) root.post(() -> terminalController.openFullTerminal());
587-
else terminalController.openFullTerminal();
593+
Runnable open = () -> {
594+
terminalController.openFullTerminal();
595+
if (terminalOnlyMode) attachTerminalOnlyFinish();
596+
};
597+
if (root != null) root.post(open);
598+
else open.run();
599+
}
600+
601+
/** In terminal-only mode, hiding the terminal sheet returns to the caller (the new UI)
602+
* rather than exposing the old dashboard behind it. */
603+
private void attachTerminalOnlyFinish() {
604+
View sheet = findViewById(R.id.terminal_bottom_sheet);
605+
if (sheet == null) return;
606+
com.google.android.material.bottomsheet.BottomSheetBehavior<View> b =
607+
com.google.android.material.bottomsheet.BottomSheetBehavior.from(sheet);
608+
b.addBottomSheetCallback(new com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback() {
609+
@Override
610+
public void onStateChanged(@androidx.annotation.NonNull View bottomSheet, int newState) {
611+
if (newState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN) {
612+
finish();
613+
}
614+
}
615+
@Override
616+
public void onSlide(@androidx.annotation.NonNull View bottomSheet, float slideOffset) { }
617+
});
588618
}
589619

590620
private void toggleTheme() {

‎controller/app/src/main/java/org/iiab/controller/PortalActivity.java‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,14 @@ protected void onCreate(Bundle savedInstanceState) {
9999
Button btnExit = findViewById(R.id.btnExit);
100100
Button btnForward = findViewById(R.id.btnForward);
101101

102-
// --- PREPARE HIDDEN BAR ---
102+
// --- PERSISTENT BAR ---
103+
// Keep the nav bar (with Exit) visible so a non-technical user always has an
104+
// obvious way out. The handle is only used after a manual hide (btnHideNav).
103105
bottomNav.post(() -> {
104-
bottomNav.setTranslationY(bottomNav.getHeight()); // Move outside the screen
106+
bottomNav.setTranslationY(0);
105107
bottomNav.setVisibility(View.VISIBLE);
106108
});
109+
btnHandle.setVisibility(View.GONE);
107110

108111
// --- AUTO-HIDE TIMER ---
109112
Handler hideHandler = new Handler(Looper.getMainLooper());
@@ -114,10 +117,8 @@ protected void onCreate(Bundle savedInstanceState) {
114117
btnHandle.animate().alpha(1f).setDuration(150);
115118
};
116119

117-
Runnable resetTimer = () -> {
118-
hideHandler.removeCallbacks(hideRunnable);
119-
hideHandler.postDelayed(hideRunnable, 5000);
120-
};
120+
// Persistent bar: never auto-hide. Cancel any pending hide; only btnHideNav hides.
121+
Runnable resetTimer = () -> hideHandler.removeCallbacks(hideRunnable);
121122

122123
// --- HANDLE LOGIC (Show Bar) ---
123124
btnHandle.setOnClickListener(v -> {

‎controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ private void completeSetup() {
243243
SharedPreferences prefs = requireContext().getSharedPreferences(
244244
getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
245245
prefs.edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply();
246-
startActivity(new Intent(requireContext(), MainActivity.class));
246+
startActivity(new Intent(requireContext(), org.iiab.controller.redesign.LibraryActivity.class));
247247
requireActivity().finish();
248248
}
249249

‎controller/app/src/main/java/org/iiab/controller/SplashActivity.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ protected void onCreate(Bundle savedInstanceState) {
6464
fade.setAlpha(0f);
6565
new Handler(Looper.getMainLooper()).postDelayed(() ->
6666
fade.animate().alpha(1f).setDuration(EXIT_FADE_MS).withEndAction(() -> {
67-
startActivity(new Intent(SplashActivity.this, MainActivity.class));
67+
startActivity(new Intent(SplashActivity.this, org.iiab.controller.redesign.LibraryActivity.class));
6868
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
6969
finish();
7070
}).start(), EXIT_AT_MS);

‎controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ private SystemStateEvaluator() {
2626
private static volatile String cachedDebianArch;
2727
private static volatile boolean archCalculated;
2828

29+
/** True when a system (rootfs) is actually installed on disk — the reliable signal for
30+
* whether "Get more" should skip the destructive system step and go straight to content. */
31+
public static boolean isSystemInstalled(Context ctx) {
32+
File rootfsDir = new File(ctx.getFilesDir(), "rootfs/installed-rootfs/iiab");
33+
return new File(rootfsDir, "bin/bash").exists()
34+
|| new File(rootfsDir, "usr/local/pdsm/flag_install_ready").exists();
35+
}
36+
2937
/** Server responding → ONLINE; else derive from the rootfs on disk. */
3038
public static DashboardFragment.SystemState evaluate(Context ctx, boolean serverAlive) {
3139
File rootfsDir = new File(ctx.getFilesDir(), "rootfs/installed-rootfs/iiab");

‎controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ private Notification buildNotification() {
9797
// version-footer gesture (ADFA-4696). SINGLE_TOP reuses the running Activity.
9898
Intent open = new Intent(this, MainActivity.class)
9999
.putExtra(MainActivity.EXTRA_OPEN_TERMINAL, true)
100+
.putExtra(MainActivity.EXTRA_TERMINAL_ONLY, true)
100101
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
101102
PendingIntent pending = PendingIntent.getActivity(
102103
this, 0, open, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

0 commit comments

Comments
 (0)