initial android app snapshot

This commit is contained in:
Xisheng-Zhao
2026-05-17 04:20:20 +08:00
commit 4a0c8c8b8b
35 changed files with 36287 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
}
android {
namespace "ai.multica.app"
compileSdk 36
buildToolsVersion "36.1.0"
defaultConfig {
applicationId "ai.multica.app"
minSdk 24
targetSdk 36
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose true
}
}
dependencies {
implementation platform("androidx.compose:compose-bom:2024.10.01")
implementation "androidx.activity:activity-compose:1.9.3"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.material3:material3"
implementation "androidx.compose.material:material-icons-extended"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-tooling-preview"
implementation "io.github.alexzhirkevich:cupertino:0.1.0-alpha04"
implementation "dev.chrisbanes.haze:haze:1.6.10"
implementation "com.airbnb.android:lottie-compose:6.7.1"
implementation "io.coil-kt:coil-compose:2.7.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.json:json:20240303"
androidTestImplementation "androidx.test:runner:1.6.2"
androidTestImplementation "androidx.test.ext:junit:1.2.1"
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0"
debugImplementation "androidx.compose.ui:ui-tooling"
}
+35
View File
@@ -0,0 +1,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="false">
<activity
android:name=".ComposeVisualAuditActivity"
android:theme="@style/ComposeAppTheme"
android:exported="true" />
<activity
android:name=".ComposePilotActivity"
android:theme="@style/ComposeAppTheme"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="multica" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="false" />
</application>
</manifest>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
package ai.multica.app;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
final class AuthStore {
private static final String PREFS = "multica_auth";
private static final String TOKEN = "token";
private static final String WORKSPACE_ID = "workspace_id";
private static final String LANGUAGE = "language";
private static final String INBOX_CACHE_PREFIX = "inbox_cache_";
private static final String CLOUDFRONT_COOKIE_HEADER = "cloudfront_cookie_header";
private final SharedPreferences prefs;
AuthStore(Context context) {
prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
}
String token() {
return prefs.getString(TOKEN, null);
}
void saveToken(String token) {
prefs.edit().putString(TOKEN, token).apply();
}
void clearToken() {
prefs.edit().remove(TOKEN).remove(WORKSPACE_ID).remove(CLOUDFRONT_COOKIE_HEADER).apply();
}
String workspaceId() {
return prefs.getString(WORKSPACE_ID, null);
}
void saveWorkspaceId(String workspaceId) {
prefs.edit().putString(WORKSPACE_ID, workspaceId).apply();
}
boolean isChinese() {
return "zh".equals(prefs.getString(LANGUAGE, "en"));
}
void setChinese(boolean enabled) {
prefs.edit().putString(LANGUAGE, enabled ? "zh" : "en").apply();
}
String inboxCache(String workspaceId) {
if (workspaceId == null || workspaceId.trim().isEmpty()) return null;
return prefs.getString(INBOX_CACHE_PREFIX + workspaceId, null);
}
void saveInboxCache(String workspaceId, String json) {
if (workspaceId == null || workspaceId.trim().isEmpty() || json == null || json.trim().isEmpty()) return;
prefs.edit().putString(INBOX_CACHE_PREFIX + workspaceId, json).apply();
}
String cloudFrontCookieHeader() {
return prefs.getString(CLOUDFRONT_COOKIE_HEADER, "");
}
void saveCloudFrontCookies(List<String> setCookieHeaders) {
if (setCookieHeaders == null || setCookieHeaders.isEmpty()) return;
Map<String, String> cookies = parseCookieHeader(cloudFrontCookieHeader());
boolean changed = false;
for (String header : setCookieHeaders) {
if (header == null) continue;
int semicolon = header.indexOf(';');
String pair = (semicolon >= 0 ? header.substring(0, semicolon) : header).trim();
int equals = pair.indexOf('=');
if (equals <= 0) continue;
String name = pair.substring(0, equals).trim();
String value = pair.substring(equals + 1).trim();
if (!name.startsWith("CloudFront-") || value.isEmpty()) continue;
cookies.put(name, value);
changed = true;
}
if (!changed) return;
prefs.edit().putString(CLOUDFRONT_COOKIE_HEADER, joinCookieHeader(cookies)).apply();
}
private static Map<String, String> parseCookieHeader(String header) {
Map<String, String> cookies = new LinkedHashMap<>();
if (header == null || header.trim().isEmpty()) return cookies;
String[] parts = header.split(";");
for (String part : parts) {
String pair = part == null ? "" : part.trim();
int equals = pair.indexOf('=');
if (equals <= 0) continue;
cookies.put(pair.substring(0, equals).trim(), pair.substring(equals + 1).trim());
}
return cookies;
}
private static String joinCookieHeader(Map<String, String> cookies) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : cookies.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null || entry.getValue().isEmpty()) continue;
if (sb.length() > 0) sb.append("; ");
sb.append(entry.getKey()).append('=').append(entry.getValue());
}
return sb.toString();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,237 @@
package ai.multica.app
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ai.multica.app.ui.components.MulticaButtonTone
import ai.multica.app.ui.components.MulticaListRow
import ai.multica.app.ui.components.MulticaPillButton
import ai.multica.app.ui.components.MulticaShell
import ai.multica.app.ui.components.MulticaTab
import ai.multica.app.ui.components.StatusPill
import ai.multica.app.ui.theme.MulticaColors
import ai.multica.app.ui.theme.MulticaTheme
class ComposeVisualAuditActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MulticaTheme {
VisualAuditSandbox()
}
}
}
}
private data class InboxPreview(
val type: String,
val status: String,
val title: String,
val body: String,
val unread: Boolean,
)
private data class IssuePreview(
val identifier: String,
val title: String,
val status: String,
val priority: String,
val assignee: String?,
)
@Composable
fun VisualAuditSandbox() {
val selected = remember { mutableStateOf(MulticaTab.Inbox) }
MulticaShell(
selectedTab = selected.value,
workspaceName = "park0er",
languageLabel = "中文",
onLanguageClick = {},
onWorkspaceClick = { selected.value = MulticaTab.Settings },
onSearchClick = {},
onChatClick = { selected.value = MulticaTab.Inbox },
onTabClick = { selected.value = it },
) {
when (selected.value) {
MulticaTab.Inbox -> InboxPreviewScreen()
MulticaTab.Issues -> IssuesPreviewScreen(personal = false)
MulticaTab.MyIssues -> IssuesPreviewScreen(personal = true)
MulticaTab.Projects -> PlaceholderScreen("Projects", "Project list and detail will use the same list-row system.")
MulticaTab.Settings -> PlaceholderScreen("Settings", "Settings will become grouped rows instead of a button stack.")
}
}
}
@Composable
private fun InboxPreviewScreen() {
val rows = listOf(
InboxPreview(
type = "New Comment",
status = "Todo",
title = "核心功能:Issue 列表 & 详情页",
body = "已确认 Android 视觉修正回写:Inbox / Issues 的 pill 控件、白底细描边卡片、底部选中态和正文截断都比之前更干净。",
unread = true,
),
InboxPreview(
type = "Task Failed",
status = "Todo",
title = "核心功能:Issue 列表 & 详情页",
body = "",
unread = false,
),
InboxPreview(
type = "New Comment",
status = "Done",
title = "Android Markdown table QA",
body = "表格、代码块、引用和普通段落都应该使用同一套 Markdown 视觉系统。",
unread = false,
),
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 18.dp, vertical = 18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
item {
PageHeader(
title = "Inbox",
trailing = {
MulticaPillButton("Chat", onClick = {}, tone = MulticaButtonTone.Secondary)
},
)
}
items(rows) { row ->
MulticaListRow(
eyebrow = "${row.type} · ${row.status}",
title = row.title,
subtitle = row.body.ifBlank { null },
unread = row.unread,
onClick = {},
)
}
}
}
@Composable
private fun IssuesPreviewScreen(personal: Boolean) {
val rows = listOf(
IssuePreview("PAR-62", "了解一下金事通 🍎", "Backlog", "Low", "ZhaoXishengGmail"),
IssuePreview("PAR-68", "填写 W-8BEN 税务表格(税率 30%→10%", "Backlog", "Low", null),
IssuePreview("PAR-67", "绑定 Stripe 账户(Hong Kong / Individual", "Backlog", "Low", null),
IssuePreview("PAR-73", "核心功能:Issue 列表 & 详情页", "Todo", "High", "codex"),
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 18.dp, vertical = 18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
item {
PageHeader(
title = if (personal) "My Issues" else "Issues",
trailing = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MulticaPillButton("", onClick = {}, tone = MulticaButtonTone.Secondary)
MulticaPillButton("Board", onClick = {}, tone = MulticaButtonTone.Ghost)
MulticaPillButton("+", onClick = {}, tone = MulticaButtonTone.Primary)
}
},
)
}
item {
Text(
text = "Backlog (${rows.count { it.status == "Backlog" }})",
style = MaterialTheme.typography.titleMedium,
color = MulticaColors.Muted,
modifier = Modifier.padding(top = 8.dp),
)
}
items(rows) { issue ->
IssuePreviewRow(issue)
}
}
}
@Composable
private fun IssuePreviewRow(issue: IssuePreview) {
MulticaListRow(
eyebrow = issue.identifier,
title = issue.title,
subtitle = buildString {
append(issue.status)
append(" · ")
append(issue.priority)
if (!issue.assignee.isNullOrBlank()) {
append(" · ")
append(issue.assignee)
}
},
unread = false,
onClick = {},
)
}
@Composable
private fun PlaceholderScreen(title: String, description: String) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
PageHeader(title = title)
StatusPill("Compose migration sandbox")
Text(
text = description,
style = MaterialTheme.typography.bodyLarge,
color = MulticaColors.Muted,
)
}
}
@Composable
private fun PageHeader(
title: String,
trailing: @Composable (() -> Unit)? = null,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
color = MulticaColors.Text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
if (trailing != null) {
Spacer(modifier = Modifier.height(1.dp))
trailing()
}
}
}
@@ -0,0 +1,159 @@
package ai.multica.app;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
final class AgentMentionMarkdown {
static String markdown(String name, String agentId) {
String cleanName = name == null ? "" : name.trim()
.replace("[", "\\[")
.replace("]", "\\]");
if (cleanName.isEmpty()) cleanName = Models.shortId(agentId);
return "[@" + cleanName + "](mention://agent/" + agentId + ")";
}
private AgentMentionMarkdown() {
}
}
final class InboxNotificationDeduper {
static List<Models.InboxItem> deduplicateByIssue(Collection<Models.InboxItem> items) {
Map<String, Models.InboxItem> newestByIssue = new LinkedHashMap<>();
for (Models.InboxItem item : items) {
if (item == null || item.archived) continue;
String key = item.issueId == null || item.issueId.isEmpty() ? item.id : item.issueId;
Models.InboxItem existing = newestByIssue.get(key);
if (existing == null || compareCreatedAt(item, existing) > 0) {
newestByIssue.put(key, item);
}
}
ArrayList<Models.InboxItem> deduped = new ArrayList<>(newestByIssue.values());
deduped.sort((left, right) -> compareCreatedAt(right, left));
return deduped;
}
private static int compareCreatedAt(Models.InboxItem left, Models.InboxItem right) {
String leftCreatedAt = left.createdAt == null ? "" : left.createdAt;
String rightCreatedAt = right.createdAt == null ? "" : right.createdAt;
return leftCreatedAt.compareTo(rightCreatedAt);
}
private InboxNotificationDeduper() {
}
}
final class SkillFileTree {
static final class Node {
final String name;
final String path;
final boolean directory;
final Models.SkillFile file;
final List<Node> children = new ArrayList<>();
Node(String name, String path, boolean directory, Models.SkillFile file) {
this.name = name;
this.path = path;
this.directory = directory;
this.file = file;
}
}
static List<Node> build(List<Models.SkillFile> files) {
Node root = new Node("", "", true, null);
for (Models.SkillFile file : files) {
String normalized = normalizePath(file.path);
if (normalized.isEmpty()) continue;
String[] parts = normalized.split("/");
Node parent = root;
StringBuilder path = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (parts[i].isEmpty()) continue;
if (path.length() > 0) path.append('/');
path.append(parts[i]);
boolean leaf = i == parts.length - 1;
Node existing = findChild(parent, parts[i], leaf ? path.toString() : path + "/");
if (existing == null) {
existing = new Node(parts[i], leaf ? path.toString() : path + "/", !leaf, leaf ? file : null);
parent.children.add(existing);
}
parent = existing;
}
}
sortRecursively(root.children, true);
return root.children;
}
static List<String> flattenDisplayPaths(List<Node> roots) {
ArrayList<String> paths = new ArrayList<>();
flattenInto(roots, paths);
return paths;
}
static boolean isMarkdownPath(String path) {
String lower = normalizePath(path).toLowerCase(Locale.US);
return lower.endsWith(".md") || lower.endsWith(".markdown");
}
private static void flattenInto(List<Node> nodes, List<String> paths) {
for (Node node : nodes) {
paths.add(node.path);
if (node.directory) flattenInto(node.children, paths);
}
}
private static Node findChild(Node parent, String name, String path) {
for (Node child : parent.children) {
if (child.name.equals(name) && child.path.equals(path)) return child;
}
return null;
}
private static void sortRecursively(List<Node> nodes, boolean root) {
for (Node node : nodes) {
if (node.directory) sortRecursively(node.children, false);
}
nodes.sort(Comparator
.comparingInt((Node node) -> sortBucket(node, root))
.thenComparing(node -> node.name.toLowerCase(Locale.US)));
}
private static int sortBucket(Node node, boolean root) {
if (root && !node.directory && node.name.equalsIgnoreCase("SKILL.md")) return 0;
if (node.directory) return 1;
return root ? 2 : 3;
}
private static String normalizePath(String path) {
if (path == null) return "";
return path.trim().replace('\\', '/').replaceAll("/+", "/").replaceAll("^/|/$", "");
}
private SkillFileTree() {
}
}
final class ChatTimelineState {
final String taskId;
final String status;
final boolean locallyPending;
private ChatTimelineState(String taskId, String status, boolean locallyPending) {
this.taskId = taskId == null ? "" : taskId;
this.status = status == null ? "" : status;
this.locallyPending = locallyPending;
}
static ChatTimelineState afterLocalSend(String taskId, String status) {
return new ChatTimelineState(taskId, status == null || status.isEmpty() ? "queued" : status, true);
}
boolean shouldShowPendingRow() {
return locallyPending || !taskId.isEmpty();
}
}
@@ -0,0 +1,52 @@
package ai.multica.app;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
final class IssueBuckets {
private IssueBuckets() {}
interface Fetcher {
Models.Page<Models.Issue> fetch(String status, int limit, int offset) throws Exception;
}
static List<Models.Issue> loadAll(Fetcher fetcher, int limit) throws Exception {
List<Models.Issue> all = new ArrayList<>();
for (String status : Models.STATUS_VALUES) {
all.addAll(loadStatus(fetcher, status, limit));
}
return all;
}
static List<Models.Issue> loadAllConcurrent(Fetcher fetcher, int limit) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(Math.min(4, Models.STATUS_VALUES.length));
try {
List<Future<List<Models.Issue>>> futures = new ArrayList<>();
for (String status : Models.STATUS_VALUES) {
Callable<List<Models.Issue>> task = () -> loadStatus(fetcher, status, limit);
futures.add(pool.submit(task));
}
List<Models.Issue> all = new ArrayList<>();
for (Future<List<Models.Issue>> future : futures) all.addAll(future.get());
return all;
} finally {
pool.shutdownNow();
}
}
private static List<Models.Issue> loadStatus(Fetcher fetcher, String status, int limit) throws Exception {
List<Models.Issue> issues = new ArrayList<>();
int offset = 0;
while (true) {
Models.Page<Models.Issue> page = fetcher.fetch(status, limit, offset);
issues.addAll(page.items);
if (!page.hasMore || page.items.isEmpty()) break;
offset += page.items.size();
}
return issues;
}
}
@@ -0,0 +1,74 @@
package ai.multica.app;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
final class IssueDetailLoader {
interface Fetcher {
Models.Issue issue(String issueId, String workspaceId) throws Exception;
List<Models.Comment> comments(String issueId, String workspaceId) throws Exception;
List<Models.AgentTask> runs(String issueId, String workspaceId) throws Exception;
List<Models.Project> projects() throws Exception;
List<Models.Member> members() throws Exception;
List<Models.Agent> agents() throws Exception;
}
static Data load(String issueId, String workspaceId, Fetcher fetcher, int poolSize) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(Math.max(1, poolSize));
try {
Future<Models.Issue> issueFuture = pool.submit(() -> fetcher.issue(issueId, workspaceId));
Future<List<Models.Comment>> commentsFuture = null;
Future<List<Models.AgentTask>> runsFuture = null;
if (workspaceId != null && !workspaceId.isEmpty()) {
commentsFuture = pool.submit(() -> fetcher.comments(issueId, workspaceId));
runsFuture = pool.submit(() -> fetcher.runs(issueId, workspaceId));
}
Future<List<Models.Project>> projectsFuture = pool.submit(fetcher::projects);
Future<List<Models.Member>> membersFuture = pool.submit(fetcher::members);
Future<List<Models.Agent>> agentsFuture = pool.submit(fetcher::agents);
Data data = new Data();
data.issue = get(issueFuture);
String resolvedWorkspaceId = data.issue.workspaceId;
if (resolvedWorkspaceId == null || resolvedWorkspaceId.isEmpty()) resolvedWorkspaceId = workspaceId;
final String finalWorkspaceId = resolvedWorkspaceId;
if (commentsFuture == null) commentsFuture = pool.submit(() -> fetcher.comments(issueId, finalWorkspaceId));
if (runsFuture == null) runsFuture = pool.submit(() -> fetcher.runs(issueId, finalWorkspaceId));
data.comments = get(commentsFuture);
data.runs = get(runsFuture);
data.projects = get(projectsFuture);
data.members = get(membersFuture);
data.agents = get(agentsFuture);
return data;
} finally {
pool.shutdownNow();
}
}
private static <T> T get(Future<T> future) throws Exception {
try {
return future.get();
} catch (ExecutionException error) {
Throwable cause = error.getCause();
if (cause instanceof Exception) throw (Exception) cause;
throw new RuntimeException(cause);
}
}
static final class Data {
Models.Issue issue;
List<Models.Comment> comments;
List<Models.AgentTask> runs;
List<Models.Project> projects;
List<Models.Member> members;
List<Models.Agent> agents;
}
}
@@ -0,0 +1,42 @@
package ai.multica.app;
import java.util.ArrayList;
import java.util.List;
final class IssueDetailSectionOrder {
enum Section {
HERO,
COMPACT_METADATA,
DESCRIPTION,
LATEST_PROGRESS,
SUB_ISSUES,
SUBSCRIBERS,
COMMENTS,
AGENT_WORK_DETAILS,
MORE_DETAILS,
USAGE,
ACTIVITY
}
static List<Section> defaultReadingPath(
boolean hasAgentWork,
boolean hasActivity
) {
ArrayList<Section> sections = new ArrayList<>();
sections.add(Section.HERO);
sections.add(Section.COMPACT_METADATA);
sections.add(Section.DESCRIPTION);
sections.add(Section.LATEST_PROGRESS);
sections.add(Section.SUB_ISSUES);
sections.add(Section.SUBSCRIBERS);
sections.add(Section.COMMENTS);
if (hasAgentWork) sections.add(Section.AGENT_WORK_DETAILS);
sections.add(Section.MORE_DETAILS);
sections.add(Section.USAGE);
if (hasActivity) sections.add(Section.ACTIVITY);
return sections;
}
private IssueDetailSectionOrder() {
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,273 @@
package ai.multica.app;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.view.ViewGroup;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
final class MarkdownRenderer {
private MarkdownRenderer() {}
static final class Palette {
final int blockBackground;
final int tableHeaderBackground;
final int tableCellBackground;
private Palette(int blockBackground, int tableHeaderBackground, int tableCellBackground) {
this.blockBackground = blockBackground;
this.tableHeaderBackground = tableHeaderBackground;
this.tableCellBackground = tableCellBackground;
}
}
static Palette paletteFor(int textColor, int mutedColor, int borderColor) {
boolean darkSurface = luminance(textColor) > luminance(borderColor);
if (darkSurface) {
return new Palette(0xFF1C1C1E, 0xFF1C2638, 0xFF111113);
}
return new Palette(0xFFF3F4F6, 0xFFEFF6FF, 0xFFFFFFFF);
}
static void render(Context context, LinearLayout parent, String markdown, int textColor, int mutedColor, int borderColor) {
parent.removeAllViews();
Palette palette = paletteFor(textColor, mutedColor, borderColor);
if (markdown == null || markdown.trim().isEmpty()) {
TextView empty = text(context, "", 15, textColor);
parent.addView(empty);
return;
}
String[] lines = markdown.replace("\r\n", "\n").split("\n", -1);
List<String> paragraph = new ArrayList<>();
boolean inCode = false;
StringBuilder code = new StringBuilder();
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line.trim().startsWith("```")) {
flushParagraph(context, parent, paragraph, textColor);
if (inCode) {
addCodeBlock(context, parent, code.toString(), mutedColor, palette);
code.setLength(0);
inCode = false;
} else {
inCode = true;
}
continue;
}
if (inCode) {
code.append(line).append('\n');
continue;
}
if (isTableStart(lines, i)) {
flushParagraph(context, parent, paragraph, textColor);
List<String> table = new ArrayList<>();
table.add(lines[i]);
i += 2;
while (i < lines.length && lines[i].trim().startsWith("|") && lines[i].contains("|")) {
table.add(lines[i]);
i++;
}
i--;
addTable(context, parent, table, textColor, mutedColor, borderColor, palette);
continue;
}
String trimmed = line.trim();
if (trimmed.isEmpty()) {
flushParagraph(context, parent, paragraph, textColor);
} else if (trimmed.matches("^(-{3,}|\\*{3,}|_{3,})$")) {
flushParagraph(context, parent, paragraph, textColor);
addDivider(context, parent, borderColor);
} else if (trimmed.startsWith("#")) {
flushParagraph(context, parent, paragraph, textColor);
addHeading(context, parent, trimmed, textColor);
} else if (trimmed.startsWith(">")) {
flushParagraph(context, parent, paragraph, textColor);
TextView quote = text(context, trimmed.replaceFirst("^>\\s?", ""), 15, mutedColor);
quote.setPadding(dp(context, 10), dp(context, 6), dp(context, 8), dp(context, 6));
quote.setBackgroundColor(palette.blockBackground);
parent.addView(quote);
} else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
flushParagraph(context, parent, paragraph, textColor);
TextView bullet = text(context, "" + trimmed.substring(2), 15, textColor);
bullet.setPadding(dp(context, 8), dp(context, 2), 0, dp(context, 2));
parent.addView(bullet);
} else {
paragraph.add(line);
}
}
flushParagraph(context, parent, paragraph, textColor);
if (code.length() > 0) addCodeBlock(context, parent, code.toString(), mutedColor, palette);
}
private static boolean isTableStart(String[] lines, int index) {
if (index + 1 >= lines.length) return false;
String header = lines[index].trim();
String divider = lines[index + 1].trim();
return header.startsWith("|") && header.endsWith("|")
&& divider.matches("^\\|?\\s*:?-{3,}:?\\s*(\\|\\s*:?-{3,}:?\\s*)+\\|?$");
}
private static void flushParagraph(Context context, LinearLayout parent, List<String> paragraph, int textColor) {
if (paragraph.isEmpty()) return;
TextView p = text(context, String.join("\n", paragraph), 15, textColor);
p.setText(applyInline(p.getText().toString()));
p.setMovementMethod(LinkMovementMethod.getInstance());
p.setPadding(0, dp(context, 2), 0, dp(context, 6));
parent.addView(p);
paragraph.clear();
}
private static void addHeading(Context context, LinearLayout parent, String raw, int textColor) {
int level = 0;
while (level < raw.length() && raw.charAt(level) == '#') level++;
String title = raw.substring(level).trim();
TextView h = text(context, title, level <= 1 ? 22 : level == 2 ? 19 : 17, textColor);
h.setTypeface(Typeface.DEFAULT_BOLD);
h.setPadding(0, dp(context, 8), 0, dp(context, 4));
parent.addView(h);
}
private static void addCodeBlock(Context context, LinearLayout parent, String code, int mutedColor, Palette palette) {
TextView tv = text(context, code.trim(), 13, mutedColor);
tv.setTypeface(Typeface.MONOSPACE);
tv.setPadding(dp(context, 10), dp(context, 8), dp(context, 10), dp(context, 8));
tv.setBackgroundColor(palette.blockBackground);
parent.addView(tv);
}
private static void addDivider(Context context, LinearLayout parent, int borderColor) {
View line = new View(context);
line.setBackgroundColor(borderColor);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Math.max(1, dp(context, 1)));
params.setMargins(0, dp(context, 8), 0, dp(context, 8));
parent.addView(line, params);
}
private static void addTable(Context context, LinearLayout parent, List<String> lines, int textColor, int mutedColor, int borderColor, Palette palette) {
HorizontalScrollView scroll = new HorizontalScrollView(context);
TableLayout table = new TableLayout(context);
table.setShrinkAllColumns(false);
table.setStretchAllColumns(false);
for (int r = 0; r < lines.size(); r++) {
String[] cells = splitTable(lines.get(r));
TableRow row = new TableRow(context);
for (String cell : cells) {
TextView tv = text(context, "", 14, r == 0 ? textColor : mutedColor);
tv.setText(applyInline(cell.trim()));
tv.setTypeface(r == 0 ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
tv.setPadding(dp(context, 10), dp(context, 8), dp(context, 10), dp(context, 8));
tv.setBackgroundColor(r == 0 ? palette.tableHeaderBackground : palette.tableCellBackground);
row.addView(tv);
}
table.addView(row);
}
table.setBackgroundColor(borderColor);
table.setPadding(1, 1, 1, 1);
scroll.addView(table);
scroll.setPadding(0, dp(context, 6), 0, dp(context, 8));
parent.addView(scroll, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
static String[] splitTable(String line) {
String trimmed = line.trim();
if (trimmed.startsWith("|")) trimmed = trimmed.substring(1);
if (endsWithUnescapedPipe(trimmed)) trimmed = trimmed.substring(0, trimmed.length() - 1);
List<String> cells = new ArrayList<>();
StringBuilder cell = new StringBuilder();
boolean inCode = false;
for (int i = 0; i < trimmed.length(); i++) {
char ch = trimmed.charAt(i);
if (ch == '`') {
inCode = !inCode;
cell.append(ch);
} else if (ch == '\\' && i + 1 < trimmed.length() && trimmed.charAt(i + 1) == '|') {
cell.append('|');
i++;
} else if (ch == '|' && !inCode) {
cells.add(cell.toString());
cell.setLength(0);
} else {
cell.append(ch);
}
}
cells.add(cell.toString());
return cells.toArray(new String[0]);
}
private static boolean endsWithUnescapedPipe(String value) {
if (!value.endsWith("|")) return false;
int slashCount = 0;
for (int i = value.length() - 2; i >= 0 && value.charAt(i) == '\\'; i--) {
slashCount++;
}
return slashCount % 2 == 0;
}
private static double luminance(int color) {
int red = (color >> 16) & 0xff;
int green = (color >> 8) & 0xff;
int blue = color & 0xff;
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
}
private static TextView text(Context context, String value, int sp, int color) {
TextView tv = new TextView(context);
tv.setText(value);
tv.setTextSize(sp);
tv.setTextColor(color);
tv.setLineSpacing(0, 1.08f);
return tv;
}
private static SpannableStringBuilder applyInline(String value) {
SpannableStringBuilder out = new SpannableStringBuilder();
int i = 0;
while (i < value.length()) {
if (value.startsWith("**", i)) {
int end = value.indexOf("**", i + 2);
if (end > i + 2) {
int start = out.length();
out.append(value, i + 2, end);
out.setSpan(new StyleSpan(Typeface.BOLD), start, out.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
i = end + 2;
continue;
}
}
if (value.charAt(i) == '`') {
int end = value.indexOf('`', i + 1);
if (end > i + 1) {
int start = out.length();
out.append(value, i + 1, end);
out.setSpan(new TypefaceSpan("monospace"), start, out.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
i = end + 1;
continue;
}
}
out.append(value.charAt(i));
i++;
}
return out;
}
static int dp(Context context, int value) {
return Math.round(value * context.getResources().getDisplayMetrics().density);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,89 @@
package ai.multica.app;
import org.json.JSONArray;
import org.json.JSONObject;
final class OnboardingPayload {
private OnboardingPayload() {}
static JSONObject questionnaire(String teamSize, String role, String useCase, String notes) throws Exception {
JSONObject json = new JSONObject();
putTrimmed(json, "team_size", teamSize);
putTrimmed(json, "role", role);
putTrimmed(json, "use_case", useCase);
putTrimmed(json, "notes", notes);
return json;
}
static JSONObject starterContentPayload(String workspaceId, boolean assignToSelf) throws Exception {
JSONObject payload = new JSONObject();
payload.put("workspace_id", workspaceId);
payload.put("project", new JSONObject()
.put("title", "Getting Started with Multica")
.put("description", "A lightweight starter project for learning Multica on mobile.")
.put("icon", "sparkles"));
payload.put("welcome_issue_template", new JSONObject()
.put("title", "Welcome to Multica")
.put("description", "Use this issue to test comments, Markdown, status changes, and agent collaboration.")
.put("priority", "high"));
payload.put("agent_guided_sub_issues", new JSONArray()
.put(starterIssue(
"Ask an agent to summarize your workspace",
"Open the agent picker, assign this issue to an agent, and watch the activity thread update.",
"todo",
"high",
assignToSelf))
.put(starterIssue(
"Review Markdown rendering",
"Add a comment with a table, list, quote, and code block. Confirm the mobile detail page remains readable.",
"todo",
"medium",
assignToSelf))
.put(starterIssue(
"Try Inbox triage",
"Generate a notification, mark it read, then archive it from Inbox.",
"todo",
"medium",
assignToSelf)));
payload.put("self_serve_sub_issues", new JSONArray()
.put(starterIssue(
"Create your first issue",
"Use New Issue to set project, status, priority, and assignee.",
"todo",
"high",
assignToSelf))
.put(starterIssue(
"Organize issues by status",
"Move an issue across the status groups and verify the list stays sorted.",
"todo",
"medium",
assignToSelf))
.put(starterIssue(
"Invite a teammate or add an agent later",
"When you are ready, open Settings to manage members, agents, and runtimes.",
"todo",
"low",
assignToSelf)));
return payload;
}
private static JSONObject starterIssue(
String title,
String description,
String status,
String priority,
boolean assignToSelf) throws Exception {
return new JSONObject()
.put("title", title)
.put("description", description)
.put("status", status)
.put("priority", priority)
.put("assign_to_self", assignToSelf);
}
private static void putTrimmed(JSONObject json, String key, String value) throws Exception {
if (value == null) return;
String trimmed = value.trim();
if (!trimmed.isEmpty()) json.put(key, trimmed);
}
}
@@ -0,0 +1,21 @@
package ai.multica.app;
import java.util.List;
final class RuntimeSectionLoader {
interface Loader<T> {
T load() throws Exception;
}
private RuntimeSectionLoader() {
}
static <T> T load(String label, T fallback, List<String> loadErrors, Loader<T> loader) {
try {
return loader.load();
} catch (Exception error) {
loadErrors.add(label + ": " + (error.getMessage() == null ? error.toString() : error.getMessage()));
return fallback;
}
}
}
@@ -0,0 +1,45 @@
package ai.multica.app;
import java.util.regex.Pattern;
final class TranscriptRedactor {
private static final Rule[] RULES = new Rule[] {
new Rule("\\bAKIA[0-9A-Z]{16}\\b", "[REDACTED AWS KEY]"),
new Rule("(?:aws_secret_access_key|secret_?access_?key)\\s*[=:]\\s*[A-Za-z0-9/+=]{40}", "[REDACTED AWS SECRET]", Pattern.CASE_INSENSITIVE),
new Rule("-----BEGIN[A-Z\\s]*PRIVATE KEY-----[\\s\\S]*?-----END[A-Z\\s]*PRIVATE KEY-----", "[REDACTED PRIVATE KEY]"),
new Rule("\\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}\\b", "[REDACTED GITHUB TOKEN]"),
new Rule("\\bglpat-[A-Za-z0-9_-]{20,}\\b", "[REDACTED GITLAB TOKEN]"),
new Rule("\\bsk-[A-Za-z0-9_-]{20,}\\b", "[REDACTED API KEY]"),
new Rule("\\bxox[bporas]-[A-Za-z0-9-]{10,}\\b", "[REDACTED SLACK TOKEN]"),
new Rule("\\bey[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b", "[REDACTED JWT]"),
new Rule("\\bBearer\\s+[A-Za-z0-9\\-._~+/]+=*", "Bearer [REDACTED]", Pattern.CASE_INSENSITIVE),
new Rule("(?:postgres|mysql|mongodb|redis|amqp)(?:ql)?://[^:\\s]+:[^@\\s]+@", "[REDACTED CONNECTION STRING]@", Pattern.CASE_INSENSITIVE),
new Rule("(?:API_KEY|API_SECRET|SECRET_KEY|SECRET|ACCESS_TOKEN|AUTH_TOKEN|PRIVATE_KEY|DATABASE_URL|DB_PASSWORD|DB_URL|REDIS_URL|PASSWORD|TOKEN)\\s*[=:]\\s*(?!\\[REDACTED)\\S+", "[REDACTED CREDENTIAL]", Pattern.CASE_INSENSITIVE)
};
private TranscriptRedactor() {
}
static String redactSecrets(String text) {
if (text == null || text.isEmpty()) return "";
String result = text;
for (Rule rule : RULES) {
result = rule.pattern.matcher(result).replaceAll(rule.replacement);
}
return result;
}
private static final class Rule {
final Pattern pattern;
final String replacement;
Rule(String regex, String replacement) {
this(regex, replacement, 0);
}
Rule(String regex, String replacement, int flags) {
this.pattern = Pattern.compile(regex, flags);
this.replacement = replacement;
}
}
}
@@ -0,0 +1,18 @@
package ai.multica.app;
import java.util.List;
final class WorkspaceSelection {
private WorkspaceSelection() {
}
static Models.Workspace chooseWorkspace(List<Models.Workspace> workspaces, String preferredWorkspaceId) {
if (workspaces == null || workspaces.isEmpty()) return null;
if (preferredWorkspaceId != null && !preferredWorkspaceId.isEmpty()) {
for (Models.Workspace workspace : workspaces) {
if (preferredWorkspaceId.equals(workspace.id)) return workspace;
}
}
return workspaces.get(0);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,230 @@
package ai.multica.app.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.github.alexzhirkevich.cupertino.ExperimentalCupertinoApi
import io.github.alexzhirkevich.cupertino.theme.CupertinoTheme
object MulticaColors {
var Accent = Color(0xFF2563EB)
private set
var AccentSoft = Color(0xFF1C2638)
private set
var Background = Color.Black
private set
var GroupedBackground = Color.Black
private set
var Surface = Color(0xFF1C1C1E)
private set
var SurfaceElevated = Color(0xFF242426)
private set
var Text = Color(0xFFF5F5F7)
private set
var TextPrimary = Color(0xFFF5F5F7)
private set
var TextSecondary = Color(0xFFA1A1AA)
private set
var TextTertiary = Color(0xFF71717A)
private set
var Muted = Color(0xFF8E8E93)
private set
var Border = Color(0xFF2C2C2E)
private set
var Success = Color(0xFF16A34A)
private set
var Danger = Color(0xFFFF453A)
private set
var Warning = Color(0xFFFF9F0A)
private set
fun applyDarkTheme(darkTheme: Boolean) {
Accent = Color(0xFF2563EB)
Success = Color(0xFF16A34A)
Danger = if (darkTheme) Color(0xFFFF453A) else Color(0xFFDC2626)
Warning = if (darkTheme) Color(0xFFFF9F0A) else Color(0xFFD97706)
if (darkTheme) {
AccentSoft = Color(0xFF1C2638)
Background = Color.Black
GroupedBackground = Color(0xFF0B0B0D)
Surface = Color(0xFF1C1C1E)
SurfaceElevated = Color(0xFF242426)
TextPrimary = Color(0xFFF5F5F7)
TextSecondary = Color(0xFFA1A1AA)
TextTertiary = Color(0xFF71717A)
Text = TextPrimary
Muted = TextSecondary
Border = Color(0xFF2C2C2E)
} else {
AccentSoft = Color(0xFFEAF2FF)
Background = Color(0xFFF6F7F9)
GroupedBackground = Color(0xFFF2F3F6)
Surface = Color.White
SurfaceElevated = Color.White
TextPrimary = Color(0xFF111827)
TextSecondary = Color(0xFF6B7280)
TextTertiary = Color(0xFF9CA3AF)
Text = TextPrimary
Muted = TextSecondary
Border = Color(0xFFE5E7EB)
}
}
}
@Immutable
data class MulticaSpacing(
val page: Dp = 18.dp,
val pageHorizontal: Dp = 20.dp,
val pageTop: Dp = 28.dp,
val sectionTop: Dp = 22.dp,
val rowGap: Dp = 10.dp,
val rowPaddingHorizontal: Dp = 14.dp,
val rowPaddingVertical: Dp = 12.dp,
val compactRowVertical: Dp = 9.dp,
val controlHeight: Dp = 44.dp,
val smallControlHeight: Dp = 34.dp,
val bottomBarHorizontal: Dp = 16.dp,
val pillHorizontal: Dp = 12.dp,
val pillVertical: Dp = 7.dp,
)
val LocalMulticaSpacing = staticCompositionLocalOf { MulticaSpacing() }
private fun multicaLightScheme(): ColorScheme = lightColorScheme(
primary = MulticaColors.Accent,
onPrimary = Color.White,
primaryContainer = MulticaColors.AccentSoft,
onPrimaryContainer = MulticaColors.Accent,
background = MulticaColors.Background,
onBackground = MulticaColors.Text,
surface = MulticaColors.Surface,
onSurface = MulticaColors.Text,
surfaceVariant = Color(0xFFF3F4F6),
onSurfaceVariant = MulticaColors.Muted,
outline = MulticaColors.Border,
error = MulticaColors.Danger,
onError = Color.White,
)
private fun multicaDarkScheme(): ColorScheme = darkColorScheme(
primary = MulticaColors.Accent,
onPrimary = Color.White,
primaryContainer = MulticaColors.AccentSoft,
onPrimaryContainer = Color(0xFF7DB1FF),
background = MulticaColors.Background,
onBackground = MulticaColors.Text,
surface = MulticaColors.Surface,
onSurface = MulticaColors.Text,
surfaceVariant = Color(0xFF242426),
onSurfaceVariant = MulticaColors.Muted,
outline = MulticaColors.Border,
error = MulticaColors.Danger,
onError = Color.White,
)
private val largeTitle = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 34.sp,
lineHeight = 40.sp,
)
private val pageTitle = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
lineHeight = 34.sp,
)
private val rowTitle = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 17.sp,
lineHeight = 22.sp,
)
private val bodyText = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 23.sp,
)
private val subheadline = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 15.sp,
lineHeight = 20.sp,
)
private val captionText = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 13.sp,
lineHeight = 17.sp,
)
private val eyebrowText = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 11.sp,
lineHeight = 14.sp,
)
private val multicaTypography = Typography(
displaySmall = largeTitle,
headlineMedium = pageTitle,
titleMedium = rowTitle,
bodyLarge = bodyText,
bodyMedium = subheadline,
bodySmall = captionText,
labelMedium = eyebrowText,
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 12.sp,
lineHeight = 16.sp,
),
)
private val multicaShapes = Shapes(
extraSmall = RoundedCornerShape(8.dp),
small = RoundedCornerShape(12.dp),
medium = RoundedCornerShape(14.dp),
large = RoundedCornerShape(18.dp),
extraLarge = RoundedCornerShape(24.dp),
)
@Composable
@OptIn(ExperimentalCupertinoApi::class)
fun MulticaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
MulticaColors.applyDarkTheme(darkTheme)
CupertinoTheme(
content = {
MaterialTheme(
colorScheme = if (darkTheme) multicaDarkScheme() else multicaLightScheme(),
typography = multicaTypography,
shapes = multicaShapes,
content = content,
)
},
)
}
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Multica</string>
</resources>
+20
View File
@@ -0,0 +1,20 @@
<resources>
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor">#FFFFFF</item>
<item name="android:navigationBarColor">#FFFFFF</item>
<item name="android:colorAccent">#2563EB</item>
</style>
<style name="ComposeAppTheme" parent="android:style/Theme.Material.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:statusBarColor">#000000</item>
<item name="android:navigationBarColor">#000000</item>
<item name="android:windowBackground">#000000</item>
<item name="android:windowSplashScreenBackground">#000000</item>
<item name="android:windowSplashScreenIconBackgroundColor">#000000</item>
<item name="android:colorAccent">#2563EB</item>
</style>
</resources>